Repository: electron/electron (language: cpp)

Issue:
child_process spawn locks up renderer for 300 - 3000ms on signed app on macOS Big Sur
<!--  As an open source project with a dedicated but small maintainer team, it can sometimes take a long time for issues to be addressed so please be patient and we will get back to you as soon as we can.
-->

### Preflight Checklist
<!-- Please ensure you've completed the following steps by replacing [ ] with [x]-->

* [x] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project.
* [x] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to.
* [x] I have searched the issue tracker for an issue that matches the one I want to file, without success.

### Issue Details

* **Electron Version:**
  * 10.1.3
  * 10.1.4
  * 11.0.0-beta.13
  * 11.0.0-beta.17
  * 11.0.0-beta.19
* **Operating System:**
  * macOS 11.0 Beta (20A5395g)
  * macOS 11.0.1 Release (20B28)
* **Last Known Working Electron version:**
  * works on macOS 10.15 Catalina

### Expected Behavior

If I call `require('child_process').spawn('ls', ['.'])`, it should return quickly (<10ms).

Measurable with:
```javascript
const { spawn } = require('child_process');
const start = Date.now();
spawn('ls', ['.']);
console.log(`spawn sync time: ${Date.now() - start}`);
```

### Actual Behavior
<!-- A clear and concise description of what actually happened. -->

If run in a renderer process with `nodeIntegration: true`, it takes 300+ ms. When used with `ffmpeg` (our real-world case), we're seeing it go as long as 2-3 seconds, which leads me to think it's related to the binary size/code signing?

### To Reproduce
<!--
Your best chance of getting this bug looked at quickly is to provide an example.
-->

<!--
For bugs that can be encapsulated in a small experiment, you can use Electron Fiddle (https://github.com/electron/fiddle) to publish your example to a GitHub Gist and link it your bug report.
-->

<!--
If Fiddle is insufficient to produce an example, please provide an example REPOSITORY that can be cloned and run. You can fork electron-quick-start (https://github.com/electron/electron-quick-start) and include a link to the branch with your changes.
-->

To reproduce the bug, you'll need:
* macOS 11 Beta
* an Apple Developer code-signing certificate

Or you can download the version I code-signed: https://github.com/descriptinc/electron-spawn-perf-bug/releases/tag/1.0.0

Repo: https://github.com/descriptinc/electron-spawn-perf-bug
If you want to build/run it locally:
```sh
$ git clone https://github.com/descriptinc/electron-spawn-perf-bug.git
$ yarn install
$ yarn build
$ dist/mac/electron-spawn-bug.app/Contents/MacOS/electron-spawn-bug
```

### Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
<img width="912" alt="image" src="https://user-images.githubusercontent.com/21952/97063176-feb97280-1552-11eb-8ed3-023b427b4351.png">

### Additional Information
<!-- Add any other context about the problem here. -->
I'm not sure if this is an Electron, Electron Builder, or Big Sur bug.



Discussion / hints:
I can reproduce these results. Here are my ranges of spawn durations (in ms) on Catalina & Big Sur machines:

## macOS Catalina 10.15.7 (19H2)
| Main: `spawn ls` | Main: `ls exit` | Renderer: `spawn ls` | Renderer: `ls exit` |
| --: | --: | --: | --: |
| 1-3 | 111-126 | 1-3 | 19-31 |

## macOS Big Sur 11.0 Beta (20A5395g)
| Main: `spawn ls` | Main: `ls exit` | Renderer: `spawn ls` | Renderer: `ls exit` |
| --: | --: | --: | --: |
| 3-10 | 224-275 | 282-285 | 308-2825 |

Both of these samples were taken on a MacBook Pro (15-inch, 2016), 2.9 GHz Quad-Core Intel Core i7, 16 GB RAM
They were using Node.js 12.18.3, Chromium 87.0.4280.11, and Electron 11.0.0-beta.13.
Just tested: this also happens on Electron v11.0.0-beta.17
I'm not sure if this is helpful, but I ran contentTracing and got some profile snapshots with the signed (slow) and signed (fast) versions.
- [electron-trace-not-signed.zip](https://github.com/electron/electron/files/5448526/electron-trace-not-signed.zip)
- [electron-trace-signed.zip](https://github.com/electron/electron/files/5448527/electron-trace-signed.zip)

You can open these up in Chrome by going to `chrome://tracing` and loading them in (after unzipping).

But I don't know how to read it.

This seems likely to be a Big Sur bug. Electron doesn't do anything special related to code signing.

It's surprising that the behavior is different in the renderer process, though...
@nornagon: yea that's the part that also stood out to me—and the process we're running `ls` isn't changing status (presumably codesigned by Apple?).

Is there anything special about `child_process` in a renderer process? Is it going through IPC?
This is still reproducible on macOS 11.0.1 Beta (20B5022a) and Electron 11 beta 19

I filed a bug report with Apple (http://openradar.appspot.com/radar?id=5027087777792000), will post here if there are any updates.

@MarshallOfSound is there any chance this is related to your comment here https://github.com/electron/electron/issues/14011#issuecomment-461655158? These issues are similar in that something in the renderer process is taking much longer than the same action in the main process.
It looks like a few VS Code users are now hitting this as they upgrade to Big Sur: https://github.com/microsoft/vscode/issues/105446
GitKraken users are also affected.
There's been speculation in the VSCode issue (https://github.com/microsoft/vscode/issues/105446#issuecomment-727642247) that maybe this is due to Apple's dev certificate checks, however that doesn't seem likely since: 
1. This problem still happens while offline
2. The main process avoids this issue
Per this comment https://github.com/microsoft/vscode/issues/105446#issuecomment-729390368 - I just confirmed that the [test project](https://github.com/descriptinc/electron-spawn-perf-bug) has the slowdown on the browser (main) process when using the latest nightly electron build.

```
0: spawning "ls" process
0: spawn launched in 369 ms
1: spawning "ls" process
1: spawn launched in 390 ms
2: spawning "ls" process
2: spawn launched in 415 ms
3: spawning "ls" process
3: spawn launched in 389 ms
4: spawning "ls" process
4: spawn launched in 421 ms
5: spawning "ls" process
5: spawn launched in 402 ms
6: spawning "ls" process
6: spawn launched in 397 ms
7: spawning "ls" process
7: spawn launched in 420 ms
8: spawning "ls" process
8: spawn launched in 419 ms
9: spawning "ls" process
9: spawn launched in 420 ms
```

That's without even signing the app - just running `yarn start`.
A suggestion from @davidzech to fix this issue https://github.com/microsoft/vscode/issues/105446#issuecomment-729274600

> It would be nice if libuv used posix_spawn instead.
I think that is the correct solution moving forward, looked a bit at what chromium uses for spawning child processes based on a question from @MarshallOfSound and chromium does use `posix_spawn` https://source.chromium.org/chromium/chromium/src/+/master:base/process/launch_mac.cc
@deepak1556 is the idea that https://github.com/libuv/libuv/issues/2133 needs to be fixed to resolve this? Or is this something that would be patched in Electron?
The above libuv issue tracks updating fork calls on linux too, which is outside our scope for current issue. I am working on creating a minimal repro and opening an issue with libuv to get a fix just for macOS, I would be happy if we are able to have fix atleast for big sur versions and above. Will update here once I open up an issue in libuv.
Using yarn start on an Apple Silicon mini. 
 file  dist/mac/electron-spawn-bug.app/Contents/MacOS/electron-spawn-bug 
dist/mac/electron-spawn-bug.app/Contents/MacOS/electron-spawn-bug: Mach-O 64-bit executable x86_64

![exited in 233](https://user-images.githubusercontent.com/4635997/99725863-baeb5780-2a83-11eb-9587-06d83b0b7c3c.png)


@jhhl Those results are expected. You need to sign the binary in order to see the slowdown.
Upstream issue : https://github.com/libuv/libuv/issues/3050
My colleague has been working on a patch for libuv. It currently has a breaking change, but depending on how you use child processes it may not affect you (see PR below for more details).

We've done a custom Electron build with this libuv patch, which has solved the Big Sur issue for us. If anyone else is experiencing the issue, you can try patch https://github.com/electron/electron/pull/27026

P.S. Many thanks for https://github.com/electron/build-tools, that made the build process simple!
Just a heads up, the upstream issue was reopened because the PR was reverted: https://github.com/libuv/libuv/issues/3050#issuecomment-778495429
FWIW the issue also exists with electron 9.4.4, but 8.5.5 does not have the issue. It's blocking me from upgrading my app from electron 8.

dependabot keeps nagging about upgrading my electron <9 apps due to [this vulnerability](https://github.com/advisories/GHSA-hvf8-h2qh-37m9). It seems like electron 8 did not get a patch for that vulnerability.
Any news on this ?

This is severe.
However libuv is not motivated enough to fix this issue.

Vscode has kept this patch in their code.

It's Electron being the most affected.

How about revert the revert?  #27797
As a workaround, I downgraded Electron to version 11.4.6, and the slowness is gone. When running version 12, CPU usage would often go to 100% for a second or so when `spawn` was called.

The weird thing was that for me even when calling `spawn` from the main process the UI would freeze.  I also tried tried calling from a web worker, same problem.
This is also present in Electron v13. Is there a workaround other than disabling hardened runtime and downgrading?
Oddly, with Electron 10, we saw this only with signed builds (and only on Big Sur). With Electron 13, we see this even for development builds.

Update: Electron 11 seems to behave like Electron 10 in this regard.
any updates on this issue?
It seems as everything was fine when I used version `11.4.7` (on BigSur, both signed and unsigned).
But on versions 12 and 13 every spawnSync / execSync locks the renderer..
> any updates on this issue?
> It seems as everything was fine when I used version `11.4.7` (on BigSur, both signed and unsigned).
> But on versions 12 and 13 every spawnSync / execSync locks the renderer..

Can confirm the same behaviour. Downgraded to 11 and everything is fine but seems like a werid fix xD
@cupcakearmy I hope that it will be fixed before Electron v11 will reach `end-of-support`  🥲
I found the `posix_spawn` use quite promising despite the failure in the test suite of `libuv`. I would like to dig more into this to see what's happening there. Since Electron brings its own(?) node implementation I was expecting a specific node/libuv version somewhere in the Electron project. Could someone share some brief insights on this?
@seb-mtl 

Node version: https://github.com/electron/electron/blob/main/DEPS#L19-L20
Patches we apply on top of Node: https://github.com/electron/electron/tree/main/patches/node
Today I had some time to look into the issue. Because the issue touches Electron, node, libuv and the discussion is scattered over various bugreports and PRs I would like to use this post here to quickly recap the current situation. Please correct me if anything is wrong. 

## Chronology

1. The `spawn` performance bug got reported
2. The fix [#27026](https://github.com/electron/electron/pull/27026) got commited to libuv that uses [posix_spawn](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/posix_spawn.2.html).
3. The fix got reverted [here](https://github.com/electron/electron/commit/77525acf798e8fe4915be29df27baa9b6edcb3d8) due to a unit-test failure in **node**, see [here](https://github.com/libuv/libuv/pull/3107#issuecomment-781585353)
4. [VScode](https://github.com/microsoft/vscode) kept the fix in their repo
5. Two new issues occurred in VScode, check out for more info two posts further down [here](https://github.com/electron/electron/issues/26143#issuecomment-879569238).

## Recap

Last working version for me: **Electron v8.5.5**
First broken version for me: **Electron v9.0.0-beta.1**

According to [this](https://github.com/electron/releases) release table here are some version details:

|Tag|Published|npm|Prerelease|Module Version|Node|Chrome|Downloads|
|----|----|----|----|----|----|----|----|
|[v9.0.0-beta.1](https://github.com/electron/electron/releases/tag/v9.0.0-beta.1)|2020-02-10||yes|76|12.14.1|82.0.4048.0|2082|
|[v8.5.5](https://github.com/electron/electron/releases/tag/v8.5.5)|2020-11-18|8-x-y|no|76|12.13.0|80.0.3987.163|272251|

The error seems to be in libuv. Since libuv is located in [deps/uv/](https://github.com/nodejs/node/tree/v12.13.1/deps/uv) of node we can diff the two node versions **v12.13.0** and **v12.14.1**: https://github.com/nodejs/node/compare/v12.13.0...v12.14.1

Recap done, let's go on. There are 2 possible fixes:

1. Find the change in libuv that causes the problem
2. Fix the issue that causes the libuv unit-tests to fail.

Personally I would like to find the issue first in libuv. There seems to be only one change that looks suspicious at `deps/uv/src/unix/kqueue.c` (search for file in diff mentioned above) but can't point my finger on it. Ideas?
Thanks for looking into this, to clarify some points

> The fix got reverted here due to a unit-test failure in the libuv suite.

Nope it was reverted because of unittest failures in node test suite, you can refer to https://github.com/libuv/libuv/pull/3107#issuecomment-781585353 for discussions along what specific tests failed. I got most of them passing, the only one I couldn't fix is the `test.parallel/test-child-process-spawnsync-validation-errors` where it tries to test uid option. If this can be fixed then `posix_spawn` change can be given another attempt in libuv.

> VScode kept the fix in their repo

vscode does not rely on uid, gid options of child_process module, so I decided to try the patch. But over user testing, we found two new issues in the implementation,

1) https://github.com/microsoft/vscode-remote-release/issues/4630 - the manual path resolution implementation used for `posix_spawn` path https://github.com/descriptinc/electron/blob/2f9ed1de9f8e533c0458c83fd2bec7b4cfb01cf0/patches/node/darwin_libuv_use_posix_spawn.patch#L413-L439 differed from `execvp` in that it started respecting `ENOTDIR` errors.

2) https://github.com/microsoft/vscode/issues/118267 - cwd option of child_process module didn't work well in some situations, turned out to be a bug in `posix_spawnp` implementation https://github.com/microsoft/vscode/issues/118267#issuecomment-840122005

Attaching the final version of the patch which we use in vscode 
[use_posix_spawn_to_spawn_subprocesses_on_macos.patch.zip](https://github.com/electron/electron/files/6813387/use_posix_spawn_to_spawn_subprocesses_on_macos.patch.zip)


> Fix the issue that causes the libuv unit-tests to fail.

Yup the ideal next step is to get the node test suite passing with the `posix_spawn` change. Also it would be best to test with vanilla node but if you are running it for node built alongside electron then use this script https://github.com/electron/electron/blob/main/script/node-spec-runner.js to ignore tests that are known to be broken.

I am unassigning myself from this bug due to lack of cycles, but happy to assist were possible.
Great insights, and thanks for the clarification. I updated/corrected my post accordingly
@deepak1556 Where can we keep track of the patch used in VScode (and any guide to apply it). Thinking of trying it out for [hyper](https://github.com/vercel/hyper).
Thanks
There is no public endpoint to track the patch, if you are building electron locally then the patch should cleanly apply on `src/electron`, no special steps required. Also the patch should be exercised with caution when using with apps, it was posted as a starting point for anyone interested in upstreaming.
@deepak1556 since vscode is already applying the patch, can we download the patched and prebuilt electron binaries from somewhere?

Edit: Forked electron and committed the patch: https://github.com/KishanBagaria/electron

~~Also built electron-v13.1.8-darwin-x64.zip with CircleCI (took 2h 42m) and released it here: https://github.com/KishanBagaria/electron/releases/tag/v13.1.8~~

~~darwin-arm64 build was stopped midway by CircleCI because of exceeding 5 hours in the macOS Medium executor. Support mentioned there's no way to increase the 5 hours timeout. Access to the macOS Large executor (which should take < 5 hours) will be given only for a large annual contract. Access to the CircleCI local runner where you can use your own Mac will be given for the same large annual contract.~~

---

After battling with CircleCI config, I was able to [build Electron v13.2.1 with the patch](https://github.com/KishanBagaria/electron/releases/tag/v13.2.1)

---

@dennisameling has built v14.0.1 with the patch: https://github.com/dennisameling/electron/releases

---

I and @dennisameling built patched v15.3.0: https://github.com/KishanBagaria/electron/releases/tag/v15.3.0
I will email 10 funny cat GIFs to whoever fixes this bug. It's causing some issues for our users. For now, we are [working around](https://github.com/cypress-io/cypress/pull/17807) this slowdown by spawning a bare Node.js process and then using that process to run additional spawns, but this is pretty ugly and still adds a couple of seconds to execution time due to the 1 spawn call made from Electron.
Might want to wait on https://github.com/libuv/libuv/pull/3257 instead. Thanks!
Does anyone know of a solution that does not involve compiling and managing a fork of Electron, or downloading a pre-built application? 

What about the possibility of using the Electron shipped with VS Code? 
Two other workaround solutions:
- Running spawn instead from the main process (not renderer)
- Downgrading to electron 8
While performing security engagements on behalf of my customers I am discovering more and more apps that are behind the latest Electron stable release due to the fact that they have to maintain a separate fork of Electron (hence upgrading is a lot more work). While this issue is not strictly security related, this is clearly affecting their security posture. 
@mifi Last we tested, running from main process doesn't help on Electron 12+ (see https://github.com/electron/electron/issues/26143#issuecomment-729393664). 

It looks like Electron 8 is no longer supported/maintained: https://www.electronjs.org/releases/stable?version=8#end-of-support-for-8xy-855


FYI, we're seeing the same behaviour on the beta release of macOS Monterey 12.0.1. 

In 5 days Electron child_process will be slugish for the last two releases of macOS, without any fix in sight. 
Based on what I can gather from the current [libuv PR](https://github.com/libuv/libuv/pull/3257) and other history

* It's not looking possible to fix in libuv because of downstream breaking impacts
* Apple hasn't fixed this in 2 major OS updates and doesn't seem motivated to
* Everyone with sufficient scale to maintain their own electron fork is patching this manually and accepting the regressions as the lesser evil, but that's going to be very much the minority of electron users

So what about us smaller fry who are stuck on now unsupported Electron versions like v8, while Electron has charged on to v15 without us? I gather that even the workaround (running in main process) has stopped working in newer Electron versions.

What's a stable workaround which isn't going to stop working again and cause a major architectural rework in our apps?
In reply to deleted comment:
> Electron v11 doesn't have this issue – which should be an improvement over Electron v8.

Interesting you say that. I upgraded from v8 to v11 this week and the production build had the issue, though not the dev build. At that point I gave up for the time being.
It could be some entitlement or that we double `codesign` the Electron binary but we didn't have the issue on Electron 11 – dev or prod. Only started in Electron 12 for us.
e11 has the issue for signed+notarized builds, but not for plain builds. e12+ all have the issue even for unsigned builds.
Iirc, we didn't have the issue with signed+notarized E11 builds. We use these entitlements among others:

com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
com.apple.security.cs.disable-library-validation

Here's my original dupe issue: https://github.com/electron/electron/issues/28683
Just confirmed that these are already set for me. It's strange the inconsistent findings here, I wonder what the variable at play is?

Here's my full entitlements file, they're basically as-is from the starter I used a few years ago.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
  </dict>
</plist>
```
[I’m also having](https://github.com/mifi/lossless-cut) the issue with v11 with signed+notarized (spawn from renderer process, have not tried from main.)

Could a workaround be to have an always-running «proxy» child process that receives commands and spawns off other child processes on behalf of electron, has anyone tried that?
Yes this is most likely a valid workaround.

That or reduce the need for child_process (for example by batching command
to be run through child_process in a script).

.... But it's obviously not ideal to have to specialized part of your code
to work around issues on only some of the OS supported by your software.

Doing a little investigation of workarounds today and have some notes. Possibly no new information here as I've read most of this scattered around various issue chains, but now just compiled into one comment.

I'm currently on electron 8, and have tried electron 11.5 and 13.6 today. Since I use node-pty and they have their own problems right now, 14/15 are incompatible with my code so I have no results from that, but I don't gather they're any better than 13 for the subject of this issue.

The choice of electron versions may feel a little un-satisfying. These are just the two I've seen referred to most and where the problems supposedly differ.

### Possible ways & workarounds to spawn a process

There are a few suggestions I've seen floating around to work around the issue:

1. (baseline) Spawn within a renderer/BrowserWindow 
2. Spawn within the Main process and use Electron IPC to communicate with the renderer
3. Spawn within a process forked off from Main (via child_process.fork) and use child_process IPC to communicate to Main and then Electron IPC to the renderer (have seen it suggested as a possible workaround in later electron versions)

### Which workarounds... work-around?

I have written spawns which do `git status` for each of the above workarounds:

* Electron 8: both main and renderer spawns run around 10-15ms, in both development and production builds.
* Electron 11: both spawns perform around 10-15ms in dev build, renderer spawn goes to 300ms in production build.
* Electron 13: both spawns perform around 300ms in development
* Electron 13 with 'fork' from main: 300ms in development

So for E11 it seems that using the main process and IPC, that's a fine workaround. For E13+ the workaround breaks and doesn't have a clear replacement. 

As a developer I find it hard to justify investing in a workaround which adds complexity and I know is just going to break when I eventually need to get off E11, especially since [electron 11 is already out of support at this point](https://www.electronjs.org/releases/stable?version=11#end-of-support-for-11xy-1150)

Guess electron 8 is the place to stay until there's some movement with a real fix 😪
FWIW I've had a tiny bit of success using ideas from [child_process-remote](https://www.npmjs.com/package/child_process-remote) along with node's `cluster` module in order to work around this for local development. I was able to basically patch `child_process.spawn` to use the client instead of the native module (I think I ended up using domain sockets instead of ~~http~~ tcp as well). Dealing with connection issues and error handling was too much effort to put into production given our resources, though.
@Nick-Lucas I'm using spawn from a web worker started from the renderer process in electron 11 and that works ok in signed build (~2ms spawn).
@GitFiend did you wrap the worker with promises for easier integration?
(in my use case I even need the `execSync` / `spawnSync` functions, so promisifying is quite mandatory if the only option is to use a web worker..
@edenhermelin My app has a long-running worker thread for handling spawns and parsing results. It was this way before this bug came up. There's also sometimes a pause after starting the signed app, however, subsequent spawns perform properly. It may be that creating a worker each time you want to spawn doesn't solve the issue. Yes, everything uses promises. This is for electron 11 on Mac. Everything after that is unusable for me.
@GitFiend 
I see. DId you use any recommended library for promisifying the worker? 
Moreover, by "anything after that isn't usable to me", do you mean that this workaround doesn't work for the newer versions? (Like electron 15)?
Do you know if this is only related to macOS? Because I'm experimenting similar issues on both Linux and Windows. I have a forked process running which is roughly 300ms slower than when its not running from the forked Electron's main process. Any ideas on what might cause this issue?
Did anyone manage to workaround this issue?
We managed to fix it by writing and running a native swift client that will run command on behalf of our Electron (through XPC communication). Of course the whole things needs to be secure, so runtime check of signature and all that fun stuff. 

Such a hassle but what are you going to do ? People are finally switching to BigSur & Monterey, and if your application run a lot of child_process command, their experience is horrible (if usable at all).

Right now this issue is a big thorn in Electron's foot that will keep growing as people switch to BigSur & Monterey, and
the lack of commitment from big dev companies to help fix libuv is surprising. Unfortunately it seems they'd rather fork Electron and apply the original libuv fix, rather than help find a fix which would benefit the whole community.
I've managed to work around this bug by writing a native app shell for the Mac version (Not using Electron). The app uses WKWebView for renderer and bundled node js for the backend. This is quite a large app and this shell required <1000 lines of Swift. 

This approach is probably not for everyone as it required learning enough Swift to handle JSON, debugging message passing, async etc. It would also depend on your architecture. I had recently refactored and removed electron/remote from my app which made it easier. 
How come that this seems to be an Electron-only issue? NodeJS also uses libuv doesn't it?
perhaps it will also happen with node.js if signing the `node` binary
> perhaps it will also happen with node.js if signing the `node` binary

Oh yes, sorry, I totally forgot this is related to signing. This issue has been dragging on so long and got stuck on some MacOS/Apple system call bug (unrelated to signing). 
> perhaps it will also happen with node.js if signing the `node` binary

I've bundled the app, signed it and the included node binary and also notarized it (according to the Xcode UI). I'm not experiencing the issue on my machine, though I want to do more testing on other macs.
> I've bundled the app, signed it and the included node binary and also notarized it (according to the Xcode UI). I'm not experiencing the issue on my machine, though I want to do more testing on other macs.

and the signed node binary is the one calling `child_process.spawn`?
Yes. It didn't occur to me that this would be a problem as I'm pretty sure nodejs is signed and notarized on Mac. 
This is the biggest showstopper for me at the moment. I am trying to build Electron 14 with the **libuv** patch myself. Could anyone share some lights how I can apply the patch? Do I first build libuv with the patch and then build Electron with it? Or is anyone aware of a fork with macOS binaries for Electron >=v12?
@MMcKester https://github.com/KishanBagaria/electron/releases/
@KishanBagaria That's awesome, thx!
@KishanBagaria thanks so much for providing these builds. We have just tried your 13.2.1 build. This combined with node-pty 0.11.0-beta17 (0.11.0 has [a matching fix, porting from fork to posix_spawn](https://github.com/microsoft/node-pty/pull/487)) seems to work!

Is there a process for updating your builds? e.g. your 13.x.y is 13.2.1, whereas the latest 13.x.y is 13.6.6. 🙇 

Reasoning: there is a [security advisory on electron < 13.3.0](https://github.com/advisories/GHSA-mpjm-v997-c4h4), and Spectron does not support electron > 13. Sigh.
Did some more testing with electron 16.07. Some observations:

- the app doesn't need to be signed, also happens with `npx electron .`
- the slowdown happens when child_process.spawn is called
  - from the renderer
  - from main process
  - from a worker_thread
  - from a long-running spawned child_process, every time it calls child_process
- does **not** happen in node.js (at least v16.13.0), **has to be electron.**
- It doesn't just lock up the renderer, it also locks up the main process and **the whole macos** (whole macos UI get sluggish, mouse cursor starts lagging)
- more prominent effect when running multiple spawn in parallel (>=4)

I created a relatively simple reproducible example:

```js
// worker.js

const workerpool = require('workerpool');
const childProcess = require('child_process');

workerpool.worker({
  spawn: async () => new Promise((resolve, reject) => {
    const ps = childProcess.spawn('ls', { stdio: 'ignore' });
    ps.on('exit', resolve);
    ps.on('error', reject);
  }),
});
```

```js
// lockup-test.js

const workerpool = require('workerpool');

const pool = workerpool.pool(__dirname + '/worker.js', {
  workerType: 'process',
  minWorkers: 'max',
  maxWorkers: 5,
  onCreateWorker: () => console.log('onCreateWorker'),
});

const spawn = (...args) => pool.exec('spawn', args);

(async () => {
  try {
    for (;;) {
      await Promise.all(Array(4).fill(0).map(async () => {
        console.log('spawn');
        await spawn();
        await new Promise((resolve) => setTimeout(resolve, 30));
      }));
    }
  } catch (err) {
    console.error(err);
  }
})();
```

```
mkdir electron-lockup && cd electron-lockup
yarn add electron workerpool
```

- Now run `npx electron lockup-test.js` and observe a complete slowdown of the system and `spawn` being **printed very slowly**.
- If you instead run `node lockup-test.js`, there is no slowdown and `spawn` gets printed out rapidly.

Because the whole system slows down it strengthens the hypothesis that this is really a macos bug (not just electron).

Running big sur 11.6. **Update:** now also on Monterey 12.2.1. 

I wonder if there's an easy way to include Node.js inside electron for all platforms. If so, that could be a workaround, by using a long-running node.js child_process to spawn child_processes. Though maybe it would cause issues with `require` when the app is packaged, because asar modules are not available in Node.js.
For me the issue arises only after updating Electron to 12 or above and only when using Big Sur.

My test results are:

Electron 11@latest (11.5.0)
- macOS (Mojave) - spawn takes ~10ms
- macOS (Big Sur) - spawn takes ~10ms
- Windows 10 - spawn takes ~10ms

Electron 12@latest (12.2.3)
- macOS (Mojave) - spawn takes ~10ms
- macOS (Big Sur) - spawn takes **~500ms** // problem is here
- Windows 10 - spawn takes ~10ms

So in my tests Electron 11 (uses Node 12) is always ok. Electron 12 and above is slow but only on Big Sur.

Therefore it seems to be something since Electron 12 (uses Node 14). Most likely something changed in child_process logic between Node 12 -> 14?
Hey, @nornagon @zcbenz @MarshallOfSound.

Sorry for the intrusion of directly mentioning you. However, I am following this issue for more than a year now and I still don't know what you think and if you have plans to solve this.

I'm asking because we are blocked and can't update Electron. I see a lot of people here in the same situation. Also, correct me if I'm wrong, but Microsoft's own two products — VSCode and GitHub Desktop are dealing with the same problem.
they're working on fixing this here: https://github.com/libuv/libuv/pull/3257
This has now been fixed in libuv 🎉 hoping for a release soon!

https://github.com/libuv/libuv/pull/3257
We should consider backporting this. Node.js hasn't yet updated their version of libuv to include the fix, but it affects Electron significantly more heavily than Node, and I think the fix is probably worth backporting to supported Electron versions (>= 15.x, 14.x is currently security-only).

There are a few patches we'd need to backport in addition to https://github.com/libuv/libuv/pull/3257. Specifically, https://github.com/libuv/libuv/pull/3537 and https://github.com/libuv/libuv/pull/3441, and maybe https://github.com/libuv/libuv/pull/3512 and https://github.com/libuv/libuv/pull/3519 as well.

Fix patch:
diff --git a/patches/node/.patches b/patches/node/.patches
index 85ca6d4b58d7c..2c7f73453169f 100644
--- a/patches/node/.patches
+++ b/patches/node/.patches
@@ -35,3 +35,14 @@ fix_failing_node_js_test_on_outdated.patch
 be_compatible_with_cppgc.patch
 feat_add_knostartdebugsignalhandler_to_environment_to_prevent.patch
 worker_thread_add_asar_support.patch
+process_monitor_for_exit_with_kqueue_on_bsds_3441.patch
+unix_protect_fork_in_uv_spawn_from_signals.patch
+process_bsd_handle_kevent_note_exit_failure_3451.patch
+reland_macos_use_posix_spawn_instead_of_fork_3257.patch
+process_reset_the_signal_mask_if_the_fork_fails_3537.patch
+process_only_use_f_dupfd_cloexec_if_it_is_defined_3512.patch
+unix_simplify_uv_cloexec_fcntl_3492.patch
+unix_remove_uv_cloexec_ioctl_3515.patch
+process_simplify_uv_write_int_calls_3519.patch
+macos_don_t_use_thread-unsafe_strtok_3524.patch
+process_fix_hang_after_note_exit_3521.patch
diff --git a/patches/node/macos_don_t_use_thread-unsafe_strtok_3524.patch b/patches/node/macos_don_t_use_thread-unsafe_strtok_3524.patch
new file mode 100644
index 0000000000000..69e57568216d0
--- /dev/null
+++ b/patches/node/macos_don_t_use_thread-unsafe_strtok_3524.patch
@@ -0,0 +1,54 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Ben Noordhuis <info@bnoordhuis.nl>
+Date: Wed, 9 Mar 2022 11:06:39 +0100
+Subject: macos: don't use thread-unsafe strtok() (#3524)
+
+Refs https://github.com/libuv/libuv/pull/3524
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index 8cde389b826b6b167437845eccd5fed440dcadc4..147164e7ea25abbf655452930d78ee0a714cce36 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -387,30 +387,22 @@ static void uv__spawn_init_posix_spawn_fncs(void) {
+ 
+ 
+ static void uv__spawn_init_can_use_setsid(void) {
+-  static const int MACOS_CATALINA_VERSION_MAJOR = 19;
+-  char version_str[256];
+-  char* version_major_str;
+-  size_t version_str_size = 256;
+-  int r;
+-  int version_major;
+-
+-  /* Get a version string */
+-  r = sysctlbyname("kern.osrelease", version_str, &version_str_size, NULL, 0);
+-  if (r != 0)
++  int which[] = {CTL_KERN, KERN_OSRELEASE};
++  unsigned major;
++  unsigned minor;
++  unsigned patch;
++  char buf[256];
++  size_t len;
++
++  len = sizeof(buf);
++  if (sysctl(which, ARRAY_SIZE(which), buf, &len, NULL, 0))
+     return;
+ 
+-  /* Try to get the major version number. If not found
+-   * fall back to the fork/exec flow */
+-  version_major_str = strtok(version_str, ".");
+-  if (version_major_str == NULL)
++  /* NULL specifies to use LC_C_LOCALE */
++  if (3 != sscanf_l(buf, NULL, "%u.%u.%u", &major, &minor, &patch))
+     return;
+ 
+-  /* Parse the version major as a number. If it is greater than
+-   * the major version for macOS Catalina (aka macOS 10.15), then
+-   * the POSIX_SPAWN_SETSID flag is available */
+-  version_major = atoi_l(version_major_str, NULL); /* Use LC_C_LOCALE */
+-  if (version_major >= MACOS_CATALINA_VERSION_MAJOR)
+-    posix_spawn_can_use_setsid = 1;
++  posix_spawn_can_use_setsid = (major >= 19);  /* macOS Catalina */
+ }
+ 
+ 
diff --git a/patches/node/process_bsd_handle_kevent_note_exit_failure_3451.patch b/patches/node/process_bsd_handle_kevent_note_exit_failure_3451.patch
new file mode 100644
index 0000000000000..c0ab59bbeab92
--- /dev/null
+++ b/patches/node/process_bsd_handle_kevent_note_exit_failure_3451.patch
@@ -0,0 +1,70 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Tue, 1 Feb 2022 15:27:12 -0500
+Subject: process,bsd: handle kevent NOTE_EXIT failure (#3451)
+
+The kernel may return ESRCH if the child has already exited here.
+This is rather annoying, and means we must indirectly handle
+notification to our event loop of the process exit.
+
+Refs: https://github.com/libuv/libuv/pull/3441
+Refs: https://github.com/libuv/libuv/pull/3257
+
+diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
+index 16be13b99f5db77741aa276e90a437ef4eb5ba32..2dcc8b32f5165dd75061a1b55cc1abd2ab93ccc9 100644
+--- a/deps/uv/src/unix/internal.h
++++ b/deps/uv/src/unix/internal.h
+@@ -145,7 +145,8 @@ typedef struct uv__stream_queued_fds_s uv__stream_queued_fds_t;
+ 
+ /* loop flags */
+ enum {
+-  UV_LOOP_BLOCK_SIGPROF = 1
++  UV_LOOP_BLOCK_SIGPROF = 0x1,
++  UV_LOOP_REAP_CHILDREN = 0x2
+ };
+ 
+ /* flags of excluding ifaddr */
+diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
+index 35200f17495d80ed2d19ef9f6f76bbc92ee042f6..071fe0ce0938657d0fb840af62a432352e938a8a 100644
+--- a/deps/uv/src/unix/kqueue.c
++++ b/deps/uv/src/unix/kqueue.c
+@@ -285,7 +285,7 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
+     for (i = 0; i < nfds; i++) {
+       ev = events + i;
+       if (ev->filter == EVFILT_PROC) {
+-        uv__wait_children(loop);
++        loop->flags |= UV_LOOP_REAP_CHILDREN;
+         nevents++;
+         continue;
+       }
+@@ -383,6 +383,11 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
+       nevents++;
+     }
+ 
++    if (loop->flags & UV_LOOP_REAP_CHILDREN) {
++      loop->flags &= ~UV_LOOP_REAP_CHILDREN;
++      uv__wait_children(loop);
++    }
++
+     if (reset_timeout != 0) {
+       timeout = user_timeout;
+       reset_timeout = 0;
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index c1f6bd4b0076f0835caf83c45a6a896e7ae5def9..2920b942962357827bae9bcad23af8333e8b007f 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -507,8 +507,12 @@ int uv_spawn(uv_loop_t* loop,
+ #if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+     struct kevent event;
+     EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
+-    if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL))
+-      abort();
++    if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL)) {
++      if (errno != ESRCH)
++        abort();
++      /* Process already exited. Call waitpid on the next loop iteration. */
++      loop->flags |= UV_LOOP_REAP_CHILDREN;
++    }
+ #endif
+ 
+     QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);
diff --git a/patches/node/process_fix_hang_after_note_exit_3521.patch b/patches/node/process_fix_hang_after_note_exit_3521.patch
new file mode 100644
index 0000000000000..68702cf332b81
--- /dev/null
+++ b/patches/node/process_fix_hang_after_note_exit_3521.patch
@@ -0,0 +1,158 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Wed, 23 Mar 2022 15:39:38 +0900
+Subject: process: fix hang after NOTE_EXIT (#3521)
+
+Bug #3504 seems to affect more platforms than just OpenBSD. As this
+seems to be a race condition in these kernels, we do not want to fail
+because of it. Instead, we remove the WNOHANG flag from waitpid, and
+track exactly which processes have exited. Should also be a slight speed
+improvement for excessively large numbers of live children.
+
+diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
+index 071fe0ce0938657d0fb840af62a432352e938a8a..4c4d990ff5fa6c8ab937be2e4f79ccdaf90670c2 100644
+--- a/deps/uv/src/unix/kqueue.c
++++ b/deps/uv/src/unix/kqueue.c
+@@ -117,6 +117,7 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
+   unsigned int revents;
+   QUEUE* q;
+   uv__io_t* w;
++  uv_process_t* process;
+   sigset_t* pset;
+   sigset_t set;
+   uint64_t base;
+@@ -284,12 +285,22 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
+     loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
+     for (i = 0; i < nfds; i++) {
+       ev = events + i;
++      fd = ev->ident;
++
++      /* Handle kevent NOTE_EXIT results */
+       if (ev->filter == EVFILT_PROC) {
+-        loop->flags |= UV_LOOP_REAP_CHILDREN;
++        QUEUE_FOREACH(q, &loop->process_handles) {
++          process = QUEUE_DATA(q, uv_process_t, queue);
++          if (process->pid == fd) {
++            process->flags |= UV_HANDLE_REAP;
++            loop->flags |= UV_LOOP_REAP_CHILDREN;
++            break;
++          }
++        }
+         nevents++;
+         continue;
+       }
+-      fd = ev->ident;
++
+       /* Skip invalidated events, see uv__platform_invalidate_fd */
+       if (fd == -1)
+         continue;
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index 147164e7ea25abbf655452930d78ee0a714cce36..c8816b85b7e531648064e739fb89257565ad64bb 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -63,12 +63,18 @@ extern char **environ;
+ # include "zos-base.h"
+ #endif
+ 
+-#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
++#if defined(__APPLE__) || \
++    defined(__DragonFly__) || \
++    defined(__FreeBSD__) || \
++    defined(__NetBSD__) || \
++    defined(__OpenBSD__)
+ #include <sys/event.h>
++#else
++#define UV_USE_SIGCHLD
+ #endif
+ 
+ 
+-#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
++#ifdef UV_USE_SIGCHLD
+ static void uv__chld(uv_signal_t* handle, int signum) {
+   assert(signum == SIGCHLD);
+   uv__wait_children(handle->loop);
+@@ -80,6 +86,7 @@ void uv__wait_children(uv_loop_t* loop) {
+   int exit_status;
+   int term_signal;
+   int status;
++  int options;
+   pid_t pid;
+   QUEUE pending;
+   QUEUE* q;
+@@ -93,19 +100,33 @@ void uv__wait_children(uv_loop_t* loop) {
+     process = QUEUE_DATA(q, uv_process_t, queue);
+     q = QUEUE_NEXT(q);
+ 
++#ifndef UV_USE_SIGCHLD
++    if ((process->flags & UV_HANDLE_REAP) == 0)
++      continue;
++    options = 0;
++    process->flags &= ~UV_HANDLE_REAP;
++#else
++    options = WNOHANG;
++#endif
++
+     do
+-      pid = waitpid(process->pid, &status, WNOHANG);
++      pid = waitpid(process->pid, &status, options);
+     while (pid == -1 && errno == EINTR);
+ 
+-    if (pid == 0)
++#ifdef UV_USE_SIGCHLD
++    if (pid == 0) /* Not yet exited */
+       continue;
++#endif
+ 
+     if (pid == -1) {
+       if (errno != ECHILD)
+         abort();
++      /* The child died, and we missed it. This probably means someone else
++       * stole the waitpid from us. Handle this by not handling it at all. */
+       continue;
+     }
+ 
++    assert(pid == process->pid);
+     process->status = status;
+     QUEUE_REMOVE(&process->queue);
+     QUEUE_INSERT_TAIL(&pending, &process->queue);
+@@ -964,7 +985,7 @@ int uv_spawn(uv_loop_t* loop,
+       goto error;
+   }
+ 
+-#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
++#ifdef UV_USE_SIGCHLD
+   uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
+ #endif
+ 
+@@ -983,13 +1004,14 @@ int uv_spawn(uv_loop_t* loop,
+    * fail to open a stdio handle. This ensures we can eventually reap the child
+    * with waitpid. */
+   if (exec_errorno == 0) {
+-#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
++#ifndef UV_USE_SIGCHLD
+     struct kevent event;
+     EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
+     if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL)) {
+       if (errno != ESRCH)
+         abort();
+       /* Process already exited. Call waitpid on the next loop iteration. */
++      process->flags |= UV_HANDLE_REAP;
+       loop->flags |= UV_LOOP_REAP_CHILDREN;
+     }
+ #endif
+diff --git a/deps/uv/src/uv-common.h b/deps/uv/src/uv-common.h
+index 8a190bf8fa8c5a282feaf251aec2a30c95776888..6001b0cf68d0b0268b578218b664a737f43c9521 100644
+--- a/deps/uv/src/uv-common.h
++++ b/deps/uv/src/uv-common.h
+@@ -130,7 +130,10 @@ enum {
+   UV_SIGNAL_ONE_SHOT                    = 0x02000000,
+ 
+   /* Only used by uv_poll_t handles. */
+-  UV_HANDLE_POLL_SLOW                   = 0x01000000
++  UV_HANDLE_POLL_SLOW                   = 0x01000000,
++
++  /* Only used by uv_process_t handles. */
++  UV_HANDLE_REAP                        = 0x10000000
+ };
+ 
+ int uv__loop_configure(uv_loop_t* loop, uv_loop_option option, va_list ap);
diff --git a/patches/node/process_monitor_for_exit_with_kqueue_on_bsds_3441.patch b/patches/node/process_monitor_for_exit_with_kqueue_on_bsds_3441.patch
new file mode 100644
index 0000000000000..917b589dbd37b
--- /dev/null
+++ b/patches/node/process_monitor_for_exit_with_kqueue_on_bsds_3441.patch
@@ -0,0 +1,170 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jeremy Rose <nornagon@nornagon.net>
+Date: Mon, 31 Jan 2022 11:49:22 -0800
+Subject: process: monitor for exit with kqueue on BSDs (#3441)
+
+This adds a workaround for an xnu kernel bug that sometimes results in
+SIGCHLD not being delivered. The workaround is to use kevent to listen
+for EVFILT_PROC/NOTE_EXIT events instead of relying on SIGCHLD on *BSD.
+
+Apple rdar: FB9529664
+Refs: https://github.com/libuv/libuv/pull/3257
+
+diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
+index 12d4da93686e993830a7d09e74d08191fc808f4f..16be13b99f5db77741aa276e90a437ef4eb5ba32 100644
+--- a/deps/uv/src/unix/internal.h
++++ b/deps/uv/src/unix/internal.h
+@@ -282,6 +282,7 @@ uv_handle_type uv__handle_type(int fd);
+ FILE* uv__open_file(const char* path);
+ int uv__getpwuid_r(uv_passwd_t* pwd);
+ int uv__search_path(const char* prog, char* buf, size_t* buflen);
++void uv__wait_children(uv_loop_t* loop);
+ 
+ /* random */
+ int uv__random_devurandom(void* buf, size_t buflen);
+diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
+index bf183d5fdc0ba89913469a294322eef84bc4cee8..35200f17495d80ed2d19ef9f6f76bbc92ee042f6 100644
+--- a/deps/uv/src/unix/kqueue.c
++++ b/deps/uv/src/unix/kqueue.c
+@@ -284,6 +284,11 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
+     loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
+     for (i = 0; i < nfds; i++) {
+       ev = events + i;
++      if (ev->filter == EVFILT_PROC) {
++        uv__wait_children(loop);
++        nevents++;
++        continue;
++      }
+       fd = ev->ident;
+       /* Skip invalidated events, see uv__platform_invalidate_fd */
+       if (fd == -1)
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index f4aebb0490e198cd9adcadfeb6b006de479cc993..cfcba341e0e380ecd595e4b59e39c08a7b374a48 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -48,10 +48,20 @@ extern char **environ;
+ # include "zos-base.h"
+ #endif
+ 
++#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
++#include <sys/event.h>
++#endif
++
+ 
++#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
+ static void uv__chld(uv_signal_t* handle, int signum) {
++  assert(signum == SIGCHLD);
++  uv__wait_children(handle->loop);
++}
++#endif
++
++void uv__wait_children(uv_loop_t* loop) {
+   uv_process_t* process;
+-  uv_loop_t* loop;
+   int exit_status;
+   int term_signal;
+   int status;
+@@ -60,10 +70,7 @@ static void uv__chld(uv_signal_t* handle, int signum) {
+   QUEUE* q;
+   QUEUE* h;
+ 
+-  assert(signum == SIGCHLD);
+-
+   QUEUE_INIT(&pending);
+-  loop = handle->loop;
+ 
+   h = &loop->process_handles;
+   q = QUEUE_HEAD(h);
+@@ -419,7 +426,9 @@ int uv_spawn(uv_loop_t* loop,
+   if (err)
+     goto error;
+ 
++#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
+   uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
++#endif
+ 
+   /* Acquire write lock to prevent opening new fds in worker threads */
+   uv_rwlock_wrlock(&loop->cloexec_lock);
+@@ -478,6 +487,13 @@ int uv_spawn(uv_loop_t* loop,
+ 
+   /* Only activate this handle if exec() happened successfully */
+   if (exec_errorno == 0) {
++#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
++    struct kevent event;
++    EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
++    if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL))
++      abort();
++#endif
++
+     QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);
+     uv__handle_start(process);
+   }
+diff --git a/deps/uv/test/test-list.h b/deps/uv/test/test-list.h
+index 59b95da9ebe3464bd1f9ce1c534122b1f9e06636..58489c4be7b3a7b36d5b01a1f07d411ef3d99ae3 100644
+--- a/deps/uv/test/test-list.h
++++ b/deps/uv/test/test-list.h
+@@ -318,6 +318,7 @@ TEST_DECLARE   (spawn_reads_child_path)
+ TEST_DECLARE   (spawn_inherit_streams)
+ TEST_DECLARE   (spawn_quoted_path)
+ TEST_DECLARE   (spawn_tcp_server)
++TEST_DECLARE   (spawn_exercise_sigchld_issue)
+ TEST_DECLARE   (fs_poll)
+ TEST_DECLARE   (fs_poll_getpath)
+ TEST_DECLARE   (fs_poll_close_request)
+@@ -944,6 +945,7 @@ TASK_LIST_START
+   TEST_ENTRY  (spawn_inherit_streams)
+   TEST_ENTRY  (spawn_quoted_path)
+   TEST_ENTRY  (spawn_tcp_server)
++  TEST_ENTRY  (spawn_exercise_sigchld_issue)
+   TEST_ENTRY  (fs_poll)
+   TEST_ENTRY  (fs_poll_getpath)
+   TEST_ENTRY  (fs_poll_close_request)
+diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c
+index 9f2eb24b2d6daf339bec12027134409cfc3b4a82..dfd5458ef37c664af9a55a8383bdb3121885db3b 100644
+--- a/deps/uv/test/test-spawn.c
++++ b/deps/uv/test/test-spawn.c
+@@ -1891,6 +1891,44 @@ TEST_IMPL(spawn_quoted_path) {
+ #endif
+ }
+ 
++TEST_IMPL(spawn_exercise_sigchld_issue) {
++  int r;
++  int i;
++  uv_process_options_t dummy_options = {0};
++  uv_process_t dummy_processes[100];
++  char* args[2];
++
++  init_process_options("spawn_helper1", exit_cb);
++
++  r = uv_spawn(uv_default_loop(), &process, &options);
++  ASSERT_EQ(r, 0);
++
++  // This test exercises a bug in the darwin kernel that causes SIGCHLD not to
++  // be delivered sometimes. Calling posix_spawn many times increases the
++  // likelihood of encountering this issue, so spin a few times to make this
++  // test more reliable.
++  dummy_options.file = args[0] = "program-that-had-better-not-exist";
++  args[1] = NULL;
++  dummy_options.args = args;
++  dummy_options.exit_cb = fail_cb;
++  dummy_options.flags = 0;
++  for (i = 0; i < 100; i++) {
++    r = uv_spawn(uv_default_loop(), &dummy_processes[i], &dummy_options);
++    if (r != UV_ENOENT)
++      ASSERT_EQ(r, UV_EACCES);
++    uv_close((uv_handle_t*) &dummy_processes[i], close_cb);
++  }
++
++  r = uv_run(uv_default_loop(), UV_RUN_DEFAULT);
++  ASSERT_EQ(r, 0);
++
++  ASSERT_EQ(exit_cb_called, 1);
++  ASSERT_EQ(close_cb_called, 101);
++
++  MAKE_VALGRIND_HAPPY();
++  return 0;
++}
++
+ /* Helper for child process of spawn_inherit_streams */
+ #ifndef _WIN32
+ void spawn_stdin_stdout(void) {
diff --git a/patches/node/process_only_use_f_dupfd_cloexec_if_it_is_defined_3512.patch b/patches/node/process_only_use_f_dupfd_cloexec_if_it_is_defined_3512.patch
new file mode 100644
index 0000000000000..5b525fb18a5f0
--- /dev/null
+++ b/patches/node/process_only_use_f_dupfd_cloexec_if_it_is_defined_3512.patch
@@ -0,0 +1,54 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Sat, 5 Mar 2022 12:52:04 -0500
+Subject: process: only use F_DUPFD_CLOEXEC if it is defined (#3512)
+
+We can save a syscall on most modern systems (required by POSIX 2008),
+but not on all systems.
+
+Also handle errors from CLOEXEC. Even though fcntl does not really
+define there to be any, it could theoretically be EBADF if the user
+happened to pass a bad file descriptor to the same number fd (such that
+no other code happened to already fail on that).
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index d208f99be40df9f36447552daf2772c1cab1ce79..7705068730cb0536998bad7d304cb87df99b72e8 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -275,9 +275,20 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     use_fd = pipes[fd][1];
+     if (use_fd < 0 || use_fd >= fd)
+       continue;
++#ifdef F_DUPFD_CLOEXEC /* POSIX 2008 */
+     pipes[fd][1] = fcntl(use_fd, F_DUPFD_CLOEXEC, stdio_count);
++#else
++    pipes[fd][1] = fcntl(use_fd, F_DUPFD, stdio_count);
++#endif
+     if (pipes[fd][1] == -1)
+       uv__write_errno(error_fd);
++#ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
++    n = uv__cloexec_fcntl(pipes[fd][1], 1);
++    if (n) {
++      uv__write_int(error_fd, n);
++      _exit(127);
++    }
++#endif
+   }
+ 
+   for (fd = 0; fd < stdio_count; fd++) {
+@@ -300,8 +311,13 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     }
+ 
+     if (fd == use_fd) {
+-      if (close_fd == -1)
+-        uv__cloexec_fcntl(use_fd, 0);
++      if (close_fd == -1) {
++        n = uv__cloexec_fcntl(use_fd, 0);
++        if (n) {
++          uv__write_int(error_fd, n);
++          _exit(127);
++        }
++      }
+     }
+     else {
+       fd = dup2(use_fd, fd);
diff --git a/patches/node/process_reset_the_signal_mask_if_the_fork_fails_3537.patch b/patches/node/process_reset_the_signal_mask_if_the_fork_fails_3537.patch
new file mode 100644
index 0000000000000..e0cd88c292b7e
--- /dev/null
+++ b/patches/node/process_reset_the_signal_mask_if_the_fork_fails_3537.patch
@@ -0,0 +1,36 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Fri, 11 Mar 2022 12:05:24 -0500
+Subject: process: reset the signal mask if the fork fails (#3537)
+
+Fix a regression that sneaked into posix spawn changes.
+
+Refs: https://github.com/libuv/libuv/pull/3257
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index b85aa3b94edd040952e0d350a47a38d9ba8a67d3..d208f99be40df9f36447552daf2772c1cab1ce79 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -790,11 +790,6 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
+ 
+   *pid = fork();
+ 
+-  if (*pid == -1) {
+-    /* Failed to fork */
+-    return UV__ERR(errno);
+-  }
+-
+   if (*pid == 0) {
+     /* Fork succeeded, in the child process */
+     uv__process_child_init(options, stdio_count, pipes, error_fd);
+@@ -804,6 +799,10 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
+   if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
+     abort();
+ 
++  if (*pid == -1)
++    /* Failed to fork */
++    return UV__ERR(errno);
++
+   /* Fork succeeded, in the parent process */
+   return 0;
+ }
diff --git a/patches/node/process_simplify_uv_write_int_calls_3519.patch b/patches/node/process_simplify_uv_write_int_calls_3519.patch
new file mode 100644
index 0000000000000..201c9c949d49a
--- /dev/null
+++ b/patches/node/process_simplify_uv_write_int_calls_3519.patch
@@ -0,0 +1,63 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Mon, 7 Mar 2022 17:07:49 -0500
+Subject: process: simplify uv__write_int calls (#3519)
+
+Refs https://github.com/libuv/libuv/pull/3519
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index b6f9756c6a6710f5f10762b9299cc35047b98097..8cde389b826b6b167437845eccd5fed440dcadc4 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -216,16 +216,14 @@ static void uv__write_int(int fd, int val) {
+     n = write(fd, &val, sizeof(val));
+   while (n == -1 && errno == EINTR);
+ 
+-  if (n == -1 && errno == EPIPE)
+-    return; /* parent process has quit */
+-
+-  assert(n == sizeof(val));
++  /* The write might have failed (e.g. if the parent process has died),
++   * but we have nothing left but to _exit ourself now too. */
++  _exit(127);
+ }
+ 
+ 
+ static void uv__write_errno(int error_fd) {
+   uv__write_int(error_fd, UV__ERR(errno));
+-  _exit(127);
+ }
+ 
+ 
+@@ -284,10 +282,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
+       uv__write_errno(error_fd);
+ #ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
+     n = uv__cloexec(pipes[fd][1], 1);
+-    if (n) {
++    if (n)
+       uv__write_int(error_fd, n);
+-      _exit(127);
+-    }
+ #endif
+   }
+ 
+@@ -313,10 +309,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     if (fd == use_fd) {
+       if (close_fd == -1) {
+         n = uv__cloexec(use_fd, 0);
+-        if (n) {
++        if (n)
+           uv__write_int(error_fd, n);
+-          _exit(127);
+-        }
+       }
+     }
+     else {
+@@ -368,7 +362,6 @@ static void uv__process_child_init(const uv_process_options_t* options,
+ #endif
+ 
+   uv__write_errno(error_fd);
+-  abort();
+ }
+ #endif
+ 
diff --git a/patches/node/reland_macos_use_posix_spawn_instead_of_fork_3257.patch b/patches/node/reland_macos_use_posix_spawn_instead_of_fork_3257.patch
new file mode 100644
index 0000000000000..cccbbf11736e0
--- /dev/null
+++ b/patches/node/reland_macos_use_posix_spawn_instead_of_fork_3257.patch
@@ -0,0 +1,889 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Wed, 2 Mar 2022 15:15:39 -0500
+Subject: Reland "macos: use posix_spawn instead of fork" (#3257)
+
+Fixes: https://github.com/libuv/libuv/issues/3050
+Refs: https://github.com/libuv/libuv/issues/3086
+Refs: https://github.com/libuv/libuv/pull/3064
+Refs: https://github.com/libuv/libuv/pull/3107
+Refs: https://github.com/libuv/libuv/pull/3064
+
+This reverts commit 217fdf4265589889d00c7c0622fde2710971a020, then fixes
+several issues with it:
+
+* remove error fast-cleanup code that triggers a nodejs bug
+
+Refs: https://github.com/libuv/libuv/pull/3107#issuecomment-782482608
+
+* protect posix_spawn from EINTR
+
+This is not a documented valid error, but seems to have been observed.
+
+* ignore setuid/setgid syscall
+
+This kernel function is not permitted unless the process is setuid root,
+so disable this syscall. Falling back to fork/exec should be okay for
+the rare cases that the user decides they need to do setuid(getuid()) or
+setuid(geteuid()) for the child.
+
+Refs: https://github.com/libuv/libuv/pull/3107#issuecomment-782482608
+
+* improve posix_spawn path search
+
+Ports the improvements in musl back to this function
+
+* fix some additional problems and formatting issues
+
+We previously might fail to start a watcher, in rare failure cases,
+resulting in a zombie that we would fail to kill. Also avoid creating
+the signal-pipe unless required (addresses a review comment from Apple)
+
+* fix fd->fd mapping reuse
+
+There was a chance that when duplicating the fd's into stdio_count+fd we
+might be closing a currently opened fd with that value.
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index 2920b942962357827bae9bcad23af8333e8b007f..b85aa3b94edd040952e0d350a47a38d9ba8a67d3 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -27,6 +27,7 @@
+ #include <assert.h>
+ #include <errno.h>
+ #include <signal.h>
++#include <string.h>
+ 
+ #include <sys/types.h>
+ #include <sys/wait.h>
+@@ -35,8 +36,21 @@
+ #include <poll.h>
+ 
+ #if defined(__APPLE__) && !TARGET_OS_IPHONE
++# include <spawn.h>
++# include <paths.h>
++# include <sys/kauth.h>
++# include <sys/types.h>
++# include <sys/sysctl.h>
++# include <dlfcn.h>
+ # include <crt_externs.h>
++# include <xlocale.h>
+ # define environ (*_NSGetEnviron())
++
++/* macOS 10.14 back does not define this constant */
++# ifndef POSIX_SPAWN_SETSID
++#  define POSIX_SPAWN_SETSID 1024
++# endif
++
+ #else
+ extern char **environ;
+ #endif
+@@ -261,22 +275,22 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     use_fd = pipes[fd][1];
+     if (use_fd < 0 || use_fd >= fd)
+       continue;
+-    pipes[fd][1] = fcntl(use_fd, F_DUPFD, stdio_count);
++    pipes[fd][1] = fcntl(use_fd, F_DUPFD_CLOEXEC, stdio_count);
+     if (pipes[fd][1] == -1)
+       uv__write_errno(error_fd);
+   }
+ 
+   for (fd = 0; fd < stdio_count; fd++) {
+-    close_fd = pipes[fd][0];
++    close_fd = -1;
+     use_fd = pipes[fd][1];
+ 
+     if (use_fd < 0) {
+       if (fd >= 3)
+         continue;
+       else {
+-        /* redirect stdin, stdout and stderr to /dev/null even if UV_IGNORE is
+-         * set
+-         */
++        /* Redirect stdin, stdout and stderr to /dev/null even if UV_IGNORE is
++         * set. */
++        uv__close_nocheckstdio(fd); /* Free up fd, if it happens to be open. */
+         use_fd = open("/dev/null", fd == 0 ? O_RDONLY : O_RDWR);
+         close_fd = use_fd;
+ 
+@@ -285,28 +299,24 @@ static void uv__process_child_init(const uv_process_options_t* options,
+       }
+     }
+ 
+-    if (fd == use_fd)
+-      uv__cloexec_fcntl(use_fd, 0);
+-    else
++    if (fd == use_fd) {
++      if (close_fd == -1)
++        uv__cloexec_fcntl(use_fd, 0);
++    }
++    else {
+       fd = dup2(use_fd, fd);
++    }
+ 
+     if (fd == -1)
+       uv__write_errno(error_fd);
+ 
+-    if (fd <= 2)
++    if (fd <= 2 && close_fd == -1)
+       uv__nonblock_fcntl(fd, 0);
+ 
+     if (close_fd >= stdio_count)
+       uv__close(close_fd);
+   }
+ 
+-  for (fd = 0; fd < stdio_count; fd++) {
+-    use_fd = pipes[fd][1];
+-
+-    if (use_fd >= stdio_count)
+-      uv__close(use_fd);
+-  }
+-
+   if (options->cwd != NULL && chdir(options->cwd))
+     uv__write_errno(error_fd);
+ 
+@@ -327,9 +337,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
+   if ((options->flags & UV_PROCESS_SETUID) && setuid(options->uid))
+     uv__write_errno(error_fd);
+ 
+-  if (options->env != NULL) {
++  if (options->env != NULL)
+     environ = options->env;
+-  }
+ 
+   /* Reset signal mask just before exec. */
+   sigemptyset(&signewset);
+@@ -348,6 +357,562 @@ static void uv__process_child_init(const uv_process_options_t* options,
+ #endif
+ 
+ 
++#if defined(__APPLE__)
++typedef struct uv__posix_spawn_fncs_tag {
++  struct {
++    int (*addchdir_np)(const posix_spawn_file_actions_t *, const char *);
++  } file_actions;
++} uv__posix_spawn_fncs_t;
++
++
++static uv_once_t posix_spawn_init_once = UV_ONCE_INIT;
++static uv__posix_spawn_fncs_t posix_spawn_fncs;
++static int posix_spawn_can_use_setsid;
++
++
++static void uv__spawn_init_posix_spawn_fncs(void) {
++  /* Try to locate all non-portable functions at runtime */
++  posix_spawn_fncs.file_actions.addchdir_np =
++    dlsym(RTLD_DEFAULT, "posix_spawn_file_actions_addchdir_np");
++}
++
++
++static void uv__spawn_init_can_use_setsid(void) {
++  static const int MACOS_CATALINA_VERSION_MAJOR = 19;
++  char version_str[256];
++  char* version_major_str;
++  size_t version_str_size = 256;
++  int r;
++  int version_major;
++
++  /* Get a version string */
++  r = sysctlbyname("kern.osrelease", version_str, &version_str_size, NULL, 0);
++  if (r != 0)
++    return;
++
++  /* Try to get the major version number. If not found
++   * fall back to the fork/exec flow */
++  version_major_str = strtok(version_str, ".");
++  if (version_major_str == NULL)
++    return;
++
++  /* Parse the version major as a number. If it is greater than
++   * the major version for macOS Catalina (aka macOS 10.15), then
++   * the POSIX_SPAWN_SETSID flag is available */
++  version_major = atoi_l(version_major_str, NULL); /* Use LC_C_LOCALE */
++  if (version_major >= MACOS_CATALINA_VERSION_MAJOR)
++    posix_spawn_can_use_setsid = 1;
++}
++
++
++static void uv__spawn_init_posix_spawn(void) {
++  /* Init handles to all potentially non-defined functions */
++  uv__spawn_init_posix_spawn_fncs();
++
++  /* Init feature detection for POSIX_SPAWN_SETSID flag */
++  uv__spawn_init_can_use_setsid();
++}
++
++
++static int uv__spawn_set_posix_spawn_attrs(
++    posix_spawnattr_t* attrs,
++    const uv__posix_spawn_fncs_t* posix_spawn_fncs,
++    const uv_process_options_t* options) {
++  int err;
++  unsigned int flags;
++  sigset_t signal_set;
++
++  err = posix_spawnattr_init(attrs);
++  if (err != 0) {
++    /* If initialization fails, no need to de-init, just return */
++    return err;
++  }
++
++  if (options->flags & (UV_PROCESS_SETUID | UV_PROCESS_SETGID)) {
++    /* kauth_cred_issuser currently requires exactly uid == 0 for these
++     * posixspawn_attrs (set_groups_np, setuid_np, setgid_np), which deviates
++     * from the normal specification of setuid (which also uses euid), and they
++     * are also undocumented syscalls, so we do not use them. */
++    err = ENOSYS;
++    goto error;
++  }
++
++  /* Set flags for spawn behavior
++   * 1) POSIX_SPAWN_CLOEXEC_DEFAULT: (Apple Extension) All descriptors in the
++   *    parent will be treated as if they had been created with O_CLOEXEC. The
++   *    only fds that will be passed on to the child are those manipulated by
++   *    the file actions
++   * 2) POSIX_SPAWN_SETSIGDEF: Signals mentioned in spawn-sigdefault in the
++   *    spawn attributes will be reset to behave as their default
++   * 3) POSIX_SPAWN_SETSIGMASK: Signal mask will be set to the value of
++   *    spawn-sigmask in attributes
++   * 4) POSIX_SPAWN_SETSID: Make the process a new session leader if a detached
++   *    session was requested. */
++  flags = POSIX_SPAWN_CLOEXEC_DEFAULT |
++          POSIX_SPAWN_SETSIGDEF |
++          POSIX_SPAWN_SETSIGMASK;
++  if (options->flags & UV_PROCESS_DETACHED) {
++    /* If running on a version of macOS where this flag is not supported,
++     * revert back to the fork/exec flow. Otherwise posix_spawn will
++     * silently ignore the flag. */
++    if (!posix_spawn_can_use_setsid) {
++      err = ENOSYS;
++      goto error;
++    }
++
++    flags |= POSIX_SPAWN_SETSID;
++  }
++  err = posix_spawnattr_setflags(attrs, flags);
++  if (err != 0)
++    goto error;
++
++  /* Reset all signal the child to their default behavior */
++  sigfillset(&signal_set);
++  err = posix_spawnattr_setsigdefault(attrs, &signal_set);
++  if (err != 0)
++    goto error;
++
++  /* Reset the signal mask for all signals */
++  sigemptyset(&signal_set);
++  err = posix_spawnattr_setsigmask(attrs, &signal_set);
++  if (err != 0)
++    goto error;
++
++  return err;
++
++error:
++  (void) posix_spawnattr_destroy(attrs);
++  return err;
++}
++
++
++static int uv__spawn_set_posix_spawn_file_actions(
++    posix_spawn_file_actions_t* actions,
++    const uv__posix_spawn_fncs_t* posix_spawn_fncs,
++    const uv_process_options_t* options,
++    int stdio_count,
++    int (*pipes)[2]) {
++  int fd;
++  int fd2;
++  int use_fd;
++  int err;
++
++  err = posix_spawn_file_actions_init(actions);
++  if (err != 0) {
++    /* If initialization fails, no need to de-init, just return */
++    return err;
++  }
++
++  /* Set the current working directory if requested */
++  if (options->cwd != NULL) {
++    if (posix_spawn_fncs->file_actions.addchdir_np == NULL) {
++      err = ENOSYS;
++      goto error;
++    }
++
++    err = posix_spawn_fncs->file_actions.addchdir_np(actions, options->cwd);
++    if (err != 0)
++      goto error;
++  }
++
++  /* Do not return ENOSYS after this point, as we may mutate pipes. */
++
++  /* First duplicate low numbered fds, since it's not safe to duplicate them,
++   * they could get replaced. Example: swapping stdout and stderr; without
++   * this fd 2 (stderr) would be duplicated into fd 1, thus making both
++   * stdout and stderr go to the same fd, which was not the intention. */
++  for (fd = 0; fd < stdio_count; fd++) {
++    use_fd = pipes[fd][1];
++    if (use_fd < 0 || use_fd >= fd)
++      continue;
++    use_fd = stdio_count;
++    for (fd2 = 0; fd2 < stdio_count; fd2++) {
++      /* If we were not setting POSIX_SPAWN_CLOEXEC_DEFAULT, we would need to
++       * also consider whether fcntl(fd, F_GETFD) returned without the
++       * FD_CLOEXEC flag set. */
++      if (pipes[fd2][1] == use_fd) {
++        use_fd++;
++        fd2 = 0;
++      }
++    }
++    err = posix_spawn_file_actions_adddup2(
++      actions,
++      pipes[fd][1],
++      use_fd);
++    assert(err != ENOSYS);
++    if (err != 0)
++      goto error;
++    pipes[fd][1] = use_fd;
++  }
++
++  /* Second, move the descriptors into their respective places */
++  for (fd = 0; fd < stdio_count; fd++) {
++    use_fd = pipes[fd][1];
++    if (use_fd < 0) {
++      if (fd >= 3)
++        continue;
++      else {
++        /* If ignored, redirect to (or from) /dev/null, */
++        err = posix_spawn_file_actions_addopen(
++          actions,
++          fd,
++          "/dev/null",
++          fd == 0 ? O_RDONLY : O_RDWR,
++          0);
++        assert(err != ENOSYS);
++        if (err != 0)
++          goto error;
++        continue;
++      }
++    }
++
++    if (fd == use_fd)
++        err = posix_spawn_file_actions_addinherit_np(actions, fd);
++    else
++        err = posix_spawn_file_actions_adddup2(actions, use_fd, fd);
++    assert(err != ENOSYS);
++    if (err != 0)
++      goto error;
++
++    /* Make sure the fd is marked as non-blocking (state shared between child
++     * and parent). */
++    uv__nonblock_fcntl(use_fd, 0);
++  }
++
++  /* Finally, close all the superfluous descriptors */
++  for (fd = 0; fd < stdio_count; fd++) {
++    use_fd = pipes[fd][1];
++    if (use_fd < stdio_count)
++      continue;
++
++    /* Check if we already closed this. */
++    for (fd2 = 0; fd2 < fd; fd2++) {
++      if (pipes[fd2][1] == use_fd)
++          break;
++    }
++    if (fd2 < fd)
++      continue;
++
++    err = posix_spawn_file_actions_addclose(actions, use_fd);
++    assert(err != ENOSYS);
++    if (err != 0)
++      goto error;
++  }
++
++  return 0;
++
++error:
++  (void) posix_spawn_file_actions_destroy(actions);
++  return err;
++}
++
++char* uv__spawn_find_path_in_env(char** env) {
++  char** env_iterator;
++  const char path_var[] = "PATH=";
++
++  /* Look for an environment variable called PATH in the
++   * provided env array, and return its value if found */
++  for (env_iterator = env; *env_iterator != NULL; env_iterator++) {
++    if (strncmp(*env_iterator, path_var, sizeof(path_var) - 1) == 0) {
++      /* Found "PATH=" at the beginning of the string */
++      return *env_iterator + sizeof(path_var) - 1;
++    }
++  }
++
++  return NULL;
++}
++
++
++static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options,
++                                       posix_spawnattr_t* attrs,
++                                       posix_spawn_file_actions_t* actions,
++                                       pid_t* pid) {
++  const char *p;
++  const char *z;
++  const char *path;
++  size_t l;
++  size_t k;
++  int err;
++  int seen_eacces;
++
++  path = NULL;
++  err = -1;
++  seen_eacces = 0;
++
++  /* Short circuit for erroneous case */
++  if (options->file == NULL)
++    return ENOENT;
++
++  /* The environment for the child process is that of the parent unless overriden
++   * by options->env */
++  char** env = environ;
++  if (options->env != NULL)
++    env = options->env;
++
++  /* If options->file contains a slash, posix_spawn/posix_spawnp behave
++   * the same, and don't involve PATH resolution at all. Otherwise, if
++   * options->file does not include a slash, but no custom environment is
++   * to be used, the environment used for path resolution as well for the
++   * child process is that of the parent process, so posix_spawnp is the
++   * way to go. */
++  if (strchr(options->file, '/') != NULL || options->env == NULL) {
++    do
++      err = posix_spawnp(pid, options->file, actions, attrs, options->args, env);
++    while (err == EINTR);
++    return err;
++  }
++
++  /* Look for the definition of PATH in the provided env */
++  path = uv__spawn_find_path_in_env(options->env);
++
++  /* The following resolution logic (execvpe emulation) is copied from
++   * https://git.musl-libc.org/cgit/musl/tree/src/process/execvp.c
++   * and adapted to work for our specific usage */
++
++  /* If no path was provided in options->env, use the default value
++   * to look for the executable */
++  if (path == NULL)
++    path = _PATH_DEFPATH;
++
++  k = strnlen(options->file, NAME_MAX + 1);
++  if (k > NAME_MAX)
++    return ENAMETOOLONG;
++
++  l = strnlen(path, PATH_MAX - 1) + 1;
++
++  for (p = path;; p = z) {
++    /* Compose the new process file from the entry in the PATH
++     * environment variable and the actual file name */
++    char b[PATH_MAX + NAME_MAX];
++    z = strchr(p, ':');
++    if (!z)
++      z = p + strlen(p);
++    if ((size_t)(z - p) >= l) {
++      if (!*z++)
++        break;
++
++      continue;
++    }
++    memcpy(b, p, z - p);
++    b[z - p] = '/';
++    memcpy(b + (z - p) + (z > p), options->file, k + 1);
++
++    /* Try to spawn the new process file. If it fails with ENOENT, the
++     * new process file is not in this PATH entry, continue with the next
++     * PATH entry. */
++    do
++      err = posix_spawn(pid, b, actions, attrs, options->args, env);
++    while (err == EINTR);
++
++    switch (err) {
++    case EACCES:
++      seen_eacces = 1;
++      break; /* continue search */
++    case ENOENT:
++    case ENOTDIR:
++      break; /* continue search */
++    default:
++      return err;
++    }
++
++    if (!*z++)
++      break;
++  }
++
++  if (seen_eacces)
++    return EACCES;
++  return err;
++}
++
++
++static int uv__spawn_and_init_child_posix_spawn(
++    const uv_process_options_t* options,
++    int stdio_count,
++    int (*pipes)[2],
++    pid_t* pid,
++    const uv__posix_spawn_fncs_t* posix_spawn_fncs) {
++  int err;
++  posix_spawnattr_t attrs;
++  posix_spawn_file_actions_t actions;
++
++  err = uv__spawn_set_posix_spawn_attrs(&attrs, posix_spawn_fncs, options);
++  if (err != 0)
++    goto error;
++
++  /* This may mutate pipes. */
++  err = uv__spawn_set_posix_spawn_file_actions(&actions,
++                                               posix_spawn_fncs,
++                                               options,
++                                               stdio_count,
++                                               pipes);
++  if (err != 0) {
++    (void) posix_spawnattr_destroy(&attrs);
++    goto error;
++  }
++
++  /* Try to spawn options->file resolving in the provided environment
++   * if any */
++  err = uv__spawn_resolve_and_spawn(options, &attrs, &actions, pid);
++  assert(err != ENOSYS);
++
++  /* Destroy the actions/attributes */
++  (void) posix_spawn_file_actions_destroy(&actions);
++  (void) posix_spawnattr_destroy(&attrs);
++
++error:
++  /* In an error situation, the attributes and file actions are
++   * already destroyed, only the happy path requires cleanup */
++  return UV__ERR(err);
++}
++#endif
++
++static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
++                                         int stdio_count,
++                                         int (*pipes)[2],
++                                         int error_fd,
++                                         pid_t* pid) {
++  sigset_t signewset;
++  sigset_t sigoldset;
++
++  /* Start the child with most signals blocked, to avoid any issues before we
++   * can reset them, but allow program failures to exit (and not hang). */
++  sigfillset(&signewset);
++  sigdelset(&signewset, SIGKILL);
++  sigdelset(&signewset, SIGSTOP);
++  sigdelset(&signewset, SIGTRAP);
++  sigdelset(&signewset, SIGSEGV);
++  sigdelset(&signewset, SIGBUS);
++  sigdelset(&signewset, SIGILL);
++  sigdelset(&signewset, SIGSYS);
++  sigdelset(&signewset, SIGABRT);
++  if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
++    abort();
++
++  *pid = fork();
++
++  if (*pid == -1) {
++    /* Failed to fork */
++    return UV__ERR(errno);
++  }
++
++  if (*pid == 0) {
++    /* Fork succeeded, in the child process */
++    uv__process_child_init(options, stdio_count, pipes, error_fd);
++    abort();
++  }
++
++  if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
++    abort();
++
++  /* Fork succeeded, in the parent process */
++  return 0;
++}
++
++static int uv__spawn_and_init_child(
++    uv_loop_t* loop,
++    const uv_process_options_t* options,
++    int stdio_count,
++    int (*pipes)[2],
++    pid_t* pid) {
++  int signal_pipe[2] = { -1, -1 };
++  int status;
++  int err;
++  int exec_errorno;
++  ssize_t r;
++
++#if defined(__APPLE__)
++  uv_once(&posix_spawn_init_once, uv__spawn_init_posix_spawn);
++
++  /* Special child process spawn case for macOS Big Sur (11.0) onwards
++   *
++   * Big Sur introduced a significant performance degradation on a call to
++   * fork/exec when the process has many pages mmaped in with MAP_JIT, like, say
++   * a javascript interpreter. Electron-based applications, for example,
++   * are impacted; though the magnitude of the impact depends on how much the
++   * app relies on subprocesses.
++   *
++   * On macOS, though, posix_spawn is implemented in a way that does not
++   * exhibit the problem. This block implements the forking and preparation
++   * logic with posix_spawn and its related primitives. It also takes advantage of
++   * the macOS extension POSIX_SPAWN_CLOEXEC_DEFAULT that makes impossible to
++   * leak descriptors to the child process. */
++  err = uv__spawn_and_init_child_posix_spawn(options,
++                                             stdio_count,
++                                             pipes,
++                                             pid,
++                                             &posix_spawn_fncs);
++
++  /* The posix_spawn flow will return UV_ENOSYS if any of the posix_spawn_x_np
++   * non-standard functions is both _needed_ and _undefined_. In those cases,
++   * default back to the fork/execve strategy. For all other errors, just fail. */
++  if (err != UV_ENOSYS)
++    return err;
++
++#endif
++
++  /* This pipe is used by the parent to wait until
++   * the child has called `execve()`. We need this
++   * to avoid the following race condition:
++   *
++   *    if ((pid = fork()) > 0) {
++   *      kill(pid, SIGTERM);
++   *    }
++   *    else if (pid == 0) {
++   *      execve("/bin/cat", argp, envp);
++   *    }
++   *
++   * The parent sends a signal immediately after forking.
++   * Since the child may not have called `execve()` yet,
++   * there is no telling what process receives the signal,
++   * our fork or /bin/cat.
++   *
++   * To avoid ambiguity, we create a pipe with both ends
++   * marked close-on-exec. Then, after the call to `fork()`,
++   * the parent polls the read end until it EOFs or errors with EPIPE.
++   */
++  err = uv__make_pipe(signal_pipe, 0);
++  if (err)
++    return err;
++
++  /* Acquire write lock to prevent opening new fds in worker threads */
++  uv_rwlock_wrlock(&loop->cloexec_lock);
++
++  err = uv__spawn_and_init_child_fork(options, stdio_count, pipes, signal_pipe[1], pid);
++
++  /* Release lock in parent process */
++  uv_rwlock_wrunlock(&loop->cloexec_lock);
++
++  uv__close(signal_pipe[1]);
++
++  if (err == 0) {
++    do
++      r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno));
++    while (r == -1 && errno == EINTR);
++
++    if (r == 0)
++      ; /* okay, EOF */
++    else if (r == sizeof(exec_errorno)) {
++      do
++        err = waitpid(*pid, &status, 0); /* okay, read errorno */
++      while (err == -1 && errno == EINTR);
++      assert(err == *pid);
++      err = exec_errorno;
++    } else if (r == -1 && errno == EPIPE) {
++      /* Something unknown happened to our child before spawn */
++      do
++        err = waitpid(*pid, &status, 0); /* okay, got EPIPE */
++      while (err == -1 && errno == EINTR);
++      assert(err == *pid);
++      err = UV_EPIPE;
++    } else
++      abort();
++  }
++
++  uv__close_nocheckstdio(signal_pipe[0]);
++
++  return err;
++}
++
+ int uv_spawn(uv_loop_t* loop,
+              uv_process_t* process,
+              const uv_process_options_t* options) {
+@@ -355,18 +920,13 @@ int uv_spawn(uv_loop_t* loop,
+   /* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
+   return UV_ENOSYS;
+ #else
+-  sigset_t signewset;
+-  sigset_t sigoldset;
+-  int signal_pipe[2] = { -1, -1 };
+   int pipes_storage[8][2];
+   int (*pipes)[2];
+   int stdio_count;
+-  ssize_t r;
+   pid_t pid;
+   int err;
+   int exec_errorno;
+   int i;
+-  int status;
+ 
+   assert(options->file != NULL);
+   assert(!(options->flags & ~(UV_PROCESS_DETACHED |
+@@ -379,6 +939,7 @@ int uv_spawn(uv_loop_t* loop,
+ 
+   uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS);
+   QUEUE_INIT(&process->queue);
++  process->status = 0;
+ 
+   stdio_count = options->stdio_count;
+   if (stdio_count < 3)
+@@ -403,106 +964,24 @@ int uv_spawn(uv_loop_t* loop,
+       goto error;
+   }
+ 
+-  /* This pipe is used by the parent to wait until
+-   * the child has called `execve()`. We need this
+-   * to avoid the following race condition:
+-   *
+-   *    if ((pid = fork()) > 0) {
+-   *      kill(pid, SIGTERM);
+-   *    }
+-   *    else if (pid == 0) {
+-   *      execve("/bin/cat", argp, envp);
+-   *    }
+-   *
+-   * The parent sends a signal immediately after forking.
+-   * Since the child may not have called `execve()` yet,
+-   * there is no telling what process receives the signal,
+-   * our fork or /bin/cat.
+-   *
+-   * To avoid ambiguity, we create a pipe with both ends
+-   * marked close-on-exec. Then, after the call to `fork()`,
+-   * the parent polls the read end until it EOFs or errors with EPIPE.
+-   */
+-  err = uv__make_pipe(signal_pipe, 0);
+-  if (err)
+-    goto error;
+-
+ #if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
+   uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
+ #endif
+ 
+-  /* Acquire write lock to prevent opening new fds in worker threads */
+-  uv_rwlock_wrlock(&loop->cloexec_lock);
+-
+-  /* Start the child with most signals blocked, to avoid any issues before we
+-   * can reset them, but allow program failures to exit (and not hang). */
+-  sigfillset(&signewset);
+-  sigdelset(&signewset, SIGKILL);
+-  sigdelset(&signewset, SIGSTOP);
+-  sigdelset(&signewset, SIGTRAP);
+-  sigdelset(&signewset, SIGSEGV);
+-  sigdelset(&signewset, SIGBUS);
+-  sigdelset(&signewset, SIGILL);
+-  sigdelset(&signewset, SIGSYS);
+-  sigdelset(&signewset, SIGABRT);
+-  if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
+-    abort();
+-
+-  pid = fork();
+-  if (pid == -1)
+-    err = UV__ERR(errno);
+-
+-  if (pid == 0)
+-    uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
++  /* Spawn the child */
++  exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, &pid);
+ 
+-  if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
+-    abort();
+-
+-  /* Release lock in parent process */
+-  uv_rwlock_wrunlock(&loop->cloexec_lock);
+-
+-  uv__close(signal_pipe[1]);
+-
+-  if (pid == -1) {
+-    uv__close(signal_pipe[0]);
+-    goto error;
+-  }
+-
+-  process->status = 0;
+-  exec_errorno = 0;
+-  do
+-    r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno));
+-  while (r == -1 && errno == EINTR);
+-
+-  if (r == 0)
+-    ; /* okay, EOF */
+-  else if (r == sizeof(exec_errorno)) {
+-    do
+-      err = waitpid(pid, &status, 0); /* okay, read errorno */
+-    while (err == -1 && errno == EINTR);
+-    assert(err == pid);
+-  } else if (r == -1 && errno == EPIPE) {
+-    do
+-      err = waitpid(pid, &status, 0); /* okay, got EPIPE */
+-    while (err == -1 && errno == EINTR);
+-    assert(err == pid);
+-  } else
+-    abort();
+-
+-  uv__close_nocheckstdio(signal_pipe[0]);
+-
+-  for (i = 0; i < options->stdio_count; i++) {
+-    err = uv__process_open_stream(options->stdio + i, pipes[i]);
+-    if (err == 0)
+-      continue;
+-
+-    while (i--)
+-      uv__process_close_stream(options->stdio + i);
+-
+-    goto error;
+-  }
++#if 0
++  /* This runs into a nodejs issue (it expects initialized streams, even if the
++   * exec failed).
++   * See https://github.com/libuv/libuv/pull/3107#issuecomment-782482608 */
++  if (exec_errorno != 0)
++      goto error;
++#endif
+ 
+-  /* Only activate this handle if exec() happened successfully */
++  /* Activate this handle if exec() happened successfully, even if we later
++   * fail to open a stdio handle. This ensures we can eventually reap the child
++   * with waitpid. */
+   if (exec_errorno == 0) {
+ #if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+     struct kevent event;
+@@ -515,12 +994,22 @@ int uv_spawn(uv_loop_t* loop,
+     }
+ #endif
+ 
++    process->pid = pid;
++    process->exit_cb = options->exit_cb;
+     QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);
+     uv__handle_start(process);
+   }
+ 
+-  process->pid = pid;
+-  process->exit_cb = options->exit_cb;
++  for (i = 0; i < options->stdio_count; i++) {
++    err = uv__process_open_stream(options->stdio + i, pipes[i]);
++    if (err == 0)
++      continue;
++
++    while (i--)
++      uv__process_close_stream(options->stdio + i);
++
++    goto error;
++  }
+ 
+   if (pipes != pipes_storage)
+     uv__free(pipes);
diff --git a/patches/node/unix_protect_fork_in_uv_spawn_from_signals.patch b/patches/node/unix_protect_fork_in_uv_spawn_from_signals.patch
new file mode 100644
index 0000000000000..e8f06d17973e6
--- /dev/null
+++ b/patches/node/unix_protect_fork_in_uv_spawn_from_signals.patch
@@ -0,0 +1,173 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Thu, 29 Jul 2021 12:09:51 -0400
+Subject: unix: protect fork in uv_spawn from signals
+
+Years ago, we found that various kernels (linux, macOS) were known to
+fail if they try to deliver a signal during this syscall, so we prevent
+that from happening. They may have fixed those issues, but it is
+generally just a bad time for signals to arrive (glibc blocks them here,
+for example, including some more internal ones that it won't let us
+touch here).
+
+We try to be a bit conservative, and leave many signals unblocked which
+could happen during normal execution and should terminate the process if
+they do. There is a small race window after the child starts before we
+clear the old handlers, if the user was to send an fake signal from
+elsewhere, but that should be quite unlikely.
+
+PR-URL: https://github.com/libuv/libuv/pull/3251
+Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
+
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index cfcba341e0e380ecd595e4b59e39c08a7b374a48..c1f6bd4b0076f0835caf83c45a6a896e7ae5def9 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -26,6 +26,7 @@
+ #include <stdlib.h>
+ #include <assert.h>
+ #include <errno.h>
++#include <signal.h>
+ 
+ #include <sys/types.h>
+ #include <sys/wait.h>
+@@ -223,13 +224,32 @@ static void uv__process_child_init(const uv_process_options_t* options,
+                                    int stdio_count,
+                                    int (*pipes)[2],
+                                    int error_fd) {
+-  sigset_t set;
++  sigset_t signewset;
+   int close_fd;
+   int use_fd;
+-  int err;
+   int fd;
+   int n;
+ 
++  /* Reset signal disposition first. Use a hard-coded limit because NSIG is not
++   * fixed on Linux: it's either 32, 34 or 64, depending on whether RT signals
++   * are enabled. We are not allowed to touch RT signal handlers, glibc uses
++   * them internally.
++   */
++  for (n = 1; n < 32; n += 1) {
++    if (n == SIGKILL || n == SIGSTOP)
++      continue;  /* Can't be changed. */
++
++#if defined(__HAIKU__)
++    if (n == SIGKILLTHR)
++      continue;  /* Can't be changed. */
++#endif
++
++    if (SIG_ERR != signal(n, SIG_DFL))
++      continue;
++
++    uv__write_errno(error_fd);
++  }
++
+   if (options->flags & UV_PROCESS_DETACHED)
+     setsid();
+ 
+@@ -311,32 +331,10 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     environ = options->env;
+   }
+ 
+-  /* Reset signal disposition.  Use a hard-coded limit because NSIG
+-   * is not fixed on Linux: it's either 32, 34 or 64, depending on
+-   * whether RT signals are enabled.  We are not allowed to touch
+-   * RT signal handlers, glibc uses them internally.
+-   */
+-  for (n = 1; n < 32; n += 1) {
+-    if (n == SIGKILL || n == SIGSTOP)
+-      continue;  /* Can't be changed. */
+-
+-#if defined(__HAIKU__)
+-    if (n == SIGKILLTHR)
+-      continue;  /* Can't be changed. */
+-#endif
+-
+-    if (SIG_ERR != signal(n, SIG_DFL))
+-      continue;
+-
+-    uv__write_errno(error_fd);
+-  }
+-
+-  /* Reset signal mask. */
+-  sigemptyset(&set);
+-  err = pthread_sigmask(SIG_SETMASK, &set, NULL);
+-
+-  if (err != 0)
+-    uv__write_errno(error_fd);
++  /* Reset signal mask just before exec. */
++  sigemptyset(&signewset);
++  if (sigprocmask(SIG_SETMASK, &signewset, NULL) != 0)
++    abort();
+ 
+ #ifdef __MVS__
+   execvpe(options->file, options->args, environ);
+@@ -345,6 +343,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
+ #endif
+ 
+   uv__write_errno(error_fd);
++  abort();
+ }
+ #endif
+ 
+@@ -356,6 +355,8 @@ int uv_spawn(uv_loop_t* loop,
+   /* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
+   return UV_ENOSYS;
+ #else
++  sigset_t signewset;
++  sigset_t sigoldset;
+   int signal_pipe[2] = { -1, -1 };
+   int pipes_storage[8][2];
+   int (*pipes)[2];
+@@ -432,25 +433,41 @@ int uv_spawn(uv_loop_t* loop,
+ 
+   /* Acquire write lock to prevent opening new fds in worker threads */
+   uv_rwlock_wrlock(&loop->cloexec_lock);
+-  pid = fork();
+ 
+-  if (pid == -1) {
++  /* Start the child with most signals blocked, to avoid any issues before we
++   * can reset them, but allow program failures to exit (and not hang). */
++  sigfillset(&signewset);
++  sigdelset(&signewset, SIGKILL);
++  sigdelset(&signewset, SIGSTOP);
++  sigdelset(&signewset, SIGTRAP);
++  sigdelset(&signewset, SIGSEGV);
++  sigdelset(&signewset, SIGBUS);
++  sigdelset(&signewset, SIGILL);
++  sigdelset(&signewset, SIGSYS);
++  sigdelset(&signewset, SIGABRT);
++  if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
++    abort();
++
++  pid = fork();
++  if (pid == -1)
+     err = UV__ERR(errno);
+-    uv_rwlock_wrunlock(&loop->cloexec_lock);
+-    uv__close(signal_pipe[0]);
+-    uv__close(signal_pipe[1]);
+-    goto error;
+-  }
+ 
+-  if (pid == 0) {
++  if (pid == 0)
+     uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
++
++  if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
+     abort();
+-  }
+ 
+   /* Release lock in parent process */
+   uv_rwlock_wrunlock(&loop->cloexec_lock);
++
+   uv__close(signal_pipe[1]);
+ 
++  if (pid == -1) {
++    uv__close(signal_pipe[0]);
++    goto error;
++  }
++
+   process->status = 0;
+   exec_errorno = 0;
+   do
diff --git a/patches/node/unix_remove_uv_cloexec_ioctl_3515.patch b/patches/node/unix_remove_uv_cloexec_ioctl_3515.patch
new file mode 100644
index 0000000000000..f49d25998bba2
--- /dev/null
+++ b/patches/node/unix_remove_uv_cloexec_ioctl_3515.patch
@@ -0,0 +1,91 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jameson Nash <vtjnash@gmail.com>
+Date: Sun, 6 Mar 2022 15:01:33 -0500
+Subject: unix: remove uv__cloexec_ioctl() (#3515)
+
+Now that uv__cloexec_fcntl() is simplified
+(https://github.com/libuv/libuv/pull/3492), there is no benefit to
+maintaining duplicate code paths for the same thing.
+
+diff --git a/deps/uv/src/unix/core.c b/deps/uv/src/unix/core.c
+index 6cd519ad5b3e7af8f5c71b18a59b88458a233f15..8c30802ad15316a8ec20ecedfb5123174d74276b 100644
+--- a/deps/uv/src/unix/core.c
++++ b/deps/uv/src/unix/core.c
+@@ -597,20 +597,6 @@ int uv__nonblock_ioctl(int fd, int set) {
+ 
+   return 0;
+ }
+-
+-
+-int uv__cloexec_ioctl(int fd, int set) {
+-  int r;
+-
+-  do
+-    r = ioctl(fd, set ? FIOCLEX : FIONCLEX);
+-  while (r == -1 && errno == EINTR);
+-
+-  if (r)
+-    return UV__ERR(errno);
+-
+-  return 0;
+-}
+ #endif
+ 
+ 
+@@ -645,7 +631,7 @@ int uv__nonblock_fcntl(int fd, int set) {
+ }
+ 
+ 
+-int uv__cloexec_fcntl(int fd, int set) {
++int uv__cloexec(int fd, int set) {
+   int flags;
+   int r;
+ 
+diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
+index 2dcc8b32f5165dd75061a1b55cc1abd2ab93ccc9..543993989b8765247e140e9d950a25192e5e1ca5 100644
+--- a/deps/uv/src/unix/internal.h
++++ b/deps/uv/src/unix/internal.h
+@@ -175,11 +175,9 @@ struct uv__stream_queued_fds_s {
+     defined(__linux__) || \
+     defined(__OpenBSD__) || \
+     defined(__NetBSD__)
+-#define uv__cloexec uv__cloexec_ioctl
+ #define uv__nonblock uv__nonblock_ioctl
+ #define UV__NONBLOCK_IS_IOCTL 1
+ #else
+-#define uv__cloexec uv__cloexec_fcntl
+ #define uv__nonblock uv__nonblock_fcntl
+ #define UV__NONBLOCK_IS_IOCTL 0
+ #endif
+@@ -197,8 +195,7 @@ struct uv__stream_queued_fds_s {
+ #endif
+ 
+ /* core */
+-int uv__cloexec_ioctl(int fd, int set);
+-int uv__cloexec_fcntl(int fd, int set);
++int uv__cloexec(int fd, int set);
+ int uv__nonblock_ioctl(int fd, int set);
+ int uv__nonblock_fcntl(int fd, int set);
+ int uv__close(int fd); /* preserves errno */
+diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
+index 7705068730cb0536998bad7d304cb87df99b72e8..b6f9756c6a6710f5f10762b9299cc35047b98097 100644
+--- a/deps/uv/src/unix/process.c
++++ b/deps/uv/src/unix/process.c
+@@ -283,7 +283,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
+     if (pipes[fd][1] == -1)
+       uv__write_errno(error_fd);
+ #ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
+-    n = uv__cloexec_fcntl(pipes[fd][1], 1);
++    n = uv__cloexec(pipes[fd][1], 1);
+     if (n) {
+       uv__write_int(error_fd, n);
+       _exit(127);
+@@ -312,7 +312,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
+ 
+     if (fd == use_fd) {
+       if (close_fd == -1) {
+-        n = uv__cloexec_fcntl(use_fd, 0);
++        n = uv__cloexec(use_fd, 0);
+         if (n) {
+           uv__write_int(error_fd, n);
+           _exit(127);
diff --git a/patches/node/unix_simplify_uv_cloexec_fcntl_3492.patch b/patches/node/unix_simplify_uv_cloexec_fcntl_3492.patch
new file mode 100644
index 0000000000000..219ae54daa15b
--- /dev/null
+++ b/patches/node/unix_simplify_uv_cloexec_fcntl_3492.patch
@@ -0,0 +1,36 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Ben Noordhuis <info@bnoordhuis.nl>
+Date: Sat, 5 Mar 2022 18:55:49 +0100
+Subject: unix: simplify uv__cloexec_fcntl() (#3492)
+
+FD_CLOEXEC is the only defined flag for fcntl(F_SETFD) so don't bother
+getting the status of that flag first with fcntl(F_GETFD), just set it.
+
+diff --git a/deps/uv/src/unix/core.c b/deps/uv/src/unix/core.c
+index a6425294086ff2f1435fdd6866380d3aaaf68200..6cd519ad5b3e7af8f5c71b18a59b88458a233f15 100644
+--- a/deps/uv/src/unix/core.c
++++ b/deps/uv/src/unix/core.c
+@@ -649,21 +649,9 @@ int uv__cloexec_fcntl(int fd, int set) {
+   int flags;
+   int r;
+ 
+-  do
+-    r = fcntl(fd, F_GETFD);
+-  while (r == -1 && errno == EINTR);
+-
+-  if (r == -1)
+-    return UV__ERR(errno);
+-
+-  /* Bail out now if already set/clear. */
+-  if (!!(r & FD_CLOEXEC) == !!set)
+-    return 0;
+-
++  flags = 0;
+   if (set)
+-    flags = r | FD_CLOEXEC;
+-  else
+-    flags = r & ~FD_CLOEXEC;
++    flags = FD_CLOEXEC;
+ 
+   do
+     r = fcntl(fd, F_SETFD, flags);


Test patch: