From 6913e600847e41daca5901ace42078d3129b6a2a Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 6 Jan 2026 13:55:57 +0100 Subject: [PATCH 1/3] Use pure FFI to write on service discovery memfd from java 25 --- dd-java-agent/agent-tooling/build.gradle | 54 +++++- .../trace/agent/tooling/TracerInstaller.java | 10 +- .../ForeignMemoryWriterImpl.java | 156 ++++++++++++++++++ .../ForeignMemoryWriterImplTest.java | 56 +++++++ 4 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java create mode 100644 dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index ce04310fd10..dd6df58422f 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -9,6 +9,11 @@ minimumBranchCoverage = 0.6 excludedClassesCoverage += ['datadog.trace.agent.tooling.*'] sourceSets { + register("main_java25") { + java { + srcDirs = [file('src/main/java25')] + } + } register("test_java11") { java { srcDirs = [file('src/test/java11')] @@ -19,6 +24,11 @@ sourceSets { srcDirs = [file('src/test/java21')] } } + register("test_java25") { + java { + srcDirs = [file('src/test/java25')] + } + } named("test") { compileClasspath += sourceSets.test_java11.output runtimeClasspath += sourceSets.test_java11.output @@ -37,8 +47,13 @@ configurations { named("test_java11Implementation") { extendsFrom testImplementation } + + named("test_java25Implementation") { + extendsFrom testImplementation + } } + dependencies { api(project(':dd-java-agent:agent-bootstrap')) { exclude group: 'com.datadoghq', module: 'agent-logging' @@ -53,13 +68,17 @@ dependencies { implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.8.0' api project(':dd-trace-core') - implementation project(':dd-java-agent:agent-crashtracking') + main_java25Implementation project(':dd-trace-core') + testImplementation project(':dd-java-agent:testing') testImplementation libs.bytebuddy testImplementation group: 'com.google.guava', name: 'guava-testlib', version: '20.0' + test_java25Implementation libs.bundles.junit5 + test_java25Implementation sourceSets.main_java25.output + jmhImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE' } @@ -68,6 +87,10 @@ jmh { includeTests = true } +tasks.named("jar") { + from(sourceSets.main_java25.output) +} + tasks.named("compileJava") { dependsOn 'generateClassNameTries' } tasks.named("sourcesJar") { dependsOn 'generateClassNameTries' } @@ -88,6 +111,14 @@ tasks.named("compileTestGroovy") { " otherwise anonymous class has one `loadClass` accessor's signature has `java.lang.Module`" ) } +tasks.named("compileMain_java25Java") { + configureCompiler( + it, + 25, + JavaVersion.VERSION_1_8, + "Java 25 sourceset for Foreign Function & Memory API, compiled with Java 25 but targeting Java 8 bytecode." + ) +} tasks.named("compileTest_java11Java") { configureCompiler(it, 11, JavaVersion.VERSION_11) } @@ -102,6 +133,27 @@ tasks.named("compileTest_java21Java") { ) } +tasks.register("test_java25", Test) { + description = "Runs tests from test_java21 sourceset that require Java 21+" + group = "verification" + testClassesDirs = sourceSets.test_java25.output.classesDirs + classpath = sourceSets.test_java25.runtimeClasspath + useJUnitPlatform() + testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_25 + } + configureCompiler( + it, + 25, + JavaVersion.VERSION_1_8, + "Needs to compile against 25" + ) +} + +tasks.named("test") { + dependsOn(tasks.named("test_java25")) +} + tasks.named("jmh") { dependsOn(tasks.named("compileTestJava")) outputs.upToDateWhen { false } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java index 38387847bd6..c8c8400f574 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java @@ -1,6 +1,7 @@ package datadog.trace.agent.tooling; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.environment.JavaVirtualMachine; import datadog.environment.OperatingSystem; import datadog.trace.api.Config; import datadog.trace.api.GlobalTracer; @@ -63,8 +64,13 @@ private static ServiceDiscovery initServiceDiscovery() { // use reflection to load MemFDUnixWriter so it doesn't get picked up when we // transitively look for all tracer class dependencies to install in GraalVM via // VMRuntimeInstrumentation - Class memFdClass = - Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriter"); + final Class memFdClass; + if (JavaVirtualMachine.isJavaVersionAtLeast(25)) { + memFdClass = + Class.forName("datadog.trace.agent.tooling.servicediscovery.ForeignMemoryWriterImpl"); + } else { + memFdClass = Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriter"); + } ForeignMemoryWriter memFd = (ForeignMemoryWriter) memFdClass.getConstructor().newInstance(); return new ServiceDiscovery(memFd); } catch (Throwable e) { diff --git a/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java b/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java new file mode 100644 index 00000000000..e160a05be59 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java @@ -0,0 +1,156 @@ +package datadog.trace.agent.tooling.servicediscovery; + +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; + +import datadog.environment.OperatingSystem; +import datadog.trace.core.servicediscovery.ForeignMemoryWriter; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ForeignMemoryWriterImpl implements ForeignMemoryWriter { + private static final Logger log = LoggerFactory.getLogger(ForeignMemoryWriterImpl.class); + + // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/memfd.h#L8-L9 + private static final int MFD_CLOEXEC = 0x0001; + private static final int MFD_ALLOW_SEALING = 0x0002; + + // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L40 + private static final int F_ADD_SEALS = 1033; + + // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L46-L49 + private static final int F_SEAL_SEAL = 0x0001; + private static final int F_SEAL_SHRINK = 0x0002; + private static final int F_SEAL_GROW = 0x0004; + + private static final Linker LINKER = Linker.nativeLinker(); + private static final SymbolLookup LIBC = LINKER.defaultLookup(); + + // Function handles - initialized once + private static final MethodHandle SYSCALL; + private static final MethodHandle WRITE; + private static final MethodHandle FCNTL; + + static { + try { + // long syscall(long number, ...) + // Note: variadic functions require special handling, we'll use a fixed signature + SYSCALL = + LINKER.downcallHandle( + LIBC.find("syscall").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, // return type: long + ValueLayout.JAVA_LONG, // syscall number + ValueLayout.ADDRESS, // const char* name + ValueLayout.JAVA_INT // int flags + )); + + // ssize_t write(int fd, const void *buf, size_t count) + WRITE = + LINKER.downcallHandle( + LIBC.find("write").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, // return type: ssize_t + ValueLayout.JAVA_INT, // int fd + ValueLayout.ADDRESS, // const void* buf + ValueLayout.JAVA_LONG // size_t count + )); + + // int fcntl(int fd, int cmd, ... /* arg */) + FCNTL = + LINKER.downcallHandle( + LIBC.find("fcntl").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, // return type: int + ValueLayout.JAVA_INT, // int fd + ValueLayout.JAVA_INT, // int cmd + ValueLayout.JAVA_INT // int arg + )); + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + @Override + public void write(String fileName, byte[] payload) { + OperatingSystem.Architecture arch = OperatingSystem.architecture(); + int memfdSyscall = getMemfdSyscall(arch); + if (memfdSyscall <= 0) { + log.debug(SEND_TELEMETRY, "service discovery not supported for arch={}", arch); + return; + } + + // Use confined arena for memory allocation during the write operation + try (Arena arena = Arena.ofConfined()) { + // Allocate native string for file name + MemorySegment fileNameSegment = arena.allocateFrom(fileName); + + // Call memfd_create via syscall + long memFd = + (long) + SYSCALL.invoke((long) memfdSyscall, fileNameSegment, MFD_CLOEXEC | MFD_ALLOW_SEALING); + + if (memFd < 0) { + log.warn("{} memfd create failed, fd={}", fileName, memFd); + return; + } + + log.debug("{} memfd created (fd={})", fileName, memFd); + + // Allocate native memory for payload + MemorySegment buffer = arena.allocate(payload.length); + MemorySegment.copy(payload, 0, buffer, ValueLayout.JAVA_BYTE, 0, payload.length); + + // Write payload to memfd + long written = (long) WRITE.invoke((int) memFd, buffer, (long) payload.length); + if (written != payload.length) { + log.warn( + "write to {} memfd failed, wrote {} bytes instead of {}", + fileName, + written, + payload.length); + return; + } + + log.debug("wrote {} bytes to memfd {}", written, memFd); + + // Add seals to prevent modification + int returnCode = + (int) FCNTL.invoke((int) memFd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); + + if (returnCode == -1) { + log.warn("failed to add seal to {} memfd", fileName); + return; + } + + // memfd is not closed to keep it readable for the lifetime of the process. + } catch (Throwable t) { + log.error("Error writing to memfd for {}", fileName, t); + } + } + + private static int getMemfdSyscall(OperatingSystem.Architecture arch) { + switch (arch) { + case X64: + // https://github.com/torvalds/linux/blob/v6.17/arch/x86/entry/syscalls/syscall_64.tbl#L331 + return 319; + case X86: + // https://github.com/torvalds/linux/blob/v6.17/arch/x86/entry/syscalls/syscall_32.tbl#L371 + return 356; + case ARM64: + // https://github.com/torvalds/linux/blob/v6.17/scripts/syscall.tbl#L329 + return 279; + case ARM: + // https://github.com/torvalds/linux/blob/v6.17/arch/arm64/tools/syscall_32.tbl#L400 + return 385; + default: + return -1; + } + } +} diff --git a/dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java b/dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java new file mode 100644 index 00000000000..9f3c268ca20 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java @@ -0,0 +1,56 @@ +package datadog.trace.agent.tooling.servicediscovery; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +@EnabledOnOs(OS.LINUX) +class ForeignMemoryWriterImplTest { + + private ForeignMemoryWriterImpl writer; + + @BeforeEach + void setUp() { + writer = new ForeignMemoryWriterImpl(); + } + + @Test + void testWriteCreatesMemFd() throws IOException { + // Given + String fileName = "test-memfd-" + System.currentTimeMillis(); + String testContent = "Hello from Foreign Memory API!"; + byte[] payload = testContent.getBytes(StandardCharsets.UTF_8); + + // When + writer.write(fileName, payload); + + // Then - verify memfd was created by checking /proc/self/fd + // The memfd should be open and readable + Path procSelfFd = Paths.get("/proc/self/fd"); + boolean memfdFound = false; + + try (Stream fdStream = Files.list(procSelfFd)) { + memfdFound = + fdStream.anyMatch( + fd -> { + try { + Path linkTarget = Files.readSymbolicLink(fd); + return linkTarget.toString().contains(fileName); + } catch (IOException e) { + return false; + } + }); + } + + assertTrue(memfdFound, "memfd should be created and visible in /proc/self/fd"); + } +} From 6a0bbf2211f71e21cb4853b19a16c0d57b7bfa98 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 6 Jan 2026 17:23:22 +0100 Subject: [PATCH 2/3] Fix build --- dd-java-agent/agent-tooling/build.gradle | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index dd6df58422f..3f36d2ae95d 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -133,8 +133,17 @@ tasks.named("compileTest_java21Java") { ) } +tasks.named("compileTest_java25Java") { + configureCompiler( + it, + 25, + JavaVersion.VERSION_1_8, + "Java 25 test sourceset, compiled with Java 25 but targeting Java 8 bytecode." + ) +} + tasks.register("test_java25", Test) { - description = "Runs tests from test_java21 sourceset that require Java 21+" + description = "Runs tests from test_java25 sourceset that require Java 25+" group = "verification" testClassesDirs = sourceSets.test_java25.output.classesDirs classpath = sourceSets.test_java25.runtimeClasspath @@ -142,12 +151,6 @@ tasks.register("test_java25", Test) { testJvmConstraints { minJavaVersion = JavaVersion.VERSION_25 } - configureCompiler( - it, - 25, - JavaVersion.VERSION_1_8, - "Needs to compile against 25" - ) } tasks.named("test") { From 808a798a22a19a696ef5a17c6c0b30b02d379825 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 7 Jan 2026 09:35:28 +0100 Subject: [PATCH 3/3] refactor --- dd-java-agent/agent-tooling/build.gradle | 39 +---- .../trace/agent/tooling/TracerInstaller.java | 24 +-- .../servicediscovery/MemFDUnixWriter.java | 42 ++--- .../servicediscovery/MemFDUnixWriterJNA.java | 41 +++++ .../ForeignMemoryWriterImpl.java | 156 ------------------ .../servicediscovery/MemFDUnixWriterFFM.java | 123 ++++++++++++++ .../MemFDUnixWriterTest.java} | 36 ++-- .../ForeignMemoryWriterFactory.java | 51 ++++++ 8 files changed, 267 insertions(+), 245 deletions(-) create mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterJNA.java delete mode 100644 dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java create mode 100644 dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM.java rename dd-java-agent/agent-tooling/src/test/{java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java => java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java} (57%) create mode 100644 dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriterFactory.java diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index 3f36d2ae95d..6f7a73393ff 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -24,16 +24,17 @@ sourceSets { srcDirs = [file('src/test/java21')] } } - register("test_java25") { - java { - srcDirs = [file('src/test/java25')] - } + named("main_java25") { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output } named("test") { compileClasspath += sourceSets.test_java11.output runtimeClasspath += sourceSets.test_java11.output compileClasspath += sourceSets.test_java21.output runtimeClasspath += sourceSets.test_java21.output + compileClasspath += sourceSets.main_java25.output + runtimeClasspath += sourceSets.main_java25.output } } @@ -47,10 +48,6 @@ configurations { named("test_java11Implementation") { extendsFrom testImplementation } - - named("test_java25Implementation") { - extendsFrom testImplementation - } } @@ -76,8 +73,6 @@ dependencies { testImplementation libs.bytebuddy testImplementation group: 'com.google.guava', name: 'guava-testlib', version: '20.0' - test_java25Implementation libs.bundles.junit5 - test_java25Implementation sourceSets.main_java25.output jmhImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE' } @@ -133,30 +128,6 @@ tasks.named("compileTest_java21Java") { ) } -tasks.named("compileTest_java25Java") { - configureCompiler( - it, - 25, - JavaVersion.VERSION_1_8, - "Java 25 test sourceset, compiled with Java 25 but targeting Java 8 bytecode." - ) -} - -tasks.register("test_java25", Test) { - description = "Runs tests from test_java25 sourceset that require Java 25+" - group = "verification" - testClassesDirs = sourceSets.test_java25.output.classesDirs - classpath = sourceSets.test_java25.runtimeClasspath - useJUnitPlatform() - testJvmConstraints { - minJavaVersion = JavaVersion.VERSION_25 - } -} - -tasks.named("test") { - dependsOn(tasks.named("test_java25")) -} - tasks.named("jmh") { dependsOn(tasks.named("compileTestJava")) outputs.upToDateWhen { false } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java index c8c8400f574..1a73b7cdba4 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java @@ -1,7 +1,6 @@ package datadog.trace.agent.tooling; import datadog.communication.ddagent.SharedCommunicationObjects; -import datadog.environment.JavaVirtualMachine; import datadog.environment.OperatingSystem; import datadog.trace.api.Config; import datadog.trace.api.GlobalTracer; @@ -10,9 +9,9 @@ import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; import datadog.trace.core.CoreTracer; import datadog.trace.core.servicediscovery.ForeignMemoryWriter; +import datadog.trace.core.servicediscovery.ForeignMemoryWriterFactory; import datadog.trace.core.servicediscovery.ServiceDiscovery; import datadog.trace.core.servicediscovery.ServiceDiscoveryFactory; -import de.thetaphi.forbiddenapis.SuppressForbidden; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,25 +57,12 @@ private static ServiceDiscoveryFactory serviceDiscoveryFactory() { return TracerInstaller::initServiceDiscovery; } - @SuppressForbidden // intentional use of Class.forName private static ServiceDiscovery initServiceDiscovery() { - try { - // use reflection to load MemFDUnixWriter so it doesn't get picked up when we - // transitively look for all tracer class dependencies to install in GraalVM via - // VMRuntimeInstrumentation - final Class memFdClass; - if (JavaVirtualMachine.isJavaVersionAtLeast(25)) { - memFdClass = - Class.forName("datadog.trace.agent.tooling.servicediscovery.ForeignMemoryWriterImpl"); - } else { - memFdClass = Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriter"); - } - ForeignMemoryWriter memFd = (ForeignMemoryWriter) memFdClass.getConstructor().newInstance(); - return new ServiceDiscovery(memFd); - } catch (Throwable e) { - log.debug("service discovery not supported", e); - return null; + final ForeignMemoryWriter writer = new ForeignMemoryWriterFactory().get(); + if (writer != null) { + return new ServiceDiscovery(writer); } + return null; } public static void installGlobalTracer(final CoreTracer tracer) { diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java index 6a080aafd35..23e13b582fb 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java @@ -2,26 +2,23 @@ import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; -import com.sun.jna.Library; -import com.sun.jna.Memory; import com.sun.jna.Native; -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; import datadog.environment.OperatingSystem; +import datadog.environment.SystemProperties; import datadog.trace.core.servicediscovery.ForeignMemoryWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MemFDUnixWriter implements ForeignMemoryWriter { +abstract class MemFDUnixWriter implements ForeignMemoryWriter { private static final Logger log = LoggerFactory.getLogger(MemFDUnixWriter.class); - private interface LibC extends Library { - int syscall(int number, Object... args); + protected abstract long syscall(long number, String name, int flags); - NativeLong write(int fd, Pointer buf, NativeLong count); + protected abstract long write(int fd, byte[] payload); - int fcntl(int fd, int cmd, int arg); - } + protected abstract int fcntl(int fd, int cmd, int arg); + + protected abstract int getLastError(); // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/memfd.h#L8-L9 private static final int MFD_CLOEXEC = 0x0001; @@ -36,36 +33,33 @@ private interface LibC extends Library { private static final int F_SEAL_GROW = 0x0004; @Override - public void write(String fileName, byte[] payload) { - final LibC libc = Native.load("c", LibC.class); - + public final void write(String fileName, byte[] payload) { OperatingSystem.Architecture arch = OperatingSystem.architecture(); int memfdSyscall = getMemfdSyscall(arch); if (memfdSyscall <= 0) { - log.debug(SEND_TELEMETRY, "service discovery not supported for arch={}", arch); + log.debug( + SEND_TELEMETRY, + "service discovery not supported for arch={}", + SystemProperties.get("os.arch")); return; } - int memFd = libc.syscall(memfdSyscall, fileName, MFD_CLOEXEC | MFD_ALLOW_SEALING); + int memFd = (int) syscall(memfdSyscall, fileName, MFD_CLOEXEC | MFD_ALLOW_SEALING); if (memFd < 0) { - log.warn("{} memfd create failed, errno={}", fileName, Native.getLastError()); + log.warn("{} memfd create failed, errno={}", fileName, getLastError()); return; } log.debug("{} memfd created (fd={})", fileName, memFd); - Memory buf = new Memory(payload.length); - buf.write(0, payload, 0, payload.length); - - NativeLong written = libc.write(memFd, buf, new NativeLong(payload.length)); - if (written.longValue() != payload.length) { + long written = write(memFd, payload); + if (written != payload.length) { log.warn("write to {} memfd failed errno={}", fileName, Native.getLastError()); return; } - log.debug("wrote {} bytes to memfd {}", written.longValue(), memFd); - int returnCode = libc.fcntl(memFd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); + log.debug("wrote {} bytes to memfd {}", written, memFd); + int returnCode = fcntl(memFd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); if (returnCode == -1) { log.warn("failed to add seal to {} memfd errno={}", fileName, Native.getLastError()); - return; } // memfd is not closed to keep it readable for the lifetime of the process. } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterJNA.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterJNA.java new file mode 100644 index 00000000000..6743a39bc08 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterJNA.java @@ -0,0 +1,41 @@ +package datadog.trace.agent.tooling.servicediscovery; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +public final class MemFDUnixWriterJNA extends MemFDUnixWriter { + private final LibC libc = Native.load("c", LibC.class); + + private interface LibC extends Library { + long syscall(long number, Object... args); + + NativeLong write(int fd, Pointer buf, NativeLong count); + + int fcntl(int fd, int cmd, int arg); + } + + @Override + protected long syscall(long number, String name, int flags) { + return libc.syscall(number, name, flags); + } + + @Override + protected long write(int fd, byte[] payload) { + Memory buf = new Memory(payload.length); + buf.write(0, payload, 0, payload.length); + return libc.write(fd, buf, new NativeLong(payload.length)).longValue(); + } + + @Override + protected int fcntl(int fd, int cmd, int arg) { + return libc.fcntl(fd, cmd, arg); + } + + @Override + protected int getLastError() { + return Native.getLastError(); + } +} diff --git a/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java b/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java deleted file mode 100644 index e160a05be59..00000000000 --- a/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImpl.java +++ /dev/null @@ -1,156 +0,0 @@ -package datadog.trace.agent.tooling.servicediscovery; - -import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; - -import datadog.environment.OperatingSystem; -import datadog.trace.core.servicediscovery.ForeignMemoryWriter; -import java.lang.foreign.Arena; -import java.lang.foreign.FunctionDescriptor; -import java.lang.foreign.Linker; -import java.lang.foreign.MemorySegment; -import java.lang.foreign.SymbolLookup; -import java.lang.foreign.ValueLayout; -import java.lang.invoke.MethodHandle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ForeignMemoryWriterImpl implements ForeignMemoryWriter { - private static final Logger log = LoggerFactory.getLogger(ForeignMemoryWriterImpl.class); - - // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/memfd.h#L8-L9 - private static final int MFD_CLOEXEC = 0x0001; - private static final int MFD_ALLOW_SEALING = 0x0002; - - // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L40 - private static final int F_ADD_SEALS = 1033; - - // https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L46-L49 - private static final int F_SEAL_SEAL = 0x0001; - private static final int F_SEAL_SHRINK = 0x0002; - private static final int F_SEAL_GROW = 0x0004; - - private static final Linker LINKER = Linker.nativeLinker(); - private static final SymbolLookup LIBC = LINKER.defaultLookup(); - - // Function handles - initialized once - private static final MethodHandle SYSCALL; - private static final MethodHandle WRITE; - private static final MethodHandle FCNTL; - - static { - try { - // long syscall(long number, ...) - // Note: variadic functions require special handling, we'll use a fixed signature - SYSCALL = - LINKER.downcallHandle( - LIBC.find("syscall").orElseThrow(), - FunctionDescriptor.of( - ValueLayout.JAVA_LONG, // return type: long - ValueLayout.JAVA_LONG, // syscall number - ValueLayout.ADDRESS, // const char* name - ValueLayout.JAVA_INT // int flags - )); - - // ssize_t write(int fd, const void *buf, size_t count) - WRITE = - LINKER.downcallHandle( - LIBC.find("write").orElseThrow(), - FunctionDescriptor.of( - ValueLayout.JAVA_LONG, // return type: ssize_t - ValueLayout.JAVA_INT, // int fd - ValueLayout.ADDRESS, // const void* buf - ValueLayout.JAVA_LONG // size_t count - )); - - // int fcntl(int fd, int cmd, ... /* arg */) - FCNTL = - LINKER.downcallHandle( - LIBC.find("fcntl").orElseThrow(), - FunctionDescriptor.of( - ValueLayout.JAVA_INT, // return type: int - ValueLayout.JAVA_INT, // int fd - ValueLayout.JAVA_INT, // int cmd - ValueLayout.JAVA_INT // int arg - )); - } catch (Throwable t) { - throw new ExceptionInInitializerError(t); - } - } - - @Override - public void write(String fileName, byte[] payload) { - OperatingSystem.Architecture arch = OperatingSystem.architecture(); - int memfdSyscall = getMemfdSyscall(arch); - if (memfdSyscall <= 0) { - log.debug(SEND_TELEMETRY, "service discovery not supported for arch={}", arch); - return; - } - - // Use confined arena for memory allocation during the write operation - try (Arena arena = Arena.ofConfined()) { - // Allocate native string for file name - MemorySegment fileNameSegment = arena.allocateFrom(fileName); - - // Call memfd_create via syscall - long memFd = - (long) - SYSCALL.invoke((long) memfdSyscall, fileNameSegment, MFD_CLOEXEC | MFD_ALLOW_SEALING); - - if (memFd < 0) { - log.warn("{} memfd create failed, fd={}", fileName, memFd); - return; - } - - log.debug("{} memfd created (fd={})", fileName, memFd); - - // Allocate native memory for payload - MemorySegment buffer = arena.allocate(payload.length); - MemorySegment.copy(payload, 0, buffer, ValueLayout.JAVA_BYTE, 0, payload.length); - - // Write payload to memfd - long written = (long) WRITE.invoke((int) memFd, buffer, (long) payload.length); - if (written != payload.length) { - log.warn( - "write to {} memfd failed, wrote {} bytes instead of {}", - fileName, - written, - payload.length); - return; - } - - log.debug("wrote {} bytes to memfd {}", written, memFd); - - // Add seals to prevent modification - int returnCode = - (int) FCNTL.invoke((int) memFd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); - - if (returnCode == -1) { - log.warn("failed to add seal to {} memfd", fileName); - return; - } - - // memfd is not closed to keep it readable for the lifetime of the process. - } catch (Throwable t) { - log.error("Error writing to memfd for {}", fileName, t); - } - } - - private static int getMemfdSyscall(OperatingSystem.Architecture arch) { - switch (arch) { - case X64: - // https://github.com/torvalds/linux/blob/v6.17/arch/x86/entry/syscalls/syscall_64.tbl#L331 - return 319; - case X86: - // https://github.com/torvalds/linux/blob/v6.17/arch/x86/entry/syscalls/syscall_32.tbl#L371 - return 356; - case ARM64: - // https://github.com/torvalds/linux/blob/v6.17/scripts/syscall.tbl#L329 - return 279; - case ARM: - // https://github.com/torvalds/linux/blob/v6.17/arch/arm64/tools/syscall_32.tbl#L400 - return 385; - default: - return -1; - } - } -} diff --git a/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM.java b/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM.java new file mode 100644 index 00000000000..27c3a8f3c44 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java25/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterFFM.java @@ -0,0 +1,123 @@ +package datadog.trace.agent.tooling.servicediscovery; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MemFDUnixWriterFFM extends MemFDUnixWriter { + private static final Logger log = LoggerFactory.getLogger(MemFDUnixWriterFFM.class); + + // Captured call state layout for errno + private static final StructLayout CAPTURE_STATE_LAYOUT = Linker.Option.captureStateLayout(); + private static final long ERRNO_OFFSET = + CAPTURE_STATE_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("errno")); + + // Function handles - initialized once + private final MethodHandle syscallMH; + private final MethodHandle writeMH; + private final MethodHandle fcntlMH; + + private final MemorySegment captureState; + + public MemFDUnixWriterFFM() { + final Linker linker = Linker.nativeLinker(); + final SymbolLookup LIBC = linker.defaultLookup(); + + // Allocate memory for capturing errno (need to be alive until the class instance is collected) + this.captureState = Arena.ofAuto().allocate(CAPTURE_STATE_LAYOUT); + + // long syscall(long number, ...) + // Note: variadic functions require special handling, we'll use a fixed signature + syscallMH = + linker.downcallHandle( + LIBC.find("syscall").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, // return type: long + ValueLayout.JAVA_LONG, // syscall number + ValueLayout.ADDRESS, // const char* name + ValueLayout.JAVA_INT // int flags + ), + Linker.Option.captureCallState("errno")); + + // ssize_t write(int fd, const void *buf, size_t count) + writeMH = + linker.downcallHandle( + LIBC.find("write").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, // return type: ssize_t + ValueLayout.JAVA_INT, // int fd + ValueLayout.ADDRESS, // const void* buf + ValueLayout.JAVA_LONG // size_t count + ), + Linker.Option.captureCallState("errno")); + + // int fcntl(int fd, int cmd, ... /* arg */) + fcntlMH = + linker.downcallHandle( + LIBC.find("fcntl").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, // return type: int + ValueLayout.JAVA_INT, // int fd + ValueLayout.JAVA_INT, // int cmd + ValueLayout.JAVA_INT // int arg + ), + Linker.Option.captureCallState("errno")); + } + + @Override + protected long syscall(long number, String name, int flags) { + try (Arena arena = Arena.ofConfined()) { + // Allocate native string for file name + MemorySegment fileNameSegment = arena.allocateFrom(name); + // Call memfd_create via syscall, passing captureState as first arg + return (long) syscallMH.invoke(captureState, (long) number, fileNameSegment, flags); + } catch (Throwable t) { + log.error("Unable to make a syscall through FFM", t); + return -1; + } + } + + @Override + protected long write(int fd, byte[] payload) { + try (Arena arena = Arena.ofConfined()) { + // Allocate native memory for payload + MemorySegment buffer = arena.allocate(payload.length); + MemorySegment.copy(payload, 0, buffer, ValueLayout.JAVA_BYTE, 0, payload.length); + + // Write payload to memfd, passing captureState as first arg + return (long) writeMH.invoke(captureState, fd, buffer, (long) payload.length); + } catch (Throwable t) { + log.error("Unable to make a write call through FFM", t); + return -1; + } + } + + @Override + protected int fcntl(int fd, int cmd, int arg) { + try { + return (int) fcntlMH.invoke(captureState, fd, cmd, arg); + } catch (Throwable t) { + log.error("Unable to make a fcntl call through FFM", t); + return -1; + } + } + + @Override + protected int getLastError() { + try { + // Read errno from the captured state memory segment + return captureState.get(ValueLayout.JAVA_INT, ERRNO_OFFSET); + } catch (Throwable t) { + log.error("Unable to read errno from captured state", t); + return -1; + } + } +} diff --git a/dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java similarity index 57% rename from dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java rename to dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java index 9f3c268ca20..e41187d92ac 100644 --- a/dd-java-agent/agent-tooling/src/test/java25/datadog/trace/agent/tooling/servicediscovery/ForeignMemoryWriterImplTest.java +++ b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java @@ -1,30 +1,36 @@ package datadog.trace.agent.tooling.servicediscovery; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import datadog.environment.JavaVirtualMachine; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @EnabledOnOs(OS.LINUX) -class ForeignMemoryWriterImplTest { - - private ForeignMemoryWriterImpl writer; - - @BeforeEach - void setUp() { - writer = new ForeignMemoryWriterImpl(); +class MemFDUnixWriterTest { + public static List writerProvider() { + final List ret = new ArrayList<>(); + ret.add(new MemFDUnixWriterJNA()); // JNA is compatible with all the java versions + if (JavaVirtualMachine.isJavaVersionAtLeast(22)) { + ret.add(new MemFDUnixWriterFFM()); // FFM API is GA from java 22. + } + return ret; } - @Test - void testWriteCreatesMemFd() throws IOException { + @ParameterizedTest + @MethodSource("writerProvider") + void testWriteCreatesMemFd(MemFDUnixWriter writer) throws IOException { // Given String fileName = "test-memfd-" + System.currentTimeMillis(); String testContent = "Hello from Foreign Memory API!"; @@ -50,7 +56,13 @@ void testWriteCreatesMemFd() throws IOException { } }); } - assertTrue(memfdFound, "memfd should be created and visible in /proc/self/fd"); } + + @ParameterizedTest + @MethodSource("writerProvider") + void testErrnoWorks(MemFDUnixWriter writer) { + assertEquals(-1, writer.fcntl(-1, 0, 0)); // this call will fail + assertTrue(writer.getLastError() > 0); + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriterFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriterFactory.java new file mode 100644 index 00000000000..6a8ace43b2e --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriterFactory.java @@ -0,0 +1,51 @@ +package datadog.trace.core.servicediscovery; + +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; + +import datadog.environment.JavaVirtualMachine; +import datadog.environment.OperatingSystem; +import datadog.environment.SystemProperties; +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ForeignMemoryWriterFactory implements Supplier { + private static final Logger log = LoggerFactory.getLogger(ForeignMemoryWriterFactory.class); + + @Override + public ForeignMemoryWriter get() { + switch (OperatingSystem.type()) { + case LINUX: + return createForLinux(); + default: + return null; + } + } + + @SuppressForbidden // intentional Class.forName to force loading + private ForeignMemoryWriter createForLinux() { + try { + // first check if the arch is supported + if (OperatingSystem.architecture() == OperatingSystem.Architecture.UNKNOWN) { + log.debug( + SEND_TELEMETRY, + "service discovery not supported for arch={}", + SystemProperties.get("os.arch")); + return null; + } + final Class memFdClass; + if (JavaVirtualMachine.isJavaVersionAtLeast(22)) { + memFdClass = + Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriterFFM"); + } else { + memFdClass = + Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriterJNA"); + } + return (ForeignMemoryWriter) memFdClass.newInstance(); + } catch (Throwable t) { + log.debug("Unable to instantiate foreign memory writer", t); + return null; + } + } +}