#53491: fix: crash when the environment is written while another thread reads it
Note: This PR also fixes a leak that the new spec exposed on the ASan shards. Native function callbacks set up in a Node.js worker were never freed when the worker exited, so the second commit frees them from an environment cleanup hook.
Description of Change
Before glibc 2.41, adding an environment variable reallocates environ and frees the old array. A getenv() on another thread can then read freed memory. This is the startup crash the Linux ASan shards keep hitting in fontconfig's getenv(): the fontconfig worker scans fonts while the main script adds variables.
shell/app/environ_linux.ccdefinessetenv,unsetenv,putenvandclearenvin the executable. Linux executables link with-rdynamic, so shared libraries (GTK, native addons) bind to these too.- A published
environarray is never freed. Growing it doubles the capacity, copies the entries and publishes the new array. This is the scheme glibc 2.41 adopted upstream. NAME=valuestrings are never freed and identical ones are reused, as in every glibc version.- Replaced arrays stay on a list so LeakSanitizer does not report them.
- MSan builds keep glibc's functions.
- On glibc 2.41 and later all four functions forward to glibc's own. Those are already safe, and their
unsetenvandclearenvbump a private counter thatgetenvuses to retry, which the replacements can't do. - New Linux spec: a worker thread reads the environment through
os.homedir()(libuv callsgetenv()with no lock) while the main script adds 2000 variables.
Results on glibc 2.35, the version CI uses:
| Build | Without this change | With this change |
|---|---|---|
| Testing, race repro | 30 of 30 crashed in getenv() |
30 of 30 passed |
| ASan, race repro | 5 of 5 crashed in getenv() |
10 of 10 passed |
| New spec, Testing | 5 of 5 failed with SIGSEGV | 5 of 5 passed |
The extra memory is bounded by the peak variable count, not by the number of writes. Live heap, starting from 40 variables:
| Pattern | glibc | This change |
|---|---|---|
| Add 3 variables | 447 B | 559 B |
| Add 1000 variables | 39 KB | 61 KB |
| Toggle one variable 100000 times | 403 B | 531 B |
| Set one variable to 100000 different values | 3.19 MB | 3.19 MB |
macOS's libc has the same class of bug and is not covered here.
The new spec also exposed a leak, fixed in the second commit:
- A Node.js worker's isolate has no
gin::PerIsolateData, so thegin_helper::CallbackHolderBaseobjects created for Electron's bindings in every worker were never freed. V8 does not run weak callbacks when it disposes an isolate. - Those holders are now tracked per isolate and freed from a Node.js environment cleanup hook, which runs on the worker's thread before the isolate is disposed.
Checklist
- PR description included
-
npm testpasses - tests are added
Release Notes
Notes: Fixed a crash on Linux when process.env was written while another thread was reading the environment, and a memory leak when a worker thread exits.
Backports
Semver Impact
Semantic Versioning helps users understand the impact of updates:
- Major (X.y.z): Breaking changes that may require code modifications
- Minor (x.Y.z): New features that maintain backward compatibility
- Patch (x.y.Z): Bug fixes that don't change the API
- None: Changes that don't affect using facing parts of Electron