win32unix create_process: duplicate std handles in create_process iff they're not inheritable #10807
Conversation
xavierleroy
left a comment
There was a problem hiding this comment.
In either case, the current create_process would needlessly duplicate an already inheritable handle
Agreed.
and leak the original handle in the child process.
I would not call this "leaking": if the handle is marked as inheritable, it is intended to "leak" (i.e. be inherited) by the child process...
At any rate, this looks quite good to me. A suggestion for refactoring below. @dra27's opinion is awaited.
otherlibs/win32unix/createprocess.c
Outdated
| if (! (handleInfo[0] & HANDLE_FLAG_INHERIT)) { | ||
| if (! DuplicateHandle(hp, fd0, hp, &(si.hStdInput), | ||
| 0, TRUE, DUPLICATE_SAME_ACCESS)) { | ||
| err = GetLastError(); goto ret1; | ||
| } | ||
| } else { | ||
| si.hStdInput = fd0; |
There was a problem hiding this comment.
Could we factor out this logic in a function? (Then we'd call the function 3 times for each of the 3 standard handles.) Something like:
- GetHandleInformation
- if inheritable, return original handle
- otherwise, duplicate to an inheritable handle
There was a problem hiding this comment.
Done in the third commit. The ensure_inheritable function must also return the information that we've duplicated the handle to close it after the child process is spawned.
Should I squash that last commit?
29c16a1 to
3d3e871
Compare
Good point, I've amended the commit message to just say that the child process has both the original handle and the duplicated handle. |
3d3e871 to
6a7778a
Compare
|
Is there any update on this? There is no rush, but I'm just worried that it might have been overlooked. |
|
It’s on my list, but I don’t think this one’s slated for either 4.14 or 5.00. |
6a7778a to
d428c55
Compare
|
I think this fixes a buggy situation I hadn't described, so I added a test. IMO it's worthwhile to have this in 4.14 if it's meant to last long. Consider the following pipeline: The problem with the child process having duplicated "leaked" stdin and stdout handles is that while it is running, it cannot signal to the writer that it should stop writing: the writer will not see a broken pipe as the child still has the leaked duplicated handle open. The same thing happens to the reader with the stdout of the child. Usually stdin/stdout are bound to the lifetime of the process, so this is not too much of a problem, but if the child itself spawns processes that should outlive it, the whole thing can become messy. |
nojb
left a comment
There was a problem hiding this comment.
LGTM. I didn't quite follow the test, a brief comment explaining what it is checking may be useful.
otherlibs/win32unix/createprocess.c
Outdated
| DWORD flags, err; | ||
| HANDLE hp; | ||
| HANDLE to_close[3] = | ||
| {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE}; |
There was a problem hiding this comment.
I wouldn't use an array here, it obfuscates matters. Let just call them to_close1, to_close2, to_close3 (and we can revert the above renaming fd1 -> fd0, etc).
There was a problem hiding this comment.
Ok for the array. There's also the possibility of returning booleans or something to mark which fd should be closed.
I liked my renumbering better, but so be it :)
otherlibs/win32unix/createprocess.c
Outdated
|
|
||
| static int win_has_console(void); | ||
|
|
||
| static int ensure_inheritable(HANDLE h, HANDLE * hStd, HANDLE * to_close) |
There was a problem hiding this comment.
Can you add a brief comment above the function definition explaining what the function does and what each argument means?
There was a problem hiding this comment.
Done, hope it's clearer.
d428c55 to
32695ce
Compare
nojb
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the nice docs.
Just to confirm: the test runs roughly instantaneously in practice, right?
|
|
||
| (* Check that the read time was determined solely by the lifetime of | ||
| the child process and _not_ the subchild. *) | ||
| assert (t' -. t <= float_of_int sub_child_lifetime) |
There was a problem hiding this comment.
Is it right that in practice t' - t will be much smaller than sub_child_lifetime (more like instantaneous) ?
Yes, |
I don't understand: bigger ? did you mean smaller ?
Yes, 20 seconds is too long. Or perhaps we can kill the children once that the read in the parent is complete? |
yes, sorry, I meant bigger without the patch…
Yes, that's sensible. Thanks for the comments, I'll implement this. |
I understand that we are blocked because of #11021... what happens if you do |
72f5b20 to
867f8b9
Compare
I don't think it does. I've reduced the waiting time, maybe that's enough? |
The current code makes the assumption that handles are not inheritable (close-on-exec), and duplicates handles to mark them inheritable (keep-on-exec) for the child process. That assumption is only mentioned in ocaml#650, and not documented in the manual. It's possible however that the calling code gives to create_process handles that are already inheritable: either because the programmer isn't aware of the security implications, or because the programmer expects create_process to be a "simple" wrapper around WinAPI's CreateProcess; in which case setting the handle inheritable would be the responsibility of the calling code. Converting the canonical example from Microsoft documentation [1] without looking at win32unix's create_process would lead to this problem. In either case, the current create_process would needlessly duplicate an already inheritable handle and the child process would inherit both the original and the duplicated handles. Consider the following pipeline: `writer | child | reader`. The problem with the child process having duplicated "leaked" stdin and stdout handles is that while it is running, it cannot signal to the writer that it should stop writing: the writer will not see a broken pipe as the child still has the leaked duplicated handle open. The same thing happens to the reader with the stdout of the child. Usually stdin/stdout are bound to the lifetime of the process, so this is not too much of a problem, but if the child itself spawns processes that should outlive it, the whole thing can become messy. We can instead check whether the given handles are inheritable, and only duplicate them if they're not. The calling code is still responsible for closing the original handle. [1]: https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output
867f8b9 to
961c184
Compare
|
I don't see how to improve the test apart from reducing the timeout. Perhaps it's best to remove the test instead. Is there a way we can move forward with this patch? |
Hi, apologies for having dropped the ball on this one. This is good to merge from my side, and a 1s sleep timeout seems not too bad. @xavierleroy any objections to having this test in the CI? |
Timing-sensitive tests have been known to fail randomly because of hiccups in scheduling in virtual machines... Not sure how sensitive this test is to this kind of hiccups. In testsuite/tests/lib-unix/common/cloexec.ml I tried to check directly in the executed program whether some file descriptors are still open. It seems disabled on Windows :-( for reasons I need to remember. But this may be a different way to check for FD leaks. |
|
As a side note, Windows 10 introduced
From
Maybe the runtime just re-uses handle numbers that we want to check whether they were closed? I've written the same patch for Lwt and made other tests, but they rely more-or-less on the same property: without the patch the test will block (optionally fails with a timeout), and with it'll succeed as expected (optionally within the bounds of the timeout). (1) from a short talk with a Windows guru, that seems to be difficult and a likely overkill for this case. |
|
OK, let's merge this and if the test becomes a problem we can always disable it. |
|
I think I see issue not related directly to this PR (it was in the original code). The issue is a race-condition with handle better explained here https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873 I see no use of |
The current code makes the assumption that handles are not inheritable
(close-on-exec), and duplicates handles to mark them inheritable
(keep-on-exec) for the child process. That assumption is only
mentioned in #650, and not documented in the manual.
It's possible however that the calling code gives to
create_processhandles that are already inheritable: either because the programmer
isn't aware of the security implications, or because the programmer
expects create_process to be a "simple" wrapper around WinAPI's
CreateProcess; in which case setting the handle inheritable would bethe responsibility of the calling code.
Converting the canonical example from Microsoft documentation [1]
without looking at win32unix's
create_processwould lead to thisproblem.
In either case, the current
create_processwould needlessly duplicatean already inheritable handle and leak the original handle in the
child process.
We can instead check whether the given handles are inheritable, and
only duplicate them if they're not. The calling code is still
responsible for closing the original handle.