diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index ce04310fd10..6f7a73393ff 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,11 +24,17 @@ sourceSets { srcDirs = [file('src/test/java21')] } } + 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 } } @@ -39,6 +50,7 @@ configurations { } } + dependencies { api(project(':dd-java-agent:agent-bootstrap')) { exclude group: 'com.datadoghq', module: 'agent-logging' @@ -53,13 +65,15 @@ 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' + jmhImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE' } @@ -68,6 +82,10 @@ jmh { includeTests = true } +tasks.named("jar") { + from(sourceSets.main_java25.output) +} + tasks.named("compileJava") { dependsOn 'generateClassNameTries' } tasks.named("sourcesJar") { dependsOn 'generateClassNameTries' } @@ -88,6 +106,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) } 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..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 @@ -9,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; @@ -57,20 +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 - Class 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/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/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java new file mode 100644 index 00000000000..e41187d92ac --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriterTest.java @@ -0,0 +1,68 @@ +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.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 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; + } + + @ParameterizedTest + @MethodSource("writerProvider") + void testWriteCreatesMemFd(MemFDUnixWriter writer) 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"); + } + + @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; + } + } +}