3434import java .nio .file .Files ;
3535import java .nio .file .Path ;
3636import java .nio .file .Paths ;
37- import java .util .Optional ;
37+
38+ import java .util .regex .Pattern ;
3839
3940import 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