Skip to content

Commit d20a6ca

Browse files
committed
backport 93e889b48cb6eb6872201a28ee19d8fd17c5d821
1 parent 960ec27 commit d20a6ca

File tree

1 file changed

+104
-99
lines changed

1 file changed

+104
-99
lines changed

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

Lines changed: 104 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
import java.nio.file.Files;
3535
import java.nio.file.Path;
3636
import java.nio.file.Paths;
37-
import java.util.Optional;
37+
38+
import java.util.regex.Pattern;
3839

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

@@ -50,26 +51,9 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
5051
private static final Path TMPDIR = Path.of("/tmp");
5152

5253
private static final Path PROC = Path.of("/proc");
53-
private static final Path NS_MNT = Path.of("ns/mnt");
54-
private static final Path NS_PID = Path.of("ns/pid");
55-
private static final Path SELF = PROC.resolve("self");
5654
private static final Path STATUS = Path.of("status");
5755
private static final Path ROOT_TMP = Path.of("root/tmp");
5856

59-
private static final Optional<Path> SELF_MNT_NS;
60-
61-
static {
62-
Path nsPath = null;
63-
64-
try {
65-
nsPath = Files.readSymbolicLink(SELF.resolve(NS_MNT));
66-
} catch (IOException e) {
67-
// do nothing
68-
} finally {
69-
SELF_MNT_NS = Optional.ofNullable(nsPath);
70-
}
71-
}
72-
7357
String socket_path;
7458

7559
/**
@@ -96,13 +80,16 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
9680
if (!socket_file.exists()) {
9781
// Keep canonical version of File, to delete, in case target process ends and /proc link has gone:
9882
File f = createAttachFile(pid, ns_pid).getCanonicalFile();
83+
84+
boolean timedout = false;
85+
9986
try {
100-
sendQuitTo(pid);
87+
checkCatchesAndSendQuitTo(pid, false);
10188

10289
// give the target VM time to start the attach mechanism
10390
final int delay_step = 100;
10491
final long timeout = attachTimeout();
105-
long time_spend = 0;
92+
long time_spent = 0;
10693
long delay = 0;
10794
do {
10895
// Increase timeout on each attempt to reduce polling
@@ -111,18 +98,19 @@ public class VirtualMachineImpl extends HotSpotVirtualMachine {
11198
Thread.sleep(delay);
11299
} catch (InterruptedException x) { }
113100

114-
time_spend += delay;
115-
if (time_spend > timeout/2 && !socket_file.exists()) {
101+
timedout = (time_spent += delay) > timeout;
102+
103+
if (time_spent > timeout/2 && !socket_file.exists()) {
116104
// Send QUIT again to give target VM the last chance to react
117-
sendQuitTo(pid);
105+
checkCatchesAndSendQuitTo(pid, !timedout);
118106
}
119-
} while (time_spend <= timeout && !socket_file.exists());
107+
} while (!timedout && !socket_file.exists());
108+
120109
if (!socket_file.exists()) {
121110
throw new AttachNotSupportedException(
122111
String.format("Unable to open socket file %s: " +
123112
"target process %d doesn't respond within %dms " +
124-
"or HotSpot VM not loaded", socket_path, pid,
125-
time_spend));
113+
"or HotSpot VM not loaded", socket_path, pid, time_spent));
126114
}
127115
} finally {
128116
f.delete();
@@ -256,79 +244,32 @@ private File createAttachFile(long pid, long ns_pid) throws AttachNotSupportedEx
256244
}
257245

258246
private String findTargetProcessTmpDirectory(long pid, long ns_pid) throws AttachNotSupportedException, IOException {
259-
// We need to handle at least 4 different cases:
260-
// 1. Caller and target processes share PID namespace and root filesystem (host to host or container to
261-
// container with both /tmp mounted between containers).
262-
// 2. Caller and target processes share PID namespace and root filesystem but the target process has elevated
263-
// privileges (host to host).
264-
// 3. Caller and target processes share PID namespace but NOT root filesystem (container to container).
265-
// 4. Caller and target processes share neither PID namespace nor root filesystem (host to container).
266-
267-
Optional<ProcessHandle> target = ProcessHandle.of(pid);
268-
Optional<ProcessHandle> ph = target;
269-
long nsPid = ns_pid;
270-
Optional<Path> prevPidNS = Optional.empty();
271-
272-
while (ph.isPresent()) {
273-
final var curPid = ph.get().pid();
274-
final var procPidPath = PROC.resolve(Long.toString(curPid));
275-
Optional<Path> targetMountNS = Optional.empty();
276-
277-
try {
278-
// attempt to read the target's mnt ns id
279-
targetMountNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_MNT)));
280-
} catch (IOException e) {
281-
// if we fail to read the target's mnt ns id then we either don't have access or it no longer exists!
282-
if (!Files.exists(procPidPath)) {
283-
throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid));
284-
}
285-
// the process still exists, but we don't have privileges to read its procfs
286-
}
287-
288-
final var sameMountNS = SELF_MNT_NS.isPresent() && SELF_MNT_NS.equals(targetMountNS);
289-
290-
if (sameMountNS) {
291-
return TMPDIR.toString(); // we share TMPDIR in common!
292-
} else {
293-
// we could not read the target's mnt ns
294-
final var procPidRootTmp = procPidPath.resolve(ROOT_TMP);
295-
if (Files.isReadable(procPidRootTmp)) {
296-
return procPidRootTmp.toString(); // not in the same mnt ns but tmp is accessible via /proc
297-
}
298-
}
299-
300-
// let's attempt to obtain the pid ns, best efforts to avoid crossing pid ns boundaries (as with a container)
301-
Optional<Path> curPidNS = Optional.empty();
302-
303-
try {
304-
// attempt to read the target's pid ns id
305-
curPidNS = Optional.ofNullable(Files.readSymbolicLink(procPidPath.resolve(NS_PID)));
306-
} catch (IOException e) {
307-
// if we fail to read the target's pid ns id then we either don't have access or it no longer exists!
308-
if (!Files.exists(procPidPath)) {
309-
throw new IOException(String.format("unable to attach, %s non-existent! process: %d terminated", procPidPath, pid));
310-
}
311-
// the process still exists, but we don't have privileges to read its procfs
312-
}
313-
314-
// recurse "up" the process hierarchy if appropriate. PID 1 cannot have a parent in the same namespace
315-
final var havePidNSes = prevPidNS.isPresent() && curPidNS.isPresent();
316-
final var ppid = ph.get().parent();
317-
318-
if (ppid.isPresent() && (havePidNSes && curPidNS.equals(prevPidNS)) || (!havePidNSes && nsPid > 1)) {
319-
ph = ppid;
320-
nsPid = getNamespacePid(ph.get().pid()); // get the ns pid of the parent
321-
prevPidNS = curPidNS;
322-
} else {
323-
ph = Optional.empty();
324-
}
325-
}
326-
327-
if (target.orElseThrow(AttachNotSupportedException::new).isAlive()) {
328-
return TMPDIR.toString(); // fallback...
329-
} else {
330-
throw new IOException(String.format("unable to attach, process: %d terminated", pid));
331-
}
247+
final var procPidRoot = PROC.resolve(Long.toString(pid)).resolve(ROOT_TMP);
248+
249+
/* We need to handle at least 4 different cases:
250+
* 1. Caller and target processes share PID namespace and root filesystem (host to host or container to
251+
* container with both /tmp mounted between containers).
252+
* 2. Caller and target processes share PID namespace and root filesystem but the target process has elevated
253+
* privileges (host to host).
254+
* 3. Caller and target processes share PID namespace but NOT root filesystem (container to container).
255+
* 4. Caller and target processes share neither PID namespace nor root filesystem (host to container)
256+
*
257+
* if target is elevated, we cant use /proc/<pid>/... so we have to fallback to /tmp, but that may not be shared
258+
* with the target/attachee process, we can try, except in the case where the ns_pid also exists in this pid ns
259+
* which is ambiguous, if we share /tmp with the intended target, the attach will succeed, if we do not,
260+
* then we will potentially attempt to attach to some arbitrary process with the same pid (in this pid ns)
261+
* as that of the intended target (in its * pid ns).
262+
*
263+
* so in that case we should prehaps throw - or risk sending SIGQUIT to some arbitrary process... which could kill it
264+
*
265+
* however we can also check the target pid's signal masks to see if it catches SIGQUIT and only do so if in
266+
* fact it does ... this reduces the risk of killing an innocent process in the current ns as opposed to
267+
* attaching to the actual target JVM ... c.f: checkCatchesAndSendQuitTo() below.
268+
*
269+
* note that if pid == ns_pid we are in a shared pid ns with the target and may (potentially) share /tmp
270+
*/
271+
272+
return (Files.isWritable(procPidRoot) ? procPidRoot : TMPDIR).toString();
332273
}
333274

334275
/*
@@ -377,6 +318,70 @@ private long getNamespacePid(long pid) throws AttachNotSupportedException, IOExc
377318
}
378319
}
379320

321+
private static final String FIELD = "field";
322+
private static final String MASK = "mask";
323+
324+
private static final Pattern SIGNAL_MASK_PATTERN = Pattern.compile("(?<" + FIELD + ">Sig\\p{Alpha}{3}):\\s+(?<" + MASK + ">\\p{XDigit}{16}).*");
325+
326+
private static final long SIGQUIT = 0b100; // mask bit for SIGQUIT
327+
328+
private static boolean checkCatchesAndSendQuitTo(int pid, boolean throwIfNotReady) throws AttachNotSupportedException, IOException {
329+
var quitIgn = false;
330+
var quitBlk = false;
331+
var quitCgt = false;
332+
333+
final var procPid = PROC.resolve(Integer.toString(pid));
334+
335+
var readBlk = false;
336+
var readIgn = false;
337+
var readCgt = false;
338+
339+
340+
if (!Files.exists(procPid)) throw new IOException("non existent JVM pid: " + pid);
341+
342+
for (var line : Files.readAllLines(procPid.resolve("status"))) {
343+
344+
if (!line.startsWith("Sig")) continue; // to speed things up ... avoids the matcher/RE invocation...
345+
346+
final var m = SIGNAL_MASK_PATTERN.matcher(line);
347+
348+
if (!m.matches()) continue;
349+
350+
var sigmask = m.group(MASK);
351+
final var slen = sigmask.length();
352+
353+
sigmask = sigmask.substring(slen / 2 , slen); // only really interested in the non r/t signals ...
354+
355+
final var sigquit = (Long.valueOf(sigmask, 16) & SIGQUIT) != 0L;
356+
357+
switch (m.group(FIELD)) {
358+
case "SigBlk": { quitBlk = sigquit; readBlk = true; break; }
359+
case "SigIgn": { quitIgn = sigquit; readIgn = true; break; }
360+
case "SigCgt": { quitCgt = sigquit; readCgt = true; break; }
361+
}
362+
363+
if (readBlk && readIgn && readCgt) break;
364+
}
365+
366+
final boolean okToSendQuit = (!quitIgn && quitCgt); // ignore blocked as it may be temporary ...
367+
368+
if (okToSendQuit) {
369+
sendQuitTo(pid);
370+
} else if (throwIfNotReady) {
371+
final var cmdline = Files.lines(procPid.resolve("cmdline")).findFirst();
372+
373+
var cmd = "null"; // default
374+
375+
if (cmdline.isPresent()) {
376+
cmd = cmdline.get();
377+
cmd = cmd.substring(0, cmd.length() - 1); // remove trailing \0
378+
}
379+
380+
throw new AttachNotSupportedException("pid: " + pid + " cmd: '" + cmd + "' state is not ready to participate in attach handshake!");
381+
}
382+
383+
return okToSendQuit;
384+
}
380385

381386
//-- native methods
382387

0 commit comments

Comments
 (0)