Check ELF relocatability by structure when bottling - #23644
Conversation
|
Thanks for your pull request. This has been closed because it appears to be missing the pull request template, perhaps because this was written by an AI not a human. We require humans to read and fill in these templates. Please edit this pull request to fill in the current pull request template. This workflow will reopen this pull request automatically once the template is complete. Do not open a new pull request for this. |
There was a problem hiding this comment.
Pull request overview
Updates bottling scans to ignore stale ELF prefix strings left by patchelf while preserving live references.
Changes:
- Adds ELF-aware match filtering.
- Adds regression tests for stale and live strings.
- Removes the completed relocation-plan item.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Summary | Findings |
|---|---|---|
Library/Homebrew/test/keg_relocate/elf_checker_spec.rb |
Adds ELF filtering regression coverage. | None |
Library/Homebrew/plans/relocatable-bottles.md |
Removes the completed plan item. | None |
Library/Homebrew/keg_relocate.rb |
Implements ELF-aware filtering. | Two moderate findings (4 votes each): account for DT_CONFIG, DT_DEPAUDIT, and DT_AUDIT; rescue parser truncation errors such as IOError/EOFError. |
Suppressed comments (3)
Library/Homebrew/keg_relocate.rb:540
- The vendored
elftoolsdefinesDT_FILTERas0x7ffffffe, the same value asDT_USED, while the ELF ABI value forDT_FILTERis0x7fffffff. Thus an actualDT_FILTERtag is not added tolive_string_offsets(and aDT_USEDvalue is treated as a string offset), so a live filter path can be discarded. Correct the dependency constant or account for the ABI value here.
ELFTools::Constants::DT::DT_FILTER,
Library/Homebrew/keg_relocate.rb:572
match_rangecovers the entire printable run returned bystrings, not the specific prefix occurrence being reported. For example, if a dynamic tag points at the interiorlibfoo.soof/old/prefix/libfoo.so, the live offset lies in this range and the unreferenced/old/prefixis retained, so this still pins a stale suffix-merged prefix. Track the byte range of the actual matched occurrence before deciding it is live.
# A string some dynamic tag still points into is live, including
# references into the middle of a suffix-merged string.
next true if live_string_offsets.any? { |live| match_range.cover?(live) }
Library/Homebrew/keg_relocate.rb:536
- The dynamic string table is also indexed by loader-visible structures such as
.dynsymsymbolst_nameentries (and version tables), not only by the string-valued dynamic tags listed here. A prefix-containing live symbol name will therefore be treated as an unreferenced.dynstrstring and dropped. Preserve offsets referenced by the dynamic symbol/version tables before classifying the remainder as dead.
# Dynamic tags whose value is an offset into the dynamic string
# table, i.e. the strings the loader can actually see.
string_tags = [
ELFTools::Constants::DT::DT_NEEDED,
ELFTools::Constants::DT::DT_SONAME,
馃挕 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
0796227 to
17c8937
Compare
17c8937 to
97ea33c
Compare
There was a problem hiding this comment.
I don't know if we can justify treating every match outside the section table as dead, which seems to be what this approach does implicitly.
ELFFile#sections reflects only the section headers, while strings - scans the whole file. ELF execution is described by program headers, not just sections, so I think we can pick up text matches in non-section data but mistakenly classify them as non-live.
Also: I think Copilot's suppressed comments contain correctness issues that shouldn't have been suppressed.
|
I think we want to keep the exception narrow here. We only need to identify strings that are demonstrably stale artefacts left behind by |
I disagree. As the person who wrote the original I'll look at all the comments here including the suppressed one but just want to be explicit that the there is an active goal here to be less "safe" and prioritise removing false negatives over false positives which we can trivially fix if we get bug reports. |
f36bcda to
0486417
Compare
0486417 to
3f33af7
Compare
In that case, this goal should have been articulated clearly in at least one of, and ideally all three of:
The PR body and design document describe the goal as removing false positives caused by stale Moreover, if the intended policy is actually that strings outside ELF sections never count, we should skip the whole-file |
The broad policy here is "make all bottles relocatable" and that is and was very clearly documented in the plan. Literally the goal is "every Homebrew/core bottle pours at every prefix".
If you're suggesting just do this for ELF files specifically: yes, if this is what you'd like me to do I'd be in favour. We shouldn't skip this for other files types. |
Making all bottles actually relocatable is not the same policy as making the checker more willing to classify bottles as relocatable. The former is clearly documented; the latter is the policy trade-off I鈥檓 saying was not.
Yes, this is what I was suggesting. |
3f33af7 to
1d0e017
Compare
|
@carlocab adjusted code/plan and PR title/body accordingly, thanks! |
1d0e017 to
d5fcbbf
Compare
carlocab
left a comment
There was a problem hiding this comment.
Minor nits. I don't know if I'm sold on how much we're trading off false-positives vs false-negatives here, especially considering that this seems to be much less conservative than what conda or spack do. But I'm not opposed to trying it.
Just mentioning that During install, Meson replaces build RPATH with install RPATH but doesn't erase old contents which can leave behind unwanted paths. Nix has carried a patch for over 5 years now to avoid this. Given this seems to be an intentional design decision in Meson based on comment, ignoring the leftover strings is reasonable. |
- `brew bottle` decides whether a bottle is relocatable by scanning every file with `strings` and treating each surviving prefix byte sequence as a pin. For ELF files that counts bytes the program can never reach: Meson's install-time RPATH fixer overwrites the build RPATH with the shorter install RPATH without clearing the rest of the old string (intentional upstream; Nix patches it out), and patchelf leaves the old dynamic string table or interpreter behind when growing them, so bottles whose live linkage is fully placeholdered (e.g. `harfbuzz` and the glib/gobject-introspection cluster) were wrongly pinned to their build cellar. - This deliberately rebalances the checker towards classifying bottles as relocatable, as now recorded in design decision 11 of the plan: a wrongly pinned bottle forces source builds for every non-default-prefix user and poisons its whole dependent subtree, whereas a wrongly relocatable one surfaces as a per-formula bug report with a trivial fix and is caught by the planned validation sweep and test-bot relocated-pour test. - ELF files are therefore no longer `strings`-scanned as a whole. `Keg.text_matches_in_file` now takes its candidates from the ELF structures themselves, using the vendored elftools: the interpreter the loader uses, the dynamic strings the loader references (dynamic tags including the ELF ABI's real `DT_FILTER`, which elftools 1.3.1 mislabels, plus `.dynsym` names; a suffix-merged reference never yields the dead prefix in front of it) and the printable contents of the remaining sections. Bytes outside every section and unreferenced entries in loader-owned string tables are never candidates. - Files that are not ELF, cannot name their sections (`e_shstrndx` of `SHN_UNDEF`) or whose tables are truncated mid-parse keep the whole-file scan, so nothing is dropped where the structure cannot be read. - Affected formulae flip to `cellar :any` on their next rebottle. This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
d5fcbbf to
7eb606f
Compare
|
@carlocab thanks for approval!
As far as I can tell, Conda and Spack don't run a relocatability checker at all; they assume relocatability by construction (padded prefix + unconditional rewrite) and rely on bug reports, which is arguably less conservative than what we're doing. If you have any links to their differences in approaches (or corrections to above): will happily read an incorporate! |
This PR deliberately rebalances the bottle relocatability checker towards classifying bottles as relocatable rather than pinned, now documented as design decision 11 in
plans/relocatable-bottles.md.The original
strings-based checker treats every prefix byte sequence in a file as a pin. For ELF files that counts bytes the program can never reach: Meson's install-time RPATH fixer overwrites the build RPATH with the shorter install RPATH without clearing the rest of the old string (intentional upstream; Nix patches it out), and patchelf leaves the old dynamic string table or interpreter behind when growing them. A wrongly pinned bottle forces source builds for every non-default-prefix user and poisons its whole dependent subtree, whereas a wrongly relocatable one surfaces as a per-formula bug report with a trivial fix and is caught by the planned validation sweep and test-bot relocated-pour test.Concretely, ELF files are no longer
strings-scanned as a whole.Keg.text_matches_in_filenow takes its candidates from the ELF structures themselves, using the vendored elftools:PT_INTERP);DT_FILTER, which elftools 1.3.1 mislabels) and.dynsymnames, read from their offset to the NUL so a suffix-merged reference never yields the dead prefix in front of it;Bytes outside every section and unreferenced entries in loader-owned string tables are never candidates. Files that are not ELF, cannot name their sections (
e_shstrndxofSHN_UNDEF) or whose tables are truncated mid-parse keep the whole-file scan, so nothing is dropped where the structure cannot be read. Other file types are unchanged.Affected formulae flip to
cellar :anyon their next rebottle.This change is part of
plans/relocatable-bottles.mdbrew benchmarkresults.brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?Claude Fable 5 with local review and testing.