sea: keep ELF segments on separate pages in --build-sea output - #65564
Conversation
When node itself is not position independent (the official Linux
binaries, and any build with a toolchain that does not default to PIE),
LIEF made room for the extra program header by moving the header table
into the largest gap between two PT_LOAD segments and extending the
earlier segment across that gap. Whenever the gap it picked was the one
between the read-only data and the read-write segment, whose boundary
is not page aligned, the extended segment ended inside the first page
of the next one. Linux 4.17 to 5.3, and RHEL 8's 4.18 kernel, map an
executable's segments with MAP_FIXED_NOREPLACE and refuse the second
mapping, so the single executable was killed with SIGSEGV before it ran
a single instruction ('elf segment at ... requested but the memory is
mapped already' in the kernel log). Which gap is largest depends on
section sizes, so roughly one build in three produced such binaries.
Ask LIEF to place the table after .bss for non-PIE executables instead,
which leaves every existing segment as the linker laid it out; the
output grows by the size of .bss. A test checks that no two PT_LOAD
segments of a --build-sea executable share a page.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
|
Review requested:
|
|
Fast-track has been requested by @panva. Please 👍 to approve. |
This comment has been minimized.
This comment has been minimized.
|
That's not ideal - merge conflict on the Attempting a test in the stress job with 100 iterations: rhel8-x64 and rhel9-x64 as we really need the results on rhel8-x64 ASAP. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65564 +/- ##
=======================================
Coverage 90.05% 90.06%
=======================================
Files 751 751
Lines 254420 254423 +3
Branches 47975 47986 +11
=======================================
+ Hits 229121 229148 +27
+ Misses 16483 16444 -39
- Partials 8816 8831 +15
🚀 New features to boost your workflow:
|
sxa
left a comment
There was a problem hiding this comment.
Annoying (as usual...) but https://ci.nodejs.org/job/node-stress-single-test/nodes=rhel8-x64/848/console which was without this patch seemed to be passing too. But on the basis this has been tested in an environment where it was reproducible I'm approving.
Out of interest, did you manage to come to any conclusion abot what change could have caused this to start going wrong recently given that it doesn't seem to have been package updates?
This comment has been minimized.
This comment has been minimized.
This comment was marked as resolved.
This comment was marked as resolved.
|
Every so often we get a problem with merge/rebase conflicts and we don't have a permanent fix for it at the moment. This one seems to have triggered it. We've had 3 PR test runs so far and I'm collating these links for my own benefit and because I'm going to try and disable one machine to let this through:
A few notes:
Hopefully that new run will get close to a pass although I'll note that between the second and third runs we do have passes on both rhel8-s390x and rhel9-s390x. Similarly for AIX we have passes on both aix72-power9 and aix73-power9](https://ci.nodejs.org/job/node-test-commit-aix/64731/) I'll keep the problematic rhel9/x64 machine offline and perhaps see if I can analyse the workspace on the machine to try and replicate the problem manually. Mentioning nodejs/build#4345 which was a similar situation seen in the past. |
|
Landed in a2bbe4e |
The
sea/*failures on the rhel8-x64 CI hosts (SIGSEGV, empty stderr, https://github.com/nodejs/reliability/blob/main/reports/2026-08-26.md) come with a kernel line,elf segment at ... requested but the memory is mapped already(nodejs/build#4433 (comment)), which isexecverefusing the injected executable. Whennodeis not PIE (RHEL toolchains, and the official Linux binaries), LIEF makes room for the new program header by moving the header table into the largest gap between twoPT_LOADsegments and extending the earlier segment across it; when that gap is the one between the read-only data and the read-write segment, whose boundary is not page aligned, the extended segment ends inside the next segment's first page. Linux 4.17 to 5.3 and RHEL 8's 4.18 map an executable's segments withMAP_FIXED_NOREPLACE, so the second mapping fails past the point of no return and the process is killed; newer kernels don't check. Which gap is largest depends on section sizes, so it comes and goes from build to build.This asks LIEF for its after-
.bssplacement when the executable isET_EXEC, which leaves every existing segment where the linker put it and gives the header table pages of its own; the output grows by the size of.bss(about 340 KB fornode). PIE builds are unchanged. postject makes the same choice in its own copy of LIEF, sotest-single-executable-application.jsandtest_sea_addon, which still inject with it, aren't covered by this.Tests:
test/sea/test-build-sea-elf-segments.jsasserts no twoPT_LOADsegments of a--build-seaexecutable share a page;test/seapassesnodebuilt with RHEL 8's clang whose read-only/read-write gap is the largest: on AlmaLinux 8.10 (4.18.0-553.150.1.el8_10) the SEA built before this change segfaults with the same kernel line and the one built after it runs; both run on a 6.12 kernelRefs: nodejs/build#4433
Disclosure: the code, test, investigation and this description were written by Claude Code, directed and reviewed by @codebytere.