Native Linux shutdown investigation (#2973)
Status: root cause unconfirmed; native fix and Ubuntu qualification remain open. The PR adds opt-in evidence capture and now updates Electron for a known Linux environment race. This is not a confirmed fix for the original SIGSEGV. Keep #2973 open pending original native crash evidence, causal attribution and source-bound native qualification.
Retained evidence
Historical native Ubuntu x64 smoke runs intermittently exited with SIGSEGV after
readiness and ordered shutdown through final-quit. The corrected smoke harness
preserved the primary signal instead of replacing it with cleanup ENOTEMPTY.
Passing attempts did not invalidate the failure. Original logs, frozen artifact
identifiers and operator-session details remain in the issue and PR review record.
The kernel report and conditional symbolization against an installed libc suggested
getenv, but supplied no original caller stack or core. That is a diagnostic lead,
not causal proof. final-quit records entry to app.quit(); it does not prove
native destruction completed or the process exited zero.
Code inspection and local validation
DesktopCredentialService.dispose() closes admission, clears the active
credential binding, aborts operations, then waits for admitted work and queued
profile mutations. ProfileStore.close() closes admission and waits for its
mutation queue. The shutdown coordinator bounds these drains, disposes session
hooks and IPC, destroys the window, and calls app.quit(). The safeStorage
encrypt/decrypt adapters are synchronous Electron calls. These observations do
not rule out outstanding native Chromium work or a native library lifetime bug.
These observations alone did not justify changing this order or inserting sleeps.
The later upstream environment-race evidence below supports a runtime update,
but does not establish that race as the cause of the original shutdown crash.
Unit coverage exercises capture authorization, private external retention, unchanged smoke launch contracts and ordered shutdown. It does not reproduce the native crash or qualify a platform. Historical local failures and reruns remain in the review record and are not replaced by later passing results.
Runtime follow-up
Electron 44.3.0
includes 44.x backport #53509
of upstream #53491.
The Linux executable interposes environment writers to keep published arrays
alive on glibc before 2.41, preventing concurrent native getenv reads from
accessing a freed array. On glibc 2.41+ it delegates to libc. This addresses a
known race relevant to the new lead; it does not establish an original caller
or fix macOS's libc environment race.
The desktop minimum is now ^44.7.0, locked to 44.7.0, the latest stable
44.x release at investigation time. This keeps the existing major and includes
the fix plus subsequent maintenance updates. The
44.x support schedule lists end of
life as 2027-03-02. The runtime update retains the existing sandbox/fuse configuration and private
capture protections.
Regression provenance and remaining acceptance
Use the official environ-write-race.js fixture,
unchanged, from the 44.7.0 source commit
61f55c4ce540416b3e43f75f6c0f125d22e5e908.
Its SHA-256 is
81fd87d0ecfc16fe0b484bef8d56265406011a0908c1241d5c22d1c7ada3a976.
This exercises real native reads via a worker's os.homedir() while the main
thread adds 2,000 environment variables. It is not a JS mock of the fix.
The upstream backport's spec/api-process-spec.ts requires all three:
stdout trimmed to exactly done, code 0, and signal null, after worker exit.
Exit 0 alone is insufficient because a worker timeout also reaches app.exit(0).
On gram, run those same fixture bytes with the official 44.0.0 and 44.7.0 Linux
binaries in the same qualified unprivileged session; verify the upstream binary
checksums and record binary/fixture hashes and libc build ID. A downloaded copy
inside this repository must use .cjs (identical contents) to avoid the root's
ES-module package scope. Launch as "$electron_binary" "$fixture_path" without
sandbox bypasses, ELECTRON_RUN_AS_NODE, or injected libraries. Predeclare the
attempt count, retain every exit/signal/stdout/stderr and any external timeout,
and apply the upstream assertion to each attempt. The operator's independent
before/after run is pending; no result is inferred here.
For the final candidate source, repeat the locked install, typecheck, full desktop suite, native package/fuse inspection, complete unmodified four-launch smoke with capture and a separately declared ordinary run. Retain source-bound packaged acceptance and native platform gates in the desktop README. Bind every result to the rebuilt source revision, lockfile, package hashes, architecture and actual runtime versions. Prior-source acceptance cannot qualify the final head. Linux evidence does not qualify macOS. Finite passing runs cannot establish that the original shutdown SIGSEGV is fixed or close #2973.
Capture on the qualified host
Build the changed application using the existing packaging procedure and record its source/package hashes separately from the frozen failing artifact. Preserve the original sanitized logs and frozen package. Use the existing unprivileged synthetic-fixture smoke procedure, display, isolated profiles and keyring session. Do not use real credentials or change host libraries, permissions, sandboxing, fuses, GPU flags, or timeouts for diagnosis.
From apps/desktop, in that already-qualified session:
crash_evidence_dir=$(mktemp -d /tmp/propr-native-shutdown-evidence-XXXXXXXX)
PROPR_DESKTOP_NATIVE_CRASH_DIRECTORY="$crash_evidence_dir" npm run smoke:package
Keep the command's actual exit status. The launcher adds only
--propr-native-crash-directory=<directory> to each existing smoke mode. The
application accepts it only after the existing packaged smoke authorization
succeeds, on Linux, with an existing owned 0700 canonical directory outside
the profile tree. The launcher checks this contract before spawning; the application revalidates it
at use time. Invalid capture configuration fails the launch.
Before ready, Electron's crash reporter starts with uploadToServer: false,
no submission URL, and ignoreSystemCrashHandler: false. Each process gets a
new private launch-* directory containing a runtime version/PID manifest and
a dumps directory. A fixed propr_shutdown_phase annotation tracks shutdown
through will-quit. No credential, environment, URL, profile data, or arbitrary
error is added as an annotation. Electron documents this local storage and
startup behavior in its crashReporter API.
Crashpad may include memory in a dump; treat all raw evidence as private even with synthetic fixtures. Never post dumps or raw debugger output to GitHub. The evidence root is operator-owned and deliberately survives harness-owned profile cleanup. Retain it privately until diagnosis is complete, then remove only that root. Do not feed dumps into public log/artifact publication. Capture does not suppress signals, retry launches, change success assertions, or bypass the existing scan for plaintext credentials under userData.
On failure, retain every launch directory and the full sanitized harness log.
Find minidumps recursively under dumps; Crashpad's internal layout is not an
API. Absence of a dump does not negate a SIGSEGV, particularly late in process
teardown. Use an already-available system core if local default permissions
allow access; do not change ptrace/core settings or request elevated access.
Required diagnosis and regression evidence
- Record
/etc/os-release, kernel, architecture, the manifest's Electron, Chromium and embedded Node versions, package/fuse checks and hashes, plus exact installed versions and ELF build IDs for libc, libsecret, GLib/GIO and every module appearing in the crash stack. Build-time Node is separate from Electron's embedded Node. - Symbolicate offline using matching Electron symbols and exact module build IDs, not symbols from a newer package. Retain the faulting thread, instruction, registers, module mappings/load biases and all-thread backtrace. If symbols or an artifact are missing, record that explicitly. Use existing unprivileged tools; no library replacement, system setting changes or sandbox bypass.
- Attribute the crash using the stack and ownership/lifetime evidence. Check whether the fault occurs in Chromium thread-pool teardown, credential-library work, or an app-triggered native operation. A last JS phase alone cannot decide this. Add a targeted failing-before/passing-after regression only once a mechanism is established; preserve the original failure as evidence.
- Run the full unmodified assertion suite against the rebuilt fix, including
release guard/normal quit, transport success, repeated quit and forced timeout,
credential protection/auth assertions and cleanup. Every observed signal or
nonzero exit remains a failure even after
final-quit. Predeclare any repeat count for intermittent reproduction and retain all outcomes; do not retry until green. Also run without capture because instrumentation changes timing. - Report all attempts, including non-reproduction and unresolved uncertainty. A successful host run qualifies that configuration only; it does not establish whole-platform support or erase the previously observed failure.
The PR2967 cleanup/diagnostic implementation, setup visual coverage, macOS setup and nightly fixture timeout work are outside this change.