Skip to content

8364281: Reduce JNI usage in Linux attach provider #26712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/java.base/unix/classes/module-info.java.extra
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,3 +24,4 @@
*/

exports sun.nio.cs to java.desktop;
exports sun.nio.fs to jdk.attach;
167 changes: 92 additions & 75 deletions src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PosixFilePermission> 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
*/
Expand All @@ -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;

Expand All @@ -104,38 +124,37 @@ 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);
}
}

// Check that the file owner/permission to avoid attaching to
// 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);
}
}
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand All @@ -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<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:
Expand All @@ -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(":");
Expand Down Expand Up @@ -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<PosixFilePermission> 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<PosixFilePermission> 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");
Expand Down
Loading