fix(common): bound the LSDA reads that lead nowhere - #335
Open
r0ny123 wants to merge 1 commit into
Open
Conversation
`_fdeLandingPads` checks the call-site table budget before reading an LSDA, and the comment says that budget "gates the read as well as the decode". It does not. The budget is charged where a table's length field is parsed, so an LSDA that fails before that point - an unsupported LPStart application mode, a short buffer, a TType offset that will not read - costs a read of up to MAX_LSDA_BYTES and charges nothing at all. A section naming MAX_RECORDS distinct such pointers reads 12.2 GB over 199,999 reads while spending none of its budget. Charging those reads to the table budget instead is not the fix. read_va is asked for MAX_LSDA_BYTES and returns whatever the section holds, so a real image asks for far more than it decodes - the heaviest cell measured reads 92 MB to decode 26 KB of tables - and 8 MB would be gone within about 128 of the 1,473 reads that cell makes, costing landing pads on an ordinary C++ binary. Only the failures need a bound, and they separate the two cases. The heaviest real image spends 29.75 MB on reads that decode to nothing, over 476 of them; the Rust cells spend none; a hostile section spends all of it. MAX_LSDA_FAILED_READ_BYTES holds that to 256 MB over 4,096 reads, which is 8.6x headroom over the heaviest real image and a 48x reduction on the hostile one. All 447 cells across the built AArch64 ELF, C/C++, Rust, Go, ARM64 Mach-O and malpedia corpora are bit-identical, which is the assertion that this reaches only sections built to exhaust it. The failure has to come before the table's length field to reach the new bound at all; one that fails after it is already charged, which is why the same shape built with a too-long length runs 512 reads rather than 200,000 and needs nothing new.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item 6 of #322.
The diagnosis in the issue is right
_fdeLandingPadschecks the table budget before reading, and the comment there says that budget "gates the read as well as the decode". It does not. The budget is charged where a table's length field is parsed, so an LSDA failing before that point — an unsupported LPStart application mode, a short buffer, a TType offset that will not read — costs a read of up toMAX_LSDA_BYTESand charges nothing.Measured: a section naming
MAX_RECORDSdistinct such pointers performs 199,999 reads handing back 12.2 GB, spending none of its budget.But the fix as described would cost recall
"Charge the read at the point it happens" cannot mean charging it to
MAX_LSDA_TABLE_BYTES.read_vais asked forMAX_LSDA_BYTESand returns whatever the section holds, so a real image asks for vastly more than it decodes:googletest_gcc-x64_O2-staticgoogletest_gcc-arm64_O2-staticfmtheavy_linux-gnu-x64_debugThe 8 MB budget would be gone within about 128 of that first cell's 1,473 reads, and every LSDA after that reads as "declares nothing" — landing pads lost on an ordinary C++ binary. The existing comment's "counts what is decoded rather than what is read" is not a wording slip; it is why the current bound works.
What separates the two cases
Only the reads that lead nowhere. Those are rare on real images and universal on hostile ones:
MAX_LSDA_FAILED_READ_BYTES = 256 MBholds the hostile shape to 4,096 reads and 256 MB — a 48× reduction — with 8.6× headroom over the heaviest real image measured.Verification
All 447 cells across all six corpora are bit-identical, which is the assertion that this reaches only sections built to exhaust it:
python -m pytest tests/→ 2,063 passed, 1 skipped, 2,593 subtestsruff check .andruff format --check .→ cleanmake typecheck→ exit 0, 261 diagnostics, identical to mastere9dfe5f→ 100% (9 lines, 0 missing)Three new cases: that a decoding LSDA is not charged (the condition that keeps the bound off working images — a budget of 1 byte still reaches every table), that an exhausted bound stops calling the reader at all, and that the constant clears the heaviest real image by a stated margin. Verified to fail when the bound is removed.
One thing worth knowing about the shape
The failure has to come before the table's length field to reach the new bound at all. One that fails after it is already charged to the existing budget — which is why the same hostile section built with a too-long length field runs 512 reads rather than 200,000 and needs nothing new. My first attempt at reproducing this used exactly that shape and measured the old bound working, not the gap.
On scope
I said on the issue I would leave this until you picked the number, since sizing it is a corpus judgement. The measurements above make it one: 29.75 MB is the worst real spend, 12.2 GB the hostile one, and any cap between them works. 256 MB is my proposal rather than a finding — change the constant and nothing else moves.