diff --git a/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java b/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java index 81d4fd259ed..3fd013d9a64 100644 --- a/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java +++ b/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java @@ -28,12 +28,13 @@ import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.spi.AttachProvider; -import java.io.InputStream; -import java.io.IOException; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.Files; +import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; @@ -46,13 +47,35 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { // location is the same for all processes, otherwise the tools // will not be able to find all Hotspot processes. // Any changes to this needs to be synchronized with HotSpot. - private static final String tmpdir = "/tmp"; + private static final Path TMPDIR = Path.of("/tmp"); + + private static final Path PROC = Path.of("/proc"); + private static final Path NS_MNT = Path.of("ns/mnt"); + private static final Path NS_PID = Path.of("ns/pid"); + private static final Path SELF = PROC.resolve("self"); + private static final Path STATUS = Path.of("status"); + private static final Path ROOT_TMP = Path.of("root/tmp"); + + private static final Optional SELF_MNT_NS; + + static { + Path nsPath = null; + + try { + nsPath = Files.readSymbolicLink(SELF.resolve(NS_MNT)); + } catch (IOException e) { + // do nothing + } finally { + SELF_MNT_NS = Optional.ofNullable(nsPath); + } + } + String socket_path; + /** * Attaches to the target VM */ - VirtualMachineImpl(AttachProvider provider, String vmid) - throws AttachNotSupportedException, IOException + VirtualMachineImpl(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException { super(provider, vmid); @@ -63,12 +86,12 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { } // Try to resolve to the "inner most" pid namespace - int ns_pid = getNamespacePid(pid); + final long ns_pid = getNamespacePid(pid); // Find the socket file. If not found then we attempt to start the // attach mechanism in the target VM by sending it a QUIT signal. // Then we attempt to find the socket file again. - File socket_file = findSocketFile(pid, ns_pid); + final File socket_file = findSocketFile(pid, ns_pid); socket_path = socket_file.getPath(); if (!socket_file.exists()) { // Keep canonical version of File, to delete, in case target process ends and /proc link has gone: @@ -210,49 +233,102 @@ protected void close(long fd) throws IOException { } // Return the socket file for the given process. - private File findSocketFile(int pid, int ns_pid) throws IOException { - String root = findTargetProcessTmpDirectory(pid, ns_pid); - return new File(root, ".java_pid" + ns_pid); + private File findSocketFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + return new File(findTargetProcessTmpDirectory(pid, ns_pid), ".java_pid" + ns_pid); } // On Linux a simple handshake is used to start the attach mechanism // if not already started. The client creates a .attach_pid file in the // target VM's working directory (or temp directory), and the SIGQUIT handler // checks for the file. - private File createAttachFile(int pid, int ns_pid) throws IOException { - String fn = ".attach_pid" + ns_pid; - String path = "/proc/" + pid + "/cwd/" + fn; - File f = new File(path); + private File createAttachFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + Path fn = Path.of(".attach_pid" + ns_pid); + Path path = PROC.resolve(Path.of(Long.toString(pid), "cwd")).resolve(fn); + File f = new File(path.toString()); try { // Do not canonicalize the file path, or we will fail to attach to a VM in a container. f.createNewFile(); - } catch (IOException x) { - String root = findTargetProcessTmpDirectory(pid, ns_pid); - f = new File(root, fn); + } catch (IOException e) { + f = new File(findTargetProcessTmpDirectory(pid, ns_pid), fn.toString()); f.createNewFile(); } return f; } - private String findTargetProcessTmpDirectory(int pid, int ns_pid) throws IOException { - String root; - if (pid != ns_pid) { - // A process may not exist in the same mount namespace as the caller, e.g. - // if we are trying to attach to a JVM process inside a container. - // Instead, attach relative to the target root filesystem as exposed by - // procfs regardless of namespaces. - String procRootDirectory = "/proc/" + pid + "/root"; - if (!Files.isReadable(Path.of(procRootDirectory))) { - throw new IOException( - String.format("Unable to access root directory %s " + - "of target process %d", procRootDirectory, pid)); + private String findTargetProcessTmpDirectory(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + // We need to handle at least 4 different cases: + // 1. Caller and target processes share PID namespace and root filesystem (host to host or container to + // container with both /tmp mounted between containers). + // 2. Caller and target processes share PID namespace and root filesystem but the target process has elevated + // privileges (host to host). + // 3. Caller and target processes share PID namespace but NOT root filesystem (container to container). + // 4. Caller and target processes share neither PID namespace nor root filesystem (host to container). + + Optional target = ProcessHandle.of(pid); + Optional ph = target; + long nsPid = ns_pid; + Optional prevPidNS = Optional.empty(); + + while (ph.isPresent()) { + final var curPid = ph.get().pid(); + final var procPidPath = PROC.resolve(Long.toString(curPid)); + Optional targetMountNS = Optional.empty(); + + try { + // attempt to read the target's mnt ns id + targetMountNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_MNT))); + } catch (IOException e) { + // if we fail to read the target's mnt ns id then we either don't have access or it no longer exists! + if (!Files.exists(procPidPath)) { + throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid)); + } + // the process still exists, but we don't have privileges to read its procfs } - root = procRootDirectory + "/" + tmpdir; + final var sameMountNS = SELF_MNT_NS.isPresent() && SELF_MNT_NS.equals(targetMountNS); + + if (sameMountNS) { + return TMPDIR.toString(); // we share TMPDIR in common! + } else { + // we could not read the target's mnt ns + final var procPidRootTmp = procPidPath.resolve(ROOT_TMP); + if (Files.isReadable(procPidRootTmp)) { + return procPidRootTmp.toString(); // not in the same mnt ns but tmp is accessible via /proc + } + } + + // let's attempt to obtain the pid ns, best efforts to avoid crossing pid ns boundaries (as with a container) + Optional curPidNS = Optional.empty(); + + try { + // attempt to read the target's pid ns id + curPidNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_PID))); + } catch (IOException e) { + // if we fail to read the target's pid ns id then we either don't have access or it no longer exists! + if (!Files.exists(procPidPath)) { + throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid)); + } + // the process still exists, but we don't have privileges to read its procfs + } + + // recurse "up" the process hierarchy if appropriate. PID 1 cannot have a parent in the same namespace + final var havePidNSes = prevPidNS.isPresent() && curPidNS.isPresent(); + final var ppid = ph.get().parent(); + + if (ppid.isPresent() && (havePidNSes && curPidNS.equals(prevPidNS)) || (!havePidNSes && nsPid > 1)) { + ph = ppid; + nsPid = getNamespacePid(ph.get().pid()); // get the ns pid of the parent + prevPidNS = curPidNS; + } else { + ph = Optional.empty(); + } + } + + if (target.orElseThrow(AttachNotSupportedException::new).isAlive()) { + return TMPDIR.toString(); // fallback... } else { - root = tmpdir; + throw new IOException(String.format("unable to attach, process: %d terminated", pid)); } - return root; } /* @@ -269,13 +345,12 @@ private void writeString(int fd, String s) throws IOException { write(fd, b, 0, 1); } - // Return the inner most namespaced PID if there is one, // otherwise return the original PID. - private int getNamespacePid(int pid) throws AttachNotSupportedException, IOException { + private long getNamespacePid(long pid) throws AttachNotSupportedException, IOException { // Assuming a real procfs sits beneath, reading this doesn't block // nor will it consume a lot of memory. - String statusFile = "/proc/" + pid + "/status"; + final var statusFile = PROC.resolve(Long.toString(pid)).resolve(STATUS).toString(); File f = new File(statusFile); if (!f.exists()) { return pid; // Likely a bad pid, but this is properly handled later. @@ -291,8 +366,7 @@ private int getNamespacePid(int pid) throws AttachNotSupportedException, IOExcep // The last entry represents the PID the JVM "thinks" it is. // Even in non-namespaced pids these entries should be // valid. You could refer to it as the inner most pid. - int ns_pid = Integer.parseInt(parts[parts.length - 1]); - return ns_pid; + return Long.parseLong(parts[parts.length - 1]); } } // Old kernels may not have NSpid field (i.e. 3.10). diff --git a/test/hotspot/jtreg/containers/docker/TestJcmdWithSideCar.java b/test/hotspot/jtreg/containers/docker/TestJcmdWithSideCar.java index 643ad390dff..ecc903a6e7a 100644 --- a/test/hotspot/jtreg/containers/docker/TestJcmdWithSideCar.java +++ b/test/hotspot/jtreg/containers/docker/TestJcmdWithSideCar.java @@ -39,12 +39,19 @@ * @build EventGeneratorLoop * @run driver TestJcmdWithSideCar */ +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Paths; import java.util.Arrays; import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.stream.Collectors; import jdk.test.lib.Container; import jdk.test.lib.Utils; @@ -61,6 +68,31 @@ public class TestJcmdWithSideCar { private static final long TIME_TO_WAIT_FOR_MAIN_METHOD_START = 50 * 1000; // milliseconds private static final String MAIN_CONTAINER_NAME = "test-container-main"; + private static final String UID = "uid"; + private static final String GID = "gid"; + + private static final Pattern ID_PATTERN = Pattern.compile("uid=(?<" + UID + ">\\d+)\\([^\\)]+\\)\\s+gid=(?<" + GID + ">\\d+).*"); + + private static final Optional USER = ProcessHandle.current().info().user().map( + user -> { + try (var br = new BufferedReader(new InputStreamReader(new ProcessBuilder("id", user).start().getInputStream()))) { + for (final var line : br.lines().toList()) { + final var m = ID_PATTERN.matcher(line); + + if (m.matches()) { + return "--user=" + m.group(UID) + ":" + m.group(GID); + } + } + } catch (IOException e) { + // do nothing... + } + + return null; + } + ); + + private static final String NET_BIND_SERVICE = "--cap-add=NET_BIND_SERVICE"; + public static void main(String[] args) throws Exception { if (!DockerTestUtils.canTestDocker()) { return; @@ -69,24 +101,28 @@ public static void main(String[] args) throws Exception { DockerTestUtils.buildJdkContainerImage(IMAGE_NAME); try { - // Start the loop process in the "main" container, then run test cases - // using a sidecar container. - MainContainer mainContainer = new MainContainer(); - mainContainer.start(); - mainContainer.waitForMainMethodStart(TIME_TO_WAIT_FOR_MAIN_METHOD_START); - - long mainProcPid = testCase01(); - - // Excluding the test case below until JDK-8228850 is fixed - // JDK-8228850: jhsdb jinfo fails with ClassCastException: - // s.j.h.oops.TypeArray cannot be cast to s.j.h.oops.Instance - // mainContainer.assertIsAlive(); - // testCase02(mainProcPid); - - mainContainer.assertIsAlive(); - testCase03(mainProcPid); + for (final boolean elevated : USER.isPresent() ? new Boolean[] { false, true } : new Boolean[] { false }) { + // Start the loop process in the "main" container, then run test cases + // using a sidecar container. + MainContainer mainContainer = new MainContainer(); + mainContainer.start(elevated); + mainContainer.waitForMainMethodStart(TIME_TO_WAIT_FOR_MAIN_METHOD_START); + + for (AttachStrategy attachStrategy : EnumSet.allOf(AttachStrategy.class)) { + long mainProcPid = testCase01(attachStrategy, elevated); + + // Excluding the test case below until JDK-8228850 is fixed + // JDK-8228850: jhsdb jinfo fails with ClassCastException: + // s.j.h.oops.TypeArray cannot be cast to s.j.h.oops.Instance + // mainContainer.assertIsAlive(); + // testCase02(mainProcPid, attachStrategy, elevated); + + mainContainer.assertIsAlive(); + testCase03(mainProcPid, attachStrategy, elevated); + } - mainContainer.waitForAndCheck(TIME_TO_RUN_MAIN_PROCESS * 1000); + mainContainer.waitForAndCheck(TIME_TO_RUN_MAIN_PROCESS * 1000); + } } finally { DockerTestUtils.removeDockerImage(IMAGE_NAME); } @@ -94,21 +130,21 @@ public static void main(String[] args) throws Exception { // Run "jcmd -l" in a sidecar container, find a target process. - private static long testCase01() throws Exception { - OutputAnalyzer out = runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "-l") + private static long testCase01(AttachStrategy attachStrategy, boolean elevated) throws Exception { + OutputAnalyzer out = runSideCar(MAIN_CONTAINER_NAME, attachStrategy, elevated, "/jdk/bin/jcmd", "-l") .shouldHaveExitValue(0) .shouldContain("sun.tools.jcmd.JCmd"); long pid = findProcess(out, "EventGeneratorLoop"); if (pid == -1) { - throw new RuntimeException("Could not find specified process"); + throw new RuntimeException(attachStrategy + ": Could not find specified process"); } return pid; } // run jhsdb jinfo (jhsdb uses PTRACE) - private static void testCase02(long pid) throws Exception { - runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jhsdb", "jinfo", "--pid", "" + pid) + private static void testCase02(long pid, AttachStrategy attachStrategy, boolean elevated) throws Exception { + runSideCar(MAIN_CONTAINER_NAME, attachStrategy, elevated, "/jdk/bin/jhsdb", "jinfo", "--pid", "" + pid) .shouldHaveExitValue(0) .shouldContain("Java System Properties") .shouldContain("VM Flags"); @@ -116,11 +152,11 @@ private static void testCase02(long pid) throws Exception { // test jcmd with some commands (help, start JFR recording) // JCMD will use signal mechanism and Unix Socket - private static void testCase03(long pid) throws Exception { - runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "help") + private static void testCase03(long pid, AttachStrategy attachStrategy, boolean elevated) throws Exception { + runSideCar(MAIN_CONTAINER_NAME, attachStrategy, elevated, "/jdk/bin/jcmd", "" + pid, "help") .shouldHaveExitValue(0) .shouldContain("VM.version"); - runSideCar(MAIN_CONTAINER_NAME, "/jdk/bin/jcmd", "" + pid, "JFR.start") + runSideCar(MAIN_CONTAINER_NAME, attachStrategy, elevated, "/jdk/bin/jcmd", "" + pid, "JFR.start") .shouldHaveExitValue(0) .shouldContain("Started recording"); } @@ -128,21 +164,36 @@ private static void testCase03(long pid) throws Exception { // JCMD relies on the attach mechanism (com.sun.tools.attach), // which in turn relies on JVMSTAT mechanism, which puts its mapped - // buffers in /tmp directory (hsperfdata_). Thus, in sidecar - // we mount /tmp via --volumes-from from the main container. - private static OutputAnalyzer runSideCar(String mainContainerName, String whatToRun, - String... args) throws Exception { - List cmd = new ArrayList<>(); - String[] command = new String[] { + // buffers in /tmp directory (hsperfdata_). Thus, in the sidecar + // we have two options: + // 1. mount /tmp from the main container using --volumes-from. + // 2. access /tmp from the main container via /proc//root/tmp. + private static OutputAnalyzer runSideCar(String mainContainerName, AttachStrategy attachStrategy, boolean elevated, String whatToRun, String... args) throws Exception { + System.out.println("Attach strategy " + attachStrategy); + + List initialCommands = List.of( Container.ENGINE_COMMAND, "run", "--tty=true", "--rm", "--cap-add=SYS_PTRACE", "--sig-proxy=true", - "--pid=container:" + mainContainerName, - "--volumes-from", mainContainerName, - IMAGE_NAME, whatToRun + "--pid=container:" + mainContainerName + ); + + List attachStrategyCommands = switch (attachStrategy) { + case TMP_MOUNTED_INTO_SIDECAR -> List.of("--volumes-from", mainContainerName); + case ACCESS_TMP_VIA_PROC_ROOT -> List.of(); }; - cmd.addAll(Arrays.asList(command)); + List elevatedOpts = elevated && USER.isPresent() ? List.of(NET_BIND_SERVICE, USER.get()) : Collections.emptyList(); + + List imageAndCommand = List.of( + IMAGE_NAME, whatToRun + ); + + List cmd = new ArrayList<>(); + cmd.addAll(initialCommands); + cmd.addAll(elevatedOpts); + cmd.addAll(attachStrategyCommands); + cmd.addAll(imageAndCommand); cmd.addAll(Arrays.asList(args)); return DockerTestUtils.execute(cmd); } @@ -189,9 +240,15 @@ static class MainContainer { } }; - public Process start() throws Exception { + public Process start(final boolean elevated) throws Exception { // start "main" container (the observee) DockerRunOptions opts = commonDockerOpts("EventGeneratorLoop"); + + if (elevated && USER.isPresent()) { + opts.addDockerOpts(USER.get()); + opts.addDockerOpts(NET_BIND_SERVICE); + } + opts.addDockerOpts("--cap-add=SYS_PTRACE") .addDockerOpts("--name", MAIN_CONTAINER_NAME) .addDockerOpts("--volume", "/tmp") @@ -242,7 +299,7 @@ public void waitForAndCheck(long timeout) throws Exception { try { exitValue = p.exitValue(); } catch(IllegalThreadStateException ex) { - System.out.println("IllegalThreadStateException occured when calling exitValue()"); + System.out.println("IllegalThreadStateException occurred when calling exitValue()"); retryCount--; } } while (exitValue == -1 && retryCount > 0); @@ -254,4 +311,8 @@ public void waitForAndCheck(long timeout) throws Exception { } + private enum AttachStrategy { + TMP_MOUNTED_INTO_SIDECAR, + ACCESS_TMP_VIA_PROC_ROOT + } }