Skip to content

Commit 52ba728

Browse files
slovdahllarry-cable
authored andcommitted
8327114: Attach in Linux may have wrong behaviour when pid == ns_pid (Kubernetes debug container)
Co-authored-by: Larry Cable <larry.cable@oracle.com> Reviewed-by: kevinw, sgehwolf
1 parent 988a531 commit 52ba728

File tree

2 files changed

+210
-75
lines changed

2 files changed

+210
-75
lines changed

src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java

+112-38
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@
2828
import com.sun.tools.attach.AttachNotSupportedException;
2929
import com.sun.tools.attach.spi.AttachProvider;
3030

31-
import java.io.InputStream;
32-
import java.io.IOException;
3331
import java.io.File;
32+
import java.io.IOException;
33+
import java.io.InputStream;
34+
import java.nio.file.Files;
3435
import java.nio.file.Path;
3536
import java.nio.file.Paths;
36-
import java.nio.file.Files;
37+
import java.util.Optional;
3738

3839
import static java.nio.charset.StandardCharsets.UTF_8;
3940

@@ -47,13 +48,35 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
4748
// location is the same for all processes, otherwise the tools
4849
// will not be able to find all Hotspot processes.
4950
// Any changes to this needs to be synchronized with HotSpot.
50-
private static final String tmpdir = "/tmp";
51+
private static final Path TMPDIR = Path.of("/tmp");
52+
53+
private static final Path PROC = Path.of("/proc");
54+
private static final Path NS_MNT = Path.of("ns/mnt");
55+
private static final Path NS_PID = Path.of("ns/pid");
56+
private static final Path SELF = PROC.resolve("self");
57+
private static final Path STATUS = Path.of("status");
58+
private static final Path ROOT_TMP = Path.of("root/tmp");
59+
60+
private static final Optional<Path> SELF_MNT_NS;
61+
62+
static {
63+
Path nsPath = null;
64+
65+
try {
66+
nsPath = Files.readSymbolicLink(SELF.resolve(NS_MNT));
67+
} catch (IOException _) {
68+
// do nothing
69+
} finally {
70+
SELF_MNT_NS = Optional.ofNullable(nsPath);
71+
}
72+
}
73+
5174
String socket_path;
75+
5276
/**
5377
* Attaches to the target VM
5478
*/
55-
VirtualMachineImpl(AttachProvider provider, String vmid)
56-
throws AttachNotSupportedException, IOException
79+
VirtualMachineImpl(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException
5780
{
5881
super(provider, vmid);
5982

@@ -64,12 +87,12 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
6487
}
6588

6689
// Try to resolve to the "inner most" pid namespace
67-
int ns_pid = getNamespacePid(pid);
90+
final long ns_pid = getNamespacePid(pid);
6891

6992
// Find the socket file. If not found then we attempt to start the
7093
// attach mechanism in the target VM by sending it a QUIT signal.
7194
// Then we attempt to find the socket file again.
72-
File socket_file = findSocketFile(pid, ns_pid);
95+
final File socket_file = findSocketFile(pid, ns_pid);
7396
socket_path = socket_file.getPath();
7497
if (!socket_file.exists()) {
7598
// Keep canonical version of File, to delete, in case target process ends and /proc link has gone:
@@ -211,49 +234,102 @@ protected void close(long fd) throws IOException {
211234
}
212235

213236
// Return the socket file for the given process.
214-
private File findSocketFile(int pid, int ns_pid) throws IOException {
215-
String root = findTargetProcessTmpDirectory(pid, ns_pid);
216-
return new File(root, ".java_pid" + ns_pid);
237+
private File findSocketFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException {
238+
return new File(findTargetProcessTmpDirectory(pid, ns_pid), ".java_pid" + ns_pid);
217239
}
218240

219241
// On Linux a simple handshake is used to start the attach mechanism
220242
// if not already started. The client creates a .attach_pid<pid> file in the
221243
// target VM's working directory (or temp directory), and the SIGQUIT handler
222244
// checks for the file.
223-
private File createAttachFile(int pid, int ns_pid) throws IOException {
224-
String fn = ".attach_pid" + ns_pid;
225-
String path = "/proc/" + pid + "/cwd/" + fn;
226-
File f = new File(path);
245+
private File createAttachFile(long pid, long ns_pid) throws AttachNotSupportedException, IOException {
246+
Path fn = Path.of(".attach_pid" + ns_pid);
247+
Path path = PROC.resolve(Path.of(Long.toString(pid), "cwd")).resolve(fn);
248+
File f = new File(path.toString());
227249
try {
228250
// Do not canonicalize the file path, or we will fail to attach to a VM in a container.
229251
f.createNewFile();
230-
} catch (IOException x) {
231-
String root = findTargetProcessTmpDirectory(pid, ns_pid);
232-
f = new File(root, fn);
252+
} catch (IOException _) {
253+
f = new File(findTargetProcessTmpDirectory(pid, ns_pid), fn.toString());
233254
f.createNewFile();
234255
}
235256
return f;
236257
}
237258

238-
private String findTargetProcessTmpDirectory(int pid, int ns_pid) throws IOException {
239-
String root;
240-
if (pid != ns_pid) {
241-
// A process may not exist in the same mount namespace as the caller, e.g.
242-
// if we are trying to attach to a JVM process inside a container.
243-
// Instead, attach relative to the target root filesystem as exposed by
244-
// procfs regardless of namespaces.
245-
String procRootDirectory = "/proc/" + pid + "/root";
246-
if (!Files.isReadable(Path.of(procRootDirectory))) {
247-
throw new IOException(
248-
String.format("Unable to access root directory %s " +
249-
"of target process %d", procRootDirectory, pid));
259+
private String findTargetProcessTmpDirectory(long pid, long ns_pid) throws AttachNotSupportedException, IOException {
260+
// We need to handle at least 4 different cases:
261+
// 1. Caller and target processes share PID namespace and root filesystem (host to host or container to
262+
// container with both /tmp mounted between containers).
263+
// 2. Caller and target processes share PID namespace and root filesystem but the target process has elevated
264+
// privileges (host to host).
265+
// 3. Caller and target processes share PID namespace but NOT root filesystem (container to container).
266+
// 4. Caller and target processes share neither PID namespace nor root filesystem (host to container).
267+
268+
Optional<ProcessHandle> target = ProcessHandle.of(pid);
269+
Optional<ProcessHandle> ph = target;
270+
long nsPid = ns_pid;
271+
Optional<Path> prevPidNS = Optional.empty();
272+
273+
while (ph.isPresent()) {
274+
final var curPid = ph.get().pid();
275+
final var procPidPath = PROC.resolve(Long.toString(curPid));
276+
Optional<Path> targetMountNS = Optional.empty();
277+
278+
try {
279+
// attempt to read the target's mnt ns id
280+
targetMountNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_MNT)));
281+
} catch (IOException _) {
282+
// if we fail to read the target's mnt ns id then we either don't have access or it no longer exists!
283+
if (!Files.exists(procPidPath)) {
284+
throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid));
285+
}
286+
// the process still exists, but we don't have privileges to read its procfs
250287
}
251288

252-
root = procRootDirectory + "/" + tmpdir;
289+
final var sameMountNS = SELF_MNT_NS.isPresent() && SELF_MNT_NS.equals(targetMountNS);
290+
291+
if (sameMountNS) {
292+
return TMPDIR.toString(); // we share TMPDIR in common!
293+
} else {
294+
// we could not read the target's mnt ns
295+
final var procPidRootTmp = procPidPath.resolve(ROOT_TMP);
296+
if (Files.isReadable(procPidRootTmp)) {
297+
return procPidRootTmp.toString(); // not in the same mnt ns but tmp is accessible via /proc
298+
}
299+
}
300+
301+
// let's attempt to obtain the pid ns, best efforts to avoid crossing pid ns boundaries (as with a container)
302+
Optional<Path> curPidNS = Optional.empty();
303+
304+
try {
305+
// attempt to read the target's pid ns id
306+
curPidNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_PID)));
307+
} catch (IOException _) {
308+
// if we fail to read the target's pid ns id then we either don't have access or it no longer exists!
309+
if (!Files.exists(procPidPath)) {
310+
throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid));
311+
}
312+
// the process still exists, but we don't have privileges to read its procfs
313+
}
314+
315+
// recurse "up" the process hierarchy if appropriate. PID 1 cannot have a parent in the same namespace
316+
final var havePidNSes = prevPidNS.isPresent() && curPidNS.isPresent();
317+
final var ppid = ph.get().parent();
318+
319+
if (ppid.isPresent() && (havePidNSes && curPidNS.equals(prevPidNS)) || (!havePidNSes && nsPid > 1)) {
320+
ph = ppid;
321+
nsPid = getNamespacePid(ph.get().pid()); // get the ns pid of the parent
322+
prevPidNS = curPidNS;
323+
} else {
324+
ph = Optional.empty();
325+
}
326+
}
327+
328+
if (target.orElseThrow(AttachNotSupportedException::new).isAlive()) {
329+
return TMPDIR.toString(); // fallback...
253330
} else {
254-
root = tmpdir;
331+
throw new IOException(String.format("unable to attach, process: %d terminated", pid));
255332
}
256-
return root;
257333
}
258334

259335
/*
@@ -270,13 +346,12 @@ private void writeString(int fd, String s) throws IOException {
270346
write(fd, b, 0, 1);
271347
}
272348

273-
274349
// Return the inner most namespaced PID if there is one,
275350
// otherwise return the original PID.
276-
private int getNamespacePid(int pid) throws AttachNotSupportedException, IOException {
351+
private long getNamespacePid(long pid) throws AttachNotSupportedException, IOException {
277352
// Assuming a real procfs sits beneath, reading this doesn't block
278353
// nor will it consume a lot of memory.
279-
String statusFile = "/proc/" + pid + "/status";
354+
final var statusFile = PROC.resolve(Long.toString(pid)).resolve(STATUS).toString();
280355
File f = new File(statusFile);
281356
if (!f.exists()) {
282357
return pid; // Likely a bad pid, but this is properly handled later.
@@ -292,8 +367,7 @@ private int getNamespacePid(int pid) throws AttachNotSupportedException, IOExcep
292367
// The last entry represents the PID the JVM "thinks" it is.
293368
// Even in non-namespaced pids these entries should be
294369
// valid. You could refer to it as the inner most pid.
295-
int ns_pid = Integer.parseInt(parts[parts.length - 1]);
296-
return ns_pid;
370+
return Long.parseLong(parts[parts.length - 1]);
297371
}
298372
}
299373
// Old kernels may not have NSpid field (i.e. 3.10).

0 commit comments

Comments
 (0)