Skip to content

8381567: ProcessHandle.descendants fails with ArrayIndexOutOfBoundException for process 0#30763

Open
Michael-Mc-Mahon wants to merge 9 commits intoopenjdk:masterfrom
Michael-Mc-Mahon:pidzero
Open

8381567: ProcessHandle.descendants fails with ArrayIndexOutOfBoundException for process 0#30763
Michael-Mc-Mahon wants to merge 9 commits intoopenjdk:masterfrom
Michael-Mc-Mahon:pidzero

Conversation

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member

@Michael-Mc-Mahon Michael-Mc-Mahon commented Apr 16, 2026

Hi,

This is a small fix for j.l.ProcessHandle on MacOS. Unlike other platforms, Mac returns a ProcessHandle for pid 0 whose descendants are all processes on the system. This specific scenario tickles an off by one error where the descendants method tries to access an element past the end of the array of pids. The fix is to break from the loop before accessing this element.

Thanks,

Michael



Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8381567: ProcessHandle.descendants fails with ArrayIndexOutOfBoundException for process 0 (Bug - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/30763/head:pull/30763
$ git checkout pull/30763

Update a local copy of the PR:
$ git checkout pull/30763
$ git pull https://git.openjdk.org/jdk.git pull/30763/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 30763

View PR using the GUI difftool:
$ git pr show -t 30763

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/30763.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link
Copy Markdown

bridgekeeper Bot commented Apr 16, 2026

👋 Welcome back michaelm! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link
Copy Markdown

openjdk Bot commented Apr 16, 2026

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk Bot added nio nio-dev@openjdk.org core-libs core-libs-dev@openjdk.org labels Apr 16, 2026
@openjdk
Copy link
Copy Markdown

openjdk Bot commented Apr 16, 2026

@Michael-Mc-Mahon The following labels will be automatically applied to this pull request:

  • core-libs
  • nio

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing lists. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk Bot added the rfr Pull request is ready for review label Apr 16, 2026
@mlbridge
Copy link
Copy Markdown

mlbridge Bot commented Apr 16, 2026

@AlanBateman
Copy link
Copy Markdown
Contributor

/label remove nio

@openjdk openjdk Bot removed the nio nio-dev@openjdk.org label Apr 16, 2026
@openjdk
Copy link
Copy Markdown

openjdk Bot commented Apr 16, 2026

@AlanBateman
The nio label was successfully removed.

}
}
ppid = pids[++count]; // pick up the next pid to scan for
if (++count >= size) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the check without incrementing and increment below.
It looks odd to bump the count even though it's not used.


import org.junit.jupiter.api.Test;

/*
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a @bug tag?

*/
@Test
public void test() {
int num = ProcessHandle.of(0).orElseThrow().descendants().toList().size();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have any assertion? E.g. assertNotEquals(0, num) / assertTrue(num > 0) (or at least a assertDoesNotThrow to clarify the intention)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can wrap the invocation in assertDoesNotThrow()

}
ppid = pids[++count]; // pick up the next pid to scan for
ppStart = starttimes[count]; // and its start time
} while (count < next);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried } while (count < next && count < (size -1)) to avoid the break out of the loop.

Copy link
Copy Markdown
Member Author

@Michael-Mc-Mahon Michael-Mc-Mahon Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried } while (count < next && count < (size -1)) to avoid the break out of the loop.

It has already dereferenced the invalid index at that point. It might be possible to restructure the loop by putting the assignment to ppid and ppStart at the top, but the initial value for ppid is not taken from the array, which is probably why that code is at the end of the loop.

Comment thread src/java.base/share/classes/java/lang/ProcessHandleImpl.java Outdated
Co-authored-by: Andrey Turbanov <turbanoff@gmail.com>
@RogerRiggs
Copy link
Copy Markdown
Contributor

The purpose of the do{...} while loop is to sort the descendants to the beginning of the arrays.
If the desired result for pid == 0 is to return all processes, then it should not be wasting time selecting and sorting.
Just return a Stream of all of the ProcessHandles (or skip the sorting).

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member Author

The purpose of the do{...} while loop is to sort the descendants to the beginning of the arrays. If the desired result for pid == 0 is to return all processes, then it should not be wasting time selecting and sorting. Just return a Stream of all of the ProcessHandles (or skip the sorting).

I wouldn't say that is desired behavior. It's just what is observed but is platform specific and undocumented. I'm not sure special casing pid == 0 in the shared code is a good idea. What if the other platforms have similar behavior, but for different pid values? I think also checking before dereferencing an out of range index value is still prudent regardless.

@RogerRiggs
Copy link
Copy Markdown
Contributor

I wouldn't say that is desired behavior. It's just what is observed but is platform specific and undocumented. I'm not sure special casing pid == 0 in the shared code is a good idea. What if the other platforms have similar behavior, but for different pid values? I think also checking before dereferencing an out of range index value is still prudent regardless.

getDescendents() does not expect to find the requested pid in the result stream/list.
The use of zero in the call to native is intended to fetch info for all processes.

Hmm, I don't think process 0 is real and am surprised to find a process zero with parent zero in the result list.
I don't have a Linux handy to check. ps on MacOS does not list it. ps -p 0.
Process ids are OS specific so there is no spec to throw IAE on ProcessHandle.of(0).
But I don't think pid zero should be exposed.

If a process zero is not real then the API should return an empty list for calls to ProcessHandle.of(0).getDescendents().toList().

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member Author

Michael-Mc-Mahon commented Apr 21, 2026

I wouldn't say that is desired behavior. It's just what is observed but is platform specific and undocumented. I'm not sure special casing pid == 0 in the shared code is a good idea. What if the other platforms have similar behavior, but for different pid values? I think also checking before dereferencing an out of range index value is still prudent regardless.

getDescendents() does not expect to find the requested pid in the result stream/list. The use of zero in the call to native is intended to fetch info for all processes.

Hmm, I don't think process 0 is real and am surprised to find a process zero with parent zero in the result list. I don't have a Linux handy to check. ps on MacOS does not list it. ps -p 0. Process ids are OS specific so there is no spec to throw IAE on ProcessHandle.of(0). But I don't think pid zero should be exposed.

If a process zero is not real then the API should return an empty list for calls to ProcessHandle.of(0).getDescendents().toList().

ProcessHandle.of(0) returns a real handle on Mac, but not on Linux or Windows. We could disallow its creation on Mac too, but existing code could be using it. The parent of 1 is 0 and the parent of 0 is itself.

jshell> ProcessHandle.of(0).get().info()
$1 ==> [user: Optional[root], startTime: Optional[2026-04-19T12:05:59.162Z]]

@RogerRiggs
Copy link
Copy Markdown
Contributor

ProcessHandle.of(0) returns a real handle on Mac, but not on Linux or Windows. We could disallow its creation on Mac too, but existing code could be using it. The parent of 1 is 0 and the parent of 0 is itself.

ok, pid 0 is the kernel on MacOS.
The anomaly is caused by the overload of 0 when calling native to return all processes.
The invariant should be that the stream returned from getDescendants() does not return a ProcessHandle for the pid itself. But I suppose that is a different bug. The current bug would not occur if the requested pid was ignored.

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member Author

Michael-Mc-Mahon commented Apr 23, 2026

ProcessHandle.of(0) returns a real handle on Mac, but not on Linux or Windows. We could disallow its creation on Mac too, but existing code could be using it. The parent of 1 is 0 and the parent of 0 is itself.

ok, pid 0 is the kernel on MacOS. The anomaly is caused by the overload of 0 when calling native to return all processes. The invariant should be that the stream returned from getDescendants() does not return a ProcessHandle for the pid itself. But I suppose that is a different bug. The current bug would not occur if the requested pid was ignored.

ProcessHandle.of(0) returns a real handle on Mac, but not on Linux or Windows. We could disallow its creation on Mac too, but existing code could be using it. The parent of 1 is 0 and the parent of 0 is itself.

ok, pid 0 is the kernel on MacOS. The anomaly is caused by the overload of 0 when calling native to return all processes. The invariant should be that the stream returned from getDescendants() does not return a ProcessHandle for the pid itself. But I suppose that is a different bug. The current bug would not occur if the requested pid was ignored.

Yes. While you can argue it is reasonable that ProcessHandle.of(0).get().descendants() should return all processes, it's interesting that ProcessHandle.of(0).get().children() also returns all processes, due to that overloaded meaning of 0 in the native code. But, it should only return the handle for pid 1.

We could change the native code to use -1 as the special value to return all processes, but in the end it would be new behavior and wouldn't return anything useful.

I'm okay with just disallowing the creation of ProcessHandle.of(0) on Mac.

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member Author

Any other comments on the fix?

@RogerRiggs
Copy link
Copy Markdown
Contributor

I looked at a refactoring to change the sentinel value to -1 and avoid 0.
Also added some MacOSX tests for pid 0.
See draft PR: #30923
Use it as you see fit or I can convert it to a non-draft PR.

@Michael-Mc-Mahon
Copy link
Copy Markdown
Member Author

I looked at a refactoring to change the sentinel value to -1 and avoid 0. Also added some MacOSX tests for pid 0. See draft PR: #30923 Use it as you see fit or I can convert it to a non-draft PR.

Okay, I'll take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-libs core-libs-dev@openjdk.org rfr Pull request is ready for review

Development

Successfully merging this pull request may close these issues.

5 participants