Subprocess call can deadlock on Windows when WaitForMultipleObjects receives 64+ events (=16+ concurrent programs)
Categories
(Toolkit :: Async Tooling, defect, P1)
Tracking
()
People
(Reporter: robwu, Assigned: robwu)
References
Details
(Whiteboard: [addons-jira])
Attachments
(2 external files)
Bug 1971449 reports a scenario with the following characteristics:
- An extension calls
sendNativeMessage, potentially many times. This launches a native application via the subprocess module internally.- Test extension and program:
extension-poc.zip(preview without downloading: echo_host.cs via crxviewer)
- Test extension and program:
- At some point a bug happens where the first message is readable by the program, but not the second chunk.
- Log file:
native-host.logat https://bugzilla.mozilla.org/show_bug.cgi?id=1971449#c9
- Log file:
- When the bug happens, native messaging hosts binaries accumulate in the process list. Could be as high as 2000 processes.
After auditing the relevant implementation in Firefox, I found a potential way for this to trigger this outcome.
- The NativeMessaging implementation always writes two chunks as part of any write. This passes through
writein subprocess_common.sys.mjs towriteinsubprocess_win.worker.js.- The first write is immediately written to the process because
bufferis initially empty (source:write,writeNext,writeBuffer). - The second write awaits the reported completion of the first write (source:
onReady).
- The first write is immediately written to the process because
- The completion is reported via
onReady(), viapoll.- The events to poll are the set of 1 signal, processes and their pipes, computed in
updatePollEvents. onReadyis called whenever a change to any pipe or process is observed through theWaitForMultipleObjects.- According to MSDN:
WaitForMultipleObjects, "The maximum number of object handles is MAXIMUM_WAIT_OBJECTS". MAXIMUM_WAIT_OBJECTSis typically 64 according to Is MAXIMUM_WAIT_OBJECTS really 64? - Stack Overflow
- According to MSDN:
- The events to poll are the set of 1 signal, processes and their pipes, computed in
- In practice, this means that the maximum number of concurrent processes supported by the Subprocess module on Windows is 15:
- 1 signal (always present, used to detect Firefox shutdown)
- 15 processes
- 45 pipes (stdin/stdout/stderr for each process - this could be lower if there are no pending reads or writes)
- is 61 events out of the limit of 64.
- If 16 or more processes are created concurrently, the number of events exceeds
MAXIMUM_WAIT_OBJECTSandWaitForMultipleObjectswill fail and thereforeio.poll()will deadlock, because it is no longer able to be notified of changes to pipes and processes.
I created a unit test that successfully creates 15 processes, but fails at 16.
Updated•1 year ago
|
| Assignee | ||
Comment 1•1 year ago
|
||
The MSDN documentation was suggesting something very complicated.
I found this article that may be a simpler path towards dealing with the limit: https://devblogs.microsoft.com/oldnewthing/?p=110169
| Assignee | ||
Comment 2•1 year ago
|
||
I considered splitting the list of handles in chunks and using WaitForMultipleObjects on separate lists of handles. When there is any activity, this works reasonably well (we can set the timeout to 0). But if all pipes and processes are pending, we need to choose which 64 of them to wait for. While waiting for these, the thread is unaware of progress of the others. A way to overcome that is to lower the timeout, from 5000ms to something shorter, at the expense of waking up the subprocess thread more often. That's not ideal either.
I learned about the concept of I/O Completion Ports (IOCP) (https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports), which seems to fit the problem space here. We need the ability to be notified of file read/write completion, process termination and a way to unblock the waiting worker thread so it can process messages from the parent thread (Subprocess.sys.mjs).
IOCP appears to provide the desired functionality:
- By design support an arbitrary number of file handles, with
ReadFileandWriteFilefunctions having first-class support that are already in use by the subprocess module. - Process termination can be detected: https://stackoverflow.com/questions/17724859/detecting-exit-failure-of-child-processes-using-iocp-c-windows
PostQueuedCompletionStatus(MSDN can be used to immediately unblock a waitingGetQueuedCompletionStatuscall.
I'll give a try at implementing this.
| Assignee | ||
Comment 3•1 year ago
|
||
For reference, the new library symbols from kernel32.dll that I will be importing in my patch.
CreateIoCompletionPort- https://learn.microsoft.com/en-us/windows/win32/fileio/createiocompletionportGetQueuedCompletionStatus- https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatusPostQueuedCompletionStatus- https://learn.microsoft.com/en-us/windows/win32/fileio/postqueuedcompletionstatusJOBOBJECT_ASSOCIATE_COMPLETION_PORT- https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_associate_completion_portJobObjectAssociateCompletionPortInformation- a constant from the already-declaredSetInformationJobObject- https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-setinformationjobobject
| Assignee | ||
Comment 4•1 year ago
|
||
The current WaitForMultipleObjects-based implementation causes a
deadlock if 16 or more processes are spawned, as explained in the bug.
This patch replaces the use of events and WaitForMultipleObjects with
I/O Completion Ports (IOCP) as the mechanism to monitor I/O progress
and process terminations. This is not subject to the
MAXIMUM_WAIT_OBJECTS (64) limit of WaitForMultipleObjects.
Updated•1 year ago
|
Comment 7•1 year ago
|
||
Reverted this because it was causing devtools failures.
- Revert link
- Push with failures
- Failure Log
- Failure line: SUMMARY: AddressSanitizer: breakpoint /builds/worker/workspace/obj-build/dist/include/mozilla/Assertions.h:55 in AnnotateMozCrashReason
Please also check these mochitests failures.
| Assignee | ||
Comment 8•1 year ago
|
||
This is an issue in the test harness, I'll fix it in https://bugzilla.mozilla.org/show_bug.cgi?id=1982170#c1 and then reland.
Note that it's worth mentioning that I believe that there is a pre-existing issue that causes the timer to be scheduled. It is the third bullet point of my list of unresolved todos that I collected while looking into this bug:
- read: libc.winLastError should be ctypes.winLastError https://searchfox.org/mozilla-central/rev/fcc0cfc7f5b1df957ae29f6b2ccfeb6df47b6be8/toolkit/modules/subprocess/subprocess_win.worker.js#193
- write: libc.winLastError should be ctypes.winLastError https://searchfox.org/mozilla-central/rev/fcc0cfc7f5b1df957ae29f6b2ccfeb6df47b6be8/toolkit/modules/subprocess/subprocess_win.worker.js#290
- loop may loop twice (and does so in my unit test): loop() from init, if run after updatePollEvents() schedules, then two loops are running repeatedly: https://searchfox.org/mozilla-central/rev/fcc0cfc7f5b1df957ae29f6b2ccfeb6df47b6be8/toolkit/modules/subprocess/subprocess_win.worker.js#686
I intended to fix these separately in a follow-up because they are pre-existing issues that have nothing to do with the bug reported here.
| Assignee | ||
Comment 9•1 year ago
|
||
Removing bug 1982170 as a dependency; Although the specific crash is indeed a test-only issue, the fact that a timer was still running was indeed an issue with the patch. I fixed it and don't see the previously failing tests don't fail any more in the try push.
Comment 10•1 year ago
|
||
Comment 11•1 year ago
|
||
| bugherder | ||
Updated•1 year ago
|
| Assignee | ||
Comment 12•1 year ago
|
||
The current WaitForMultipleObjects-based implementation causes a
deadlock if 16 or more processes are spawned, as explained in the bug.
This patch replaces the use of events and WaitForMultipleObjects with
I/O Completion Ports (IOCP) as the mechanism to monitor I/O progress
and process terminations. This is not subject to the
MAXIMUM_WAIT_OBJECTS (64) limit of WaitForMultipleObjects.
Original Revision: https://phabricator.services.mozilla.com/D258968
Updated•1 year ago
|
Comment 13•1 year ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined: Windows-only; When the Subprocess.sys.mjs module has 16 processes or more concurrent processes, the subprocess worker that manages them loses its ability to monitor the state of the processes and stdin/stdout/stderr pipes that it manages. The Subprocess module is used by DevTools (Browser Toolbox) and WebExtensions (Native Messaging), and we have received a report of extensions being broken for users (bug 1971449), with gigabytes of memory usage and over 1000 processes spawned.
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing: QA not needed; any coverage that we'd get is already fully covered by unit tests.
- Risk associated with taking this patch: medium
- Explanation of risk level: Low, barely Medium risk. Medium because this is a significant but necessary refactor of the mechanism to monitor state changes to processes and pipes in the Subprocess.sys.mjs module. The risk is managed by the patches including extensive test coverage (that demonstrated the deadlock before, and verifies the fix after), and the patches has been on the train to release (143) without reported regressions or breakage. Furthermore, the changes only affect Subprocess.sys.mjs consumers (DevTools Browser Toolbox, and WebExtensions Native Messaging), so it does not impact Firefox users who do not use these features.
- String changes made/needed: None
- Is Android affected?: no
Updated•1 year ago
|
Updated•1 year ago
|
Updated•1 year ago
|
Comment 14•1 year ago
|
||
| uplift | ||
Updated•1 year ago
|
Updated•1 year ago
|
Description
•