MarshallOfSound

#53491: fix: crash when the environment is written while another thread reads it

Merged
Created: Sep 3, 2026, 7:16:44 PM
Merged: Sep 4, 2026, 12:49:47 AM
10 comments
Target: main

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.cc defines setenv, unsetenv, putenv and clearenv in the executable. Linux executables link with -rdynamic, so shared libraries (GTK, native addons) bind to these too.
  • A published environ array 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=value strings 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 unsetenv and clearenv bump a private counter that getenv uses to retry, which the replacements can't do.
  • New Linux spec: a worker thread reads the environment through os.homedir() (libuv calls getenv() 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 the gin_helper::CallbackHolderBase objects 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 test passes
  • 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

42-x-y
Merged
PR Number
#53511
Merged At
Sep 4, 2026, 2:22:21 AM
Released In
Not yet
Release Date
Not yet
43-x-y
Merged
PR Number
#53510
Merged At
Sep 4, 2026, 2:27:18 AM
Released In
Not yet
Release Date
Not yet
44-x-y
Merged
PR Number
#53509
Merged At
Sep 4, 2026, 2:32:52 AM
Released In
Not yet
Release Date
Not yet
45-x-y
Merged
PR Number
#53508
Merged At
Sep 4, 2026, 2:26:36 AM
Released In
Not yet
Release Date
Not yet

Semver Impact

Major
Breaking changes
Minor
New features
Patch
Bug fixes
None
Docs, tests, etc.

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