diff --git a/src/java.base/unix/classes/module-info.java.extra b/src/java.base/unix/classes/module-info.java.extra index 77a61d2fea655..409fd83fe0a90 100644 --- a/src/java.base/unix/classes/module-info.java.extra +++ b/src/java.base/unix/classes/module-info.java.extra @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,3 +24,4 @@ */ exports sun.nio.cs to java.desktop; +exports sun.nio.fs to jdk.attach; 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 af8870ecf64b3..1c8b1f8c49a0b 100644 --- a/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java +++ b/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java @@ -28,18 +28,34 @@ import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.spi.AttachProvider; -import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; - +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; import java.util.Optional; - +import java.util.Set; import java.util.regex.Pattern; -import static java.nio.charset.StandardCharsets.UTF_8; +import jdk.internal.misc.VM; +import sun.nio.fs.UnixUserPrincipals; + +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; +import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE; /* * Linux implementation of HotSpotVirtualMachine @@ -57,9 +73,13 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { private static final Path STATUS = Path.of("status"); private static final Path ROOT_TMP = Path.of("root/tmp"); - String socket_path; + private static final Set NOT_EXPECTED_PERMISSIONS = EnumSet.of(GROUP_READ, GROUP_WRITE, OTHERS_READ, OTHERS_WRITE); + + Path socket_path; + private SocketAddress socket_address; private OperationProperties props = new OperationProperties(VERSION_1); // updated in ctor + /** * Attaches to the target VM */ @@ -79,11 +99,11 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { // 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. - final File socket_file = findSocketFile(pid, ns_pid); - socket_path = socket_file.getPath(); - if (!socket_file.exists()) { + final Path socket_file = findSocketFile(pid, ns_pid); + socket_path = socket_file; + if (!Files.exists(socket_file)) { // Keep canonical version of File, to delete, in case target process ends and /proc link has gone: - File f = createAttachFile(pid, ns_pid).getCanonicalFile(); + Path f = createAttachFile(pid, ns_pid).toRealPath(); boolean timedout = false; @@ -104,20 +124,20 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { timedout = (time_spent += delay) > timeout; - if (time_spent > timeout/2 && !socket_file.exists()) { + if (time_spent > timeout/2 && !Files.exists(socket_file)) { // Send QUIT again to give target VM the last chance to react checkCatchesAndSendQuitTo(pid, !timedout); } - } while (!timedout && !socket_file.exists()); + } while (!timedout && !Files.exists(socket_file)); - if (!socket_file.exists()) { + if (!Files.exists(socket_file)) { throw new AttachNotSupportedException( String.format("Unable to open socket file %s: " + "target process %d doesn't respond within %dms " + "or HotSpot VM not loaded", socket_path, pid, time_spent)); } } finally { - f.delete(); + Files.delete(f); } } @@ -125,17 +145,16 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine { // bogus process checkPermissions(socket_path); + socket_address = UnixDomainSocketAddress.of(socket_path); + if (isAPIv2Enabled()) { props = getDefaultProps(); } else { // Check that we can connect to the process // - this ensures we throw the permission denied error now rather than // later when we attempt to enqueue a command. - int s = socket(); - try { - connect(s, socket_path); - } finally { - close(s); + try (SocketChannel s = SocketChannel.open(StandardProtocolFamily.UNIX)) { + s.connect(socket_address); } } } @@ -165,13 +184,12 @@ InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOEx } // create UNIX socket - int s = socket(); - + SocketChannel s = SocketChannel.open(StandardProtocolFamily.UNIX); // connect to target VM try { - connect(s, socket_path); + s.connect(socket_address); } catch (IOException x) { - close(s); + s.close(); throw x; } @@ -187,7 +205,7 @@ InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOEx // Create an input stream to read reply - SocketInputStreamImpl sis = new SocketInputStreamImpl(s); + InputStream sis = Channels.newInputStream(s); // Process the command completion status processCompletionStatus(ioe, cmd, sis); @@ -197,59 +215,40 @@ InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOEx } private static class SocketOutputStream implements AttachOutputStream { - private int fd; - public SocketOutputStream(int fd) { - this.fd = fd; + private final SocketChannel channel; + public SocketOutputStream(SocketChannel channel) { + this.channel = channel; } @Override public void write(byte[] buffer, int offset, int length) throws IOException { - VirtualMachineImpl.write(fd, buffer, offset, length); - } - } - - /* - * InputStream for the socket connection to get target VM - */ - private static class SocketInputStreamImpl extends SocketInputStream { - public SocketInputStreamImpl(long fd) { - super(fd); - } - - @Override - protected int read(long fd, byte[] bs, int off, int len) throws IOException { - return VirtualMachineImpl.read((int)fd, bs, off, len); - } - - @Override - protected void close(long fd) throws IOException { - VirtualMachineImpl.close((int)fd); + ByteBuffer bb = ByteBuffer.wrap(buffer, offset, length); + channel.write(bb); } } // Return the socket file for the given process. - private File findSocketFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException { - return new File(findTargetProcessTmpDirectory(pid, ns_pid), ".java_pid" + ns_pid); + private static Path findSocketFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + return findTargetProcessTmpDirectory(pid, ns_pid).resolve(".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(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + private static Path 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 _) { - f = new File(findTargetProcessTmpDirectory(pid, ns_pid), fn.toString()); - f.createNewFile(); + Files.createFile(path); + } catch (FileAlreadyExistsException _) { + path = findTargetProcessTmpDirectory(pid, ns_pid).resolve(fn); + Files.createFile(path); } - return f; + return path; } - private String findTargetProcessTmpDirectory(long pid, long ns_pid) throws AttachNotSupportedException, IOException { + private static Path findTargetProcessTmpDirectory(long pid, long ns_pid) throws AttachNotSupportedException, IOException { final var procPidRoot = PROC.resolve(Long.toString(pid)).resolve(ROOT_TMP); /* We need to handle at least 4 different cases: @@ -275,22 +274,19 @@ private String findTargetProcessTmpDirectory(long pid, long ns_pid) throws Attac * note that if pid == ns_pid we are in a shared pid ns with the target and may (potentially) share /tmp */ - return (Files.isWritable(procPidRoot) ? procPidRoot : TMPDIR).toString(); + return Files.isWritable(procPidRoot) ? procPidRoot : TMPDIR; } // Return the inner most namespaced PID if there is one, // otherwise return the original PID. - private long getNamespacePid(long pid) throws AttachNotSupportedException, IOException { + private static 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. - final var statusFile = PROC.resolve(Long.toString(pid)).resolve(STATUS).toString(); - File f = new File(statusFile); - if (!f.exists()) { + final var statusPath = PROC.resolve(Long.toString(pid)).resolve(STATUS); + if (!Files.exists(statusPath)) { return pid; // Likely a bad pid, but this is properly handled later. } - Path statusPath = Paths.get(statusFile); - try { for (String line : Files.readAllLines(statusPath)) { String[] parts = line.split(":"); @@ -379,21 +375,42 @@ private static boolean checkCatchesAndSendQuitTo(int pid, boolean throwIfNotRead return okToSendQuit; } - //-- native methods - - static native void sendQuitTo(int pid) throws IOException; - - static native void checkPermissions(String path) throws IOException; + private static void checkPermissions(Path path) throws IOException { + UserPrincipal processUser = UnixUserPrincipals.fromUid((int) VM.geteuid()); + GroupPrincipal processGroup = UnixUserPrincipals.fromGid((int) VM.getegid()); + + PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class); + UserPrincipal root = path.getFileSystem().getUserPrincipalLookupService().lookupPrincipalByName("root"); + boolean isRoot = root.equals(processUser); + + Set permissions = attributes.permissions(); + UserPrincipal fileOwner = attributes.owner(); + GroupPrincipal fileGroup = attributes.group(); + + if (!fileOwner.equals(processUser) && !isRoot) { + throwFileNotSecure(path, + "file should be owned by the current user (which is " + processUser + ") but is owned by " + fileOwner); + } else if (!fileGroup.equals(processGroup) && !isRoot) { + throwFileNotSecure(path, + "file's group should be the current group (which is " + fileGroup + ") but the group is " + processGroup); + } else if (!permissions.isEmpty()) { + Set intersection = EnumSet.copyOf(permissions); + intersection.retainAll(NOT_EXPECTED_PERMISSIONS); + if (!intersection.isEmpty()) { + throwFileNotSecure(path, "file should only be readable and writable by the owner but has " + + PosixFilePermissions.toString(permissions) + " access"); - static native int socket() throws IOException; - - static native void connect(int fd, String path) throws IOException; + } + } + } - static native void close(int fd) throws IOException; + private static void throwFileNotSecure(Path pathSpec, String message) throws IOException { + throw new IOException("well-known file " + pathSpec + " is not secure: " + message); + } - static native int read(int fd, byte buf[], int off, int bufLen) throws IOException; + //-- native methods - static native void write(int fd, byte buf[], int off, int bufLen) throws IOException; + static native void sendQuitTo(int pid) throws IOException; static { System.loadLibrary("attach"); diff --git a/src/jdk.attach/linux/native/libattach/VirtualMachineImpl.c b/src/jdk.attach/linux/native/libattach/VirtualMachineImpl.c index fc9af9018352b..3b3b400b17574 100644 --- a/src/jdk.attach/linux/native/libattach/VirtualMachineImpl.c +++ b/src/jdk.attach/linux/native/libattach/VirtualMachineImpl.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,87 +25,10 @@ #include "jni_util.h" -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include "sun_tools_attach_VirtualMachineImpl.h" -#define ROOT_UID 0 - -/* - * Declare library specific JNI_Onload entry if static build - */ -DEF_STATIC_JNI_OnLoad - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: socket - * Signature: ()I - */ -JNIEXPORT jint JNICALL Java_sun_tools_attach_VirtualMachineImpl_socket - (JNIEnv *env, jclass cls) -{ - int fd = socket(PF_UNIX, SOCK_STREAM, 0); - if (fd == -1) { - JNU_ThrowIOExceptionWithLastError(env, "socket"); - } - return (jint)fd; -} - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: connect - * Signature: (ILjava/lang/String;)I - */ -JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_connect - (JNIEnv *env, jclass cls, jint fd, jstring path) -{ - jboolean isCopy; - const char* p = GetStringPlatformChars(env, path, &isCopy); - if (p != NULL) { - struct sockaddr_un addr; - int err = 0; - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - /* strncpy is safe because addr.sun_path was zero-initialized before. */ - strncpy(addr.sun_path, p, sizeof(addr.sun_path) - 1); - - if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { - err = errno; - } - - if (isCopy) { - JNU_ReleaseStringPlatformChars(env, path, p); - } - - /* - * If the connect failed then we throw the appropriate exception - * here (can't throw it before releasing the string as can't call - * JNI with pending exception) - */ - if (err != 0) { - if (err == ENOENT) { - JNU_ThrowByName(env, "java/io/FileNotFoundException", NULL); - } else { - char* msg = strdup(strerror(err)); - JNU_ThrowIOException(env, msg); - if (msg != NULL) { - free(msg); - } - } - } - } -} - /* * Class: sun_tools_attach_VirtualMachineImpl * Method: sendQuitTo @@ -118,141 +41,3 @@ JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_sendQuitTo JNU_ThrowIOExceptionWithLastError(env, "kill"); } } - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: checkPermissions - * Signature: (Ljava/lang/String;)V - */ -JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_checkPermissions - (JNIEnv *env, jclass cls, jstring path) -{ - jboolean isCopy; - const char* p = GetStringPlatformChars(env, path, &isCopy); - if (p != NULL) { - struct stat sb; - uid_t uid, gid; - int res; - - memset(&sb, 0, sizeof(struct stat)); - - /* - * Check that the path is owned by the effective uid/gid of this - * process. Also check that group/other access is not allowed. - */ - uid = geteuid(); - gid = getegid(); - - res = stat(p, &sb); - if (res != 0) { - /* save errno */ - res = errno; - } - - if (res == 0) { - char msg[100]; - jboolean isError = JNI_FALSE; - if (sb.st_uid != uid && uid != ROOT_UID) { - snprintf(msg, sizeof(msg), - "file should be owned by the current user (which is %d) but is owned by %d", uid, sb.st_uid); - isError = JNI_TRUE; - } else if (sb.st_gid != gid && uid != ROOT_UID) { - snprintf(msg, sizeof(msg), - "file's group should be the current group (which is %d) but the group is %d", gid, sb.st_gid); - isError = JNI_TRUE; - } else if ((sb.st_mode & (S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) != 0) { - snprintf(msg, sizeof(msg), - "file should only be readable and writable by the owner but has 0%03o access", sb.st_mode & 0777); - isError = JNI_TRUE; - } - if (isError) { - char buf[256]; - snprintf(buf, sizeof(buf), "well-known file %s is not secure: %s", p, msg); - JNU_ThrowIOException(env, buf); - } - } else { - char* msg = strdup(strerror(res)); - JNU_ThrowIOException(env, msg); - if (msg != NULL) { - free(msg); - } - } - - if (isCopy) { - JNU_ReleaseStringPlatformChars(env, path, p); - } - } -} - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: close - * Signature: (I)V - */ -JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_close - (JNIEnv *env, jclass cls, jint fd) -{ - shutdown(fd, SHUT_RDWR); - close(fd); -} - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: read - * Signature: (I[BI)I - */ -JNIEXPORT jint JNICALL Java_sun_tools_attach_VirtualMachineImpl_read - (JNIEnv *env, jclass cls, jint fd, jbyteArray ba, jint off, jint baLen) -{ - unsigned char buf[128]; - size_t len = sizeof(buf); - ssize_t n; - - size_t remaining = (size_t)(baLen - off); - if (len > remaining) { - len = remaining; - } - - RESTARTABLE(read(fd, buf, len), n); - if (n == -1) { - JNU_ThrowIOExceptionWithLastError(env, "read"); - } else { - if (n == 0) { - n = -1; // EOF - } else { - (*env)->SetByteArrayRegion(env, ba, off, (jint)n, (jbyte *)(buf)); - } - } - return n; -} - -/* - * Class: sun_tools_attach_VirtualMachineImpl - * Method: write - * Signature: (I[B)V - */ -JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_write - (JNIEnv *env, jclass cls, jint fd, jbyteArray ba, jint off, jint bufLen) -{ - size_t remaining = bufLen; - do { - unsigned char buf[128]; - size_t len = sizeof(buf); - int n; - - if (len > remaining) { - len = remaining; - } - (*env)->GetByteArrayRegion(env, ba, off, len, (jbyte *)buf); - - RESTARTABLE(write(fd, buf, len), n); - if (n > 0) { - off += n; - remaining -= n; - } else { - JNU_ThrowIOExceptionWithLastError(env, "write"); - return; - } - - } while (remaining > 0); -}