From 36f4468c5b43f9d55e01d3b6645365fc3bfb46f2 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 10 Jul 2026 19:16:36 +0200 Subject: [PATCH 1/2] fix(profiler): guard line-number-table copy against stale jmethodID fillJavaMethodInfo() memcpy'd the JVMTI-returned line-number table without probing it first, unlike the sibling class/method name+sig strings hardened in #537. Crash reported in production as SIGSEGV in Lookup::resolveMethod on stock HotSpot JDK 11. --- ddprof-lib/src/main/cpp/counters.h | 1 + ddprof-lib/src/main/cpp/flightRecorder.cpp | 22 ++- .../src/test/cpp/lineNumberTableCopy_ut.cpp | 183 ++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index 6550136646..06532d368e 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -73,6 +73,7 @@ X(UNWINDING_TIME_JVMTI, "unwinding_ticks_jvmti") \ X(CALLTRACE_STORAGE_DROPPED, "calltrace_storage_dropped_traces") \ X(LINE_NUMBER_TABLES, "line_number_tables") \ + X(LINE_NUMBER_TABLE_UNREADABLE, "line_number_table_unreadable") \ X(REMOTE_SYMBOLICATION_FRAMES, "remote_symbolication_frames") \ X(REMOTE_SYMBOLICATION_LIBS_WITH_BUILD_ID, "remote_symbolication_libs_with_build_id") \ X(REMOTE_SYMBOLICATION_BUILD_ID_CACHE_HITS, "remote_symbolication_build_id_cache_hits") \ diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 1bd4ec1bcd..15a2f2d80f 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -348,11 +348,25 @@ void Lookup::fillJavaMethodInfo(MethodInfo *mi, jmethodID method, void *owned_table = nullptr; if (line_number_table_size > 0) { size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry); - owned_table = malloc(bytes); - if (owned_table != nullptr) { - memcpy(owned_table, line_number_table, bytes); + // GetLineNumberTable() is called on the same possibly-stale jmethodID + // that GetMethodDeclaringClass/GetClassSignature/GetMethodName above + // were probed for -- the TOCTOU race documented above (class + // unloaded between sample capture and dump) applies here just as + // much as to those calls, and crash telemetry already showed those + // sibling calls returning JVMTI_ERROR_NONE with unmapped string + // pointers despite the spec saying the returned array should be a + // fresh, caller-owned allocation. Nothing about this call guarantees + // it is exempt from the same failure mode, so probe before copying + // rather than assume the pointer is safe to dereference. + if (SafeAccess::isReadableRange(line_number_table, bytes)) { + owned_table = malloc(bytes); + if (owned_table != nullptr) { + memcpy(owned_table, line_number_table, bytes); + } else { + TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes); + } } else { - TEST_LOG("Failed to allocate %zu bytes for line number table copy", bytes); + Counters::increment(LINE_NUMBER_TABLE_UNREADABLE); } } jvmtiError dealloc_err = jvmti->Deallocate((unsigned char *)line_number_table); diff --git a/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp b/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp new file mode 100644 index 0000000000..c44c3139cc --- /dev/null +++ b/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp @@ -0,0 +1,183 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Reproduces a crash reported in production: +// +// __memcpy_evex_unaligned_erms +// Lookup::resolveMethod(_asgct_callframe&) +// ... +// Recording::writeStackTraces(Buffer*, Lookup*) +// ... +// Profiler::dump / FlightRecorder::dump +// +// Lookup::fillJavaMethodInfo() (inlined into resolveMethod() under -O2/-O3) +// calls jvmtiEnv::GetLineNumberTable(method, ...) and then copied the +// result with an unguarded: +// +// memcpy(owned_table, line_number_table, bytes); +// +// (flightRecorder.cpp now guards this copy with SafeAccess::isReadableRange() +// -- see below -- so the line numbers of the unguarded version are no longer +// present in the file; this file's tests reproduce that removed pattern.) +// +// Unlike the class_name/method_name/method_sig strings returned by the +// preceding JVMTI calls (which ARE probed with SafeAccess::isReadableRange +// before use, see PR #537 / commit ef13aa29f), the line_number_table pointer +// is used directly. +// +// Per the JVMTI spec, GetLineNumberTable() hands back a freshly-allocated, +// caller-owned array (hence the Deallocate() call after the copy) that is +// decoupled from the Method's lifetime -- a spec-compliant implementation +// cannot invalidate it out from under the caller. The actual risk, per the +// comments already in fillJavaMethodInfo, is the TOCTOU race documented +// right there: "GetMethodDeclaringClass may return a jclass wrapping a +// stale/garbage oop when the class was unloaded between sample capture and +// dump" -- a race against class unloading, not tied to any one JVM vendor. +// That same comment block notes crash telemetry showed GetClassSignature/ +// GetMethodName returning JVMTI_ERROR_NONE with unmapped string pointers +// despite the spec saying they should be valid (a separate OpenJ9-specific +// jmethodID-corruption mode is also handled a few lines above, but is not +// the only source of this). The production crash motivating this file was +// on a stock HotSpot JDK 11, confirming the race is not OpenJ9-specific. +// GetLineNumberTable() is called on the exact same jmethodID as +// GetMethodDeclaringClass/GetClassSignature/GetMethodName, with nothing +// about it that would exempt it from that same observed failure mode. +// +// These tests isolate the copy step from JVMTI/JNI (which fillJavaMethodInfo +// requires and which is impractical to fake in a plain gtest) by exercising +// the exact copy pattern against a pointer whose backing memory is gone -- +// standing in for "GetLineNumberTable() returned a bad pointer" regardless +// of the precise reason. The result at the memcpy call site is identical +// either way, so this is sufficient to prove both the crash and the fix. + +#include +#include +#include +#include +#include + +#include "flightRecorder.h" +#include "os.h" +#include "safeAccess.h" + +namespace { + +// SafeAccess::isReadableRange()/isReadable() rely on a SIGSEGV handler +// registered via OS::replaceSigsegvHandler() to catch the fault at a known +// trampoline address and turn it into a safe return value (see +// safefetch_ut.cpp for the same pattern). Without it, a fault inside the +// safefetch trampoline is an ordinary unhandled SIGSEGV. +void (*orig_segvHandler)(int signo, siginfo_t *siginfo, void *ucontext); + +void lineNumberTableSegvHandler(int signo, siginfo_t *siginfo, void *context) { + if (!SafeAccess::handle_safefetch(signo, context)) { + if (orig_segvHandler != nullptr) { + orig_segvHandler(signo, siginfo, context); + } + } +} + +class LineNumberTableCopyTest : public ::testing::Test { +protected: + void SetUp() override { + orig_segvHandler = OS::replaceSigsegvHandler(lineNumberTableSegvHandler); + } + + void TearDown() override { OS::replaceSigsegvHandler(orig_segvHandler); } +}; + +// Allocates a page-sized region seeded with `count` jvmtiLineNumberEntry +// records, standing in for a buffer jvmtiEnv::GetLineNumberTable() would +// have returned. +jvmtiLineNumberEntry *makeFakeLineNumberTable(void *page, int count) { + jvmtiLineNumberEntry *table = (jvmtiLineNumberEntry *)page; + for (int i = 0; i < count; i++) { + table[i].start_location = i * 4; + table[i].line_number = i + 1; + } + return table; +} + +} // namespace + +// Reproducer: the code shape that used to be in flightRecorder.cpp's +// fillJavaMethodInfo() before the fix, with no readability guard before the +// memcpy. Demonstrates that once the source page is gone, the copy step used +// by resolveMethod() crashes the process rather than failing gracefully. +TEST(LineNumberTableCopyRawTest, UnguardedCopyCrashesWhenSourceUnmapped) { + EXPECT_DEATH( + { + long page_size = sysconf(_SC_PAGESIZE); + void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (page == MAP_FAILED) { + _exit(1); // treat as death via non-zero exit if mmap itself fails + } + jint line_number_table_size = 4; + jvmtiLineNumberEntry *line_number_table = + makeFakeLineNumberTable(page, line_number_table_size); + + // Stand-in for GetLineNumberTable() handing back a bad pointer for a + // corrupted/stale jmethodID: the backing memory is simply gone by + // the time the copy runs. + munmap(page, page_size); + + // This mirrors the pre-fix fillJavaMethodInfo() shape verbatim: no + // readability check on line_number_table before the copy. + size_t bytes = + (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry); + void *owned_table = malloc(bytes); + memcpy(owned_table, line_number_table, bytes); // <-- crashes here + free(owned_table); + }, + ""); +} + +// Documents the fix: guarding the same copy with +// SafeAccess::isReadableRange() (the same primitive already used to probe +// class_name/method_name/method_sig in fillJavaMethodInfo) turns the crash +// into a clean, detectable failure with no memory touched past the guard. +TEST_F(LineNumberTableCopyTest, GuardedCopySkipsSafelyWhenSourceUnmapped) { + long page_size = sysconf(_SC_PAGESIZE); + void *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(page, MAP_FAILED); + + jint line_number_table_size = 4; + jvmtiLineNumberEntry *line_number_table = + makeFakeLineNumberTable(page, line_number_table_size); + + ASSERT_EQ(0, munmap(page, page_size)); + + size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry); + void *owned_table = nullptr; + if (SafeAccess::isReadableRange(line_number_table, bytes)) { + owned_table = malloc(bytes); + memcpy(owned_table, line_number_table, bytes); + } + + EXPECT_EQ(nullptr, owned_table); + free(owned_table); +} + +// Sanity check: the guard must not reject a genuinely valid table, or every +// real dump would silently lose line-number info. +TEST_F(LineNumberTableCopyTest, GuardedCopyStillWorksForValidSource) { + jint line_number_table_size = 8; + jvmtiLineNumberEntry stack_table[8]; + jvmtiLineNumberEntry *line_number_table = + makeFakeLineNumberTable(stack_table, line_number_table_size); + + size_t bytes = (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry); + void *owned_table = nullptr; + if (SafeAccess::isReadableRange(line_number_table, bytes)) { + owned_table = malloc(bytes); + memcpy(owned_table, line_number_table, bytes); + } + + ASSERT_NE(nullptr, owned_table); + EXPECT_EQ(0, memcmp(owned_table, line_number_table, bytes)); + free(owned_table); +} From e5ef83e27614101e69defb0c599585f0e28b1ebd Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 10 Jul 2026 20:40:51 +0200 Subject: [PATCH 2/2] Fix Release-build flakiness in line-number-table death test; add jmethodID-churn stress test The EXPECT_DEATH reproducer silently passed on -O3 builds because the optimizer eliminated the dead memcpy/free once owned_table's contents were never observed; a forced volatile read fixes that. Also adds a JUnit stress test that races class-unload churn against profiler dump/resolve and asserts native whitebox counters actually observed a stale jmethodID, not just that the JVM didn't crash. Co-Authored-By: Claude Sonnet 5 --- .../src/test/cpp/lineNumberTableCopy_ut.cpp | 6 + .../JMethodIDInvalidationStressTest.java | 306 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/memleak/JMethodIDInvalidationStressTest.java diff --git a/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp b/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp index c44c3139cc..cad17a78b1 100644 --- a/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp +++ b/ddprof-lib/src/test/cpp/lineNumberTableCopy_ut.cpp @@ -130,6 +130,12 @@ TEST(LineNumberTableCopyRawTest, UnguardedCopyCrashesWhenSourceUnmapped) { (size_t)line_number_table_size * sizeof(jvmtiLineNumberEntry); void *owned_table = malloc(bytes); memcpy(owned_table, line_number_table, bytes); // <-- crashes here + // Force the copied bytes to be observed before free(); otherwise an + // optimizing (Release) build can prove owned_table's contents are + // never read and eliminate the memcpy as dead code, silently + // skipping the very fault this test exists to demonstrate. + volatile unsigned char sink = *(volatile unsigned char *)owned_table; + (void)sink; free(owned_table); }, ""); diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/JMethodIDInvalidationStressTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/JMethodIDInvalidationStressTest.java new file mode 100644 index 0000000000..16278bcb8a --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/memleak/JMethodIDInvalidationStressTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2026, Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.profiler.memleak; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exploratory stress test for jmethodID invalidation, motivated by PROF-15385 (SIGSEGV in + * {@code Lookup::fillJavaMethodInfo} copying a JVMTI line-number table for a stale jmethodID, + * fixed by guarding that copy with {@code SafeAccess::isReadableRange}). + * + *

That fix addressed one call site. This test does not target a specific call site; it tries + * to manufacture the same underlying condition — jmethodIDs whose declaring class is unloaded + * while the profiler is actively resolving/dumping — at high enough concurrency and volume that + * any *other*, currently-unguarded, use of a stale jmethodID has a chance to surface on its own. + * + *

Approach: + *

    + *
  • Several driver threads continuously define throwaway classes in fresh, disposable + * {@link ClassLoader}s, invoke their methods a few times (so the jmethodIDs are actually + * captured by the profiler), then drop every reference.
  • + *
  • A dedicated thread calls {@code System.gc()} on a tight loop for the duration of the + * test, to encourage the JVM to actually unload those discarded classes/loaders while the + * other threads are still running — rather than letting them merely accumulate.
  • + *
  • The main thread calls {@code profiler.dump()} repeatedly throughout, so + * {@code writeStackTraces}/{@code Lookup::resolveMethod}/{@code cleanupUnreferencedMethods} + * run concurrently with class unloading, not just class-loading.
  • + *
  • Both the {@code cpu} and {@code alloc} engines are active, since each has its own path + * into {@code Lookup} and its own timing relative to unload.
  • + *
+ * + *

Pass/fail signal: a SIGSEGV/SIGABRT kills the whole JVM, so JUnit would never get to + * report a failure directly -- reaching {@code profiler.stop()} at all is one necessary signal. + * But that alone would pass vacuously if the churn never actually raced class unload against + * {@code Lookup::resolveMethod}/{@code fillJavaMethodInfo}. To rule that out, this test reads the + * native whitebox counters ({@code JavaProfiler#getDebugCounters()}) for {@code + * jmethodid_skipped_count} and {@code line_number_table_unreadable} -- both are incremented in + * {@code fillJavaMethodInfo} exactly when {@code SafeAccess::isReadableRange} rejects a stale + * jmethodID's class/method metadata or line-number table (see flightRecorder.cpp) -- and asserts + * at least one of them increased during the churn window. A pass therefore means both "no crash" + * and "a stale jmethodID was actually observed and safely guarded." We cannot run this under ASan + * here, since the profiler under test must run as a live JVMTI agent inside the same JVM process + * the test is driving, not as a standalone ASan-instrumented binary the way the gtest-level native + * unit tests can. + * + *

Limitations: class unloading is JVM-discretionary and best-effort here — this test + * does not wait for or verify that any particular class actually unloads (unlike + * {@code CleanupAfterClassUnloadTest}/{@code WriteStackTracesAfterClassUnloadTest}, which target + * one specific, already-diagnosed race and gate on a confirmed unload). It relies on volume and + * concurrency to make unload-during-use likely across many call sites at once, but a clean run + * is evidence of nothing more than "no crash was hit this time" for whatever code paths happened + * to be exercised. + */ +public class JMethodIDInvalidationStressTest extends AbstractDynamicClassTest { + + private static final int CHURN_THREADS = 4; + private static final long DURATION_MILLIS = 5_000; + private static final AtomicLong CLASS_COUNTER = new AtomicLong(); + private static final AtomicLong CHURN_ITERATIONS = new AtomicLong(); + + @Override + protected String getProfilerCommand() { + return "cpu=1ms,alloc=512k"; + } + + @Test + @Timeout(60) + public void testProfilerSurvivesConcurrentClassUnloadDuringDump() throws Exception { + stopProfiler(); + + Path baseFile = tempFile("jmethodid-churn-base"); + Path dumpFile = tempFile("jmethodid-churn-dump"); + + AtomicBoolean running = new AtomicBoolean(true); + List churnThreads = new ArrayList<>(); + Thread gcThread = null; + + try { + profiler.execute( + "start," + getProfilerCommand() + ",jfr,mcleanup=true,file=" + baseFile.toAbsolutePath()); + Thread.sleep(200); // let the profiler stabilize + + Map before = profiler.getDebugCounters(); + + for (int i = 0; i < CHURN_THREADS; i++) { + Thread t = new Thread(() -> churnLoop(running), "jmethodid-churn-" + i); + t.setDaemon(true); + churnThreads.add(t); + t.start(); + } + + gcThread = new Thread(() -> { + while (running.get()) { + System.gc(); + try { + Thread.sleep(20); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + }, "jmethodid-churn-gc"); + gcThread.setDaemon(true); + gcThread.start(); + + // Drive dumps from the main thread for the whole churn window so + // writeStackTraces/resolveMethod/cleanupUnreferencedMethods run concurrently + // with class unloading, not just after it. + long deadline = System.currentTimeMillis() + DURATION_MILLIS; + int dumps = 0; + while (System.currentTimeMillis() < deadline) { + profiler.dump(dumpFile); + dumps++; + Thread.sleep(50); + } + + running.set(false); + for (Thread t : churnThreads) { + t.join(5_000); + } + gcThread.join(5_000); + + // Reaching this line means the profiler survived the whole churn window. + Map after = profiler.getDebugCounters(); + profiler.stop(); + + assertTrue(Files.size(dumpFile) > 0, + "Profiler produced no output after " + dumps + " dumps under jmethodID churn"); + + long churnIterations = CHURN_ITERATIONS.get(); + assertTrue(churnIterations > 0, + "Churn threads never completed a single load/invoke/discard iteration -- the " + + "counter-delta assertion below would be meaningless without this, since it " + + "can't tell 'no stale jmethodID was hit' apart from 'churn never ran at all' " + + "(e.g. a regression in generateChurnClassBytecode or IsolatedClassLoader)."); + + long skippedDelta = after.getOrDefault("jmethodid_skipped_count", 0L) + - before.getOrDefault("jmethodid_skipped_count", 0L); + long unreadableLineTableDelta = after.getOrDefault("line_number_table_unreadable", 0L) + - before.getOrDefault("line_number_table_unreadable", 0L); + + assertTrue(skippedDelta > 0 || unreadableLineTableDelta > 0, + "Churn window completed without the profiler ever encountering a stale/unmapped " + + "jmethodID (jmethodid_skipped_count delta=" + skippedDelta + + ", line_number_table_unreadable delta=" + unreadableLineTableDelta + + ") -- this test is meant to prove the guard actually fires under class-unload " + + "churn, not just that the JVM didn't crash; if this keeps failing, the churn " + + "isn't racing unload against resolveMethod/fillJavaMethodInfo tightly enough " + + "(consider more churn threads or a longer window)."); + } finally { + running.set(false); + for (Thread t : churnThreads) { + try { + t.join(2_000); + } catch (InterruptedException ignored) { + } + } + if (gcThread != null) { + try { + gcThread.join(2_000); + } catch (InterruptedException ignored) { + } + } + try { + profiler.stop(); + } catch (Exception ignored) { + } + try { + Files.deleteIfExists(baseFile); + } catch (IOException ignored) { + } + try { + Files.deleteIfExists(dumpFile); + } catch (IOException ignored) { + } + } + } + + private void churnLoop(AtomicBoolean running) { + while (running.get()) { + try { + String className = + "com/datadoghq/profiler/generated/JMethodIDChurn" + CLASS_COUNTER.incrementAndGet(); + byte[] bytecode = generateChurnClassBytecode(className); + + IsolatedClassLoader loader = new IsolatedClassLoader(); + Class clazz = loader.defineClass(className.replace('/', '.'), bytecode); + Object instance = clazz.getDeclaredConstructor().newInstance(); + Method compute = clazz.getDeclaredMethod("compute", int.class); + Method allocate = clazz.getDeclaredMethod("allocate", int.class); + + for (int i = 0; i < 20; i++) { + compute.invoke(instance, i); + allocate.invoke(instance, i); + } + // loader/clazz/instance/compute/allocate go out of scope here with no other + // references -- eligible for unload as soon as the GC-pressure thread runs. + CHURN_ITERATIONS.incrementAndGet(); + } catch (Throwable t) { + // A reflective/loading failure here is not itself a signal; the failure mode + // this test watches for is a JVM crash, not a Java-level exception. But we + // still need churn to actually be happening for the counter-delta assertion + // below to mean anything -- CHURN_ITERATIONS tracks that separately. + } + } + } + + /** + * Generates a class with a {@code compute(int)} method (multi-line-number arithmetic, so + * {@code GetLineNumberTable} on it returns more than one entry) and an {@code allocate(int)} + * method (array allocation, so the alloc engine's stack capture includes this jmethodID too). + */ + private byte[] generateChurnClassBytecode(String internalName) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null); + + MethodVisitor ctor = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + ctor.visitCode(); + ctor.visitVarInsn(Opcodes.ALOAD, 0); + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + ctor.visitInsn(Opcodes.RETURN); + ctor.visitMaxs(0, 0); + ctor.visitEnd(); + + // public int compute(int x) { + // int r = x; // line 10 + // r = r*1103515245 + 12345; // line 11 + // return r; // line 12 + // } + MethodVisitor compute = cw.visitMethod(Opcodes.ACC_PUBLIC, "compute", "(I)I", null, null); + compute.visitCode(); + Label l1 = new Label(); + compute.visitLabel(l1); + compute.visitLineNumber(10, l1); + compute.visitVarInsn(Opcodes.ILOAD, 1); + compute.visitVarInsn(Opcodes.ISTORE, 2); + Label l2 = new Label(); + compute.visitLabel(l2); + compute.visitLineNumber(11, l2); + compute.visitVarInsn(Opcodes.ILOAD, 2); + compute.visitLdcInsn(1103515245); + compute.visitInsn(Opcodes.IMUL); + compute.visitLdcInsn(12345); + compute.visitInsn(Opcodes.IADD); + compute.visitVarInsn(Opcodes.ISTORE, 2); + Label l3 = new Label(); + compute.visitLabel(l3); + compute.visitLineNumber(12, l3); + compute.visitVarInsn(Opcodes.ILOAD, 2); + compute.visitInsn(Opcodes.IRETURN); + compute.visitMaxs(0, 0); + compute.visitEnd(); + + // public int[] allocate(int seed) { + // int[] a = new int[16]; + // a[0] = seed; + // return a; + // } + MethodVisitor allocate = cw.visitMethod(Opcodes.ACC_PUBLIC, "allocate", "(I)[I", null, null); + allocate.visitCode(); + allocate.visitIntInsn(Opcodes.BIPUSH, 16); + allocate.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_INT); + allocate.visitVarInsn(Opcodes.ASTORE, 2); + allocate.visitVarInsn(Opcodes.ALOAD, 2); + allocate.visitInsn(Opcodes.ICONST_0); + allocate.visitVarInsn(Opcodes.ILOAD, 1); + allocate.visitInsn(Opcodes.IASTORE); + allocate.visitVarInsn(Opcodes.ALOAD, 2); + allocate.visitInsn(Opcodes.ARETURN); + allocate.visitMaxs(0, 0); + allocate.visitEnd(); + + cw.visitEnd(); + return cw.toByteArray(); + } +}