Skip to content

seg: poison a closed file's mmap - #23161

Draft
AskAlexSharov wants to merge 3 commits into
mainfrom
alex/mmap_poison_37
Draft

seg: poison a closed file's mmap#23161
AskAlexSharov wants to merge 3 commits into
mainfrom
alex/mmap_poison_37

Conversation

@AskAlexSharov

Copy link
Copy Markdown
Collaborator

A slice borrowed from a .kv mmap that outlives its file has two possible endings, and the harmless-looking one is the dangerous one. If the address stays vacant, the next read faults and the process dies. If a later file gets mapped over it, the read returns that file's bytes and nothing complains — which is how a file-lifetime bug surfaces as an eth_getProof diff instead of a crash.

ERIGON_MMAP_POISON=true makes Decompressor.Close mprotect(PROT_NONE) the range and keep it, so the address can never be handed to another file and every stale read faults at the read that misuses it. The range leaks for the life of the process — diagnostic switch, not a default.

Demonstrated on Linux 6.8, showing the two endings of the same stale borrow:

read after munmap                  -> SIGSEGV code=0x1 (SEGV_MAPERR)   crash
unmap, another file at same addr   -> exit 0, borrowed slice reads the other file's bytes
mprotect(PROT_NONE), then read     -> SIGSEGV code=0x2 (SEGV_ACCERR)   deterministic

Tests: a child process proves a poisoned range faults; a second test maps 32 further files and asserts none is given the poisoned address; a /proc/self/maps test pins that the flag actually reaches Decompressor.Close (off → range released, on → retained).

The fault arrives as SIGSEGV/SEGV_ACCERR on Linux and SIGBUS on Darwin, so the test pins the code only where it runs in production.

Scope: seg/.kv mappings only. MDBX owns its own mapping and has a separate borrow contract (GetOne passes the mmap view straight through, valid only while the read txn lives), which poisoning cannot reach.

Motivation: TrieContext.Branch clones every returned slice — ~631 MB/s of alloc traffic at tip per #21524, which removed it and was reverted by #21630 after widespread RPC proof/trace corruption with no root cause identified. This makes a second attempt testable instead of a gamble.

…ing it

A slice borrowed from a .kv mmap that outlives its file has two possible
endings, and the harmless-looking one is the dangerous one. If the address
stays vacant the next read faults and the process dies. If a later file is
mapped over it the read returns that file's bytes and nothing complains --
which is how a lifetime bug surfaces as an eth_getProof diff rather than a
crash.

MMAP_POISON=true makes Decompressor.Close mprotect(PROT_NONE) the range and
keep it, so the address can never be handed to another file and every stale
read faults at the read that misuses it. The range leaks for the life of the
process, so this is a diagnostic switch, not a default.

The fault arrives as SIGSEGV/SEGV_ACCERR on Linux and SIGBUS on Darwin, so
the test pins the code only where it runs in production.
Removes the useless defer (os.Exit skips it) and routes the unlink through
common/dir, which is what the ruleguard asks for. Unlinking while mapped also
matches what a merge does to a superseded file.
@AskAlexSharov AskAlexSharov changed the title common/mmap, db/seg: poison a closed file's mapping instead of unmapping it seg: poison a closed files' mmap Aug 11, 2026
@AskAlexSharov AskAlexSharov changed the title seg: poison a closed files' mmap seg: poison a closed file's mmap Aug 11, 2026
The wiring test checked /proc/self/maps for the address the mapping used to
occupy. A released address is reused almost immediately -- db/seg's own tests
mmap heavily -- so an unrelated mapping landing there would read as 'the
poison kept it', failing the unpoisoned case for the wrong reason. Match on
the file instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant