Skip to content

Check ELF relocatability by structure when bottling - #23644

Merged
MikeMcQuaid merged 1 commit into
mainfrom
stale-elf-strings
Aug 26, 2026
Merged

Check ELF relocatability by structure when bottling#23644
MikeMcQuaid merged 1 commit into
mainfrom
stale-elf-strings

Conversation

@MikeMcQuaid

@MikeMcQuaid MikeMcQuaid commented Aug 24, 2026

Copy link
Copy Markdown
Member

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_file now takes its candidates from the ELF structures themselves, using the vendored elftools:

  • the interpreter the loader uses (PT_INTERP);
  • the dynamic strings the loader references: dynamic tags (including the ELF ABI's real DT_FILTER, which elftools 1.3.1 mislabels) and .dynsym names, read from their offset to the NUL so a suffix-merged reference never yields the dead prefix in front of it;
  • 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. Other file types are unchanged.

Affected formulae flip to cellar :any on their next rebottle.

This change is part of plans/relocatable-bottles.md


  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include brew benchmark results.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

Claude Fable 5 with local review and testing.


@github-actions

Copy link
Copy Markdown
Contributor

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 elftools defines DT_FILTER as 0x7ffffffe, the same value as DT_USED, while the ELF ABI value for DT_FILTER is 0x7fffffff. Thus an actual DT_FILTER tag is not added to live_string_offsets (and a DT_USED value 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_range covers the entire printable run returned by strings, not the specific prefix occurrence being reported. For example, if a dynamic tag points at the interior libfoo.so of /old/prefix/libfoo.so, the live offset lies in this range and the unreferenced /old/prefix is 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 .dynsym symbol st_name entries (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 .dynstr string 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.

Comment thread Library/Homebrew/keg_relocate.rb
Comment thread Library/Homebrew/keg_relocate.rb Outdated
Base automatically changed from relocation-metadata-pour to main August 24, 2026 19:46

@carlocab carlocab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Library/Homebrew/keg_relocate.rb Outdated
Comment thread Library/Homebrew/keg_relocate.rb Outdated
@carlocab

Copy link
Copy Markdown
Member

I think we want to keep the exception narrow here. We only need to identify strings that are demonstrably stale artefacts left behind by patchelf.rb; treating every match outside the current ELF structure as non-live seems broader than necessary, and risks suppressing strings we don't actually know are dead.

@MikeMcQuaid

Copy link
Copy Markdown
Member Author

I think we want to keep the exception narrow here. We only need to identify strings that are demonstrably stale artefacts left behind by patchelf.rb; treating every match outside the current ELF structure as non-live seems broader than necessary, and risks suppressing strings we don't actually know are dead.

I disagree. As the person who wrote the original strings implementation: it's far too broad and we don't have a rock solid approach that allows us to narrow it sufficiently. It's a trade-off, ultimately, and the benefit to users on non-default prefixes is much higher when we nudge things closer towards assuming relocation rather than assuming not.

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.

@MikeMcQuaid
MikeMcQuaid force-pushed the stale-elf-strings branch 2 times, most recently from f36bcda to 0486417 Compare August 25, 2026 11:17
@MikeMcQuaid
MikeMcQuaid requested a balanced review from Copilot August 25, 2026 11:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread Library/Homebrew/keg_relocate.rb Outdated
Comment thread Library/Homebrew/keg_relocate.rb Outdated
Comment thread Library/Homebrew/test/keg_relocate/elf_checker_spec.rb Outdated
@carlocab

Copy link
Copy Markdown
Member

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

In that case, this goal should have been articulated clearly in at least one of, and ideally all three of:

  • the PR body
  • the commit message
  • the design document

The PR body and design document describe the goal as removing false positives caused by stale patchelf artefacts. Deliberately making ELF relocation detection less conservative is a materially broader policy, and changes how this PR should be reviewed.

Moreover, if the intended policy is actually that strings outside ELF sections never count, we should skip the whole-file strings - scan for ELF files and just scan the relevant ELF sections instead. This avoids wasted work, makes that policy explicit in the implementation, and makes the code easier to reason about.

@MikeMcQuaid

Copy link
Copy Markdown
Member Author

Deliberately making ELF relocation detection less conservative is a materially broader policy, and changes how this PR should be reviewed.

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".

Moreover, if the intended policy is actually that strings outside ELF sections never count, we should skip the whole-file strings - scan for ELF files and just scan the relevant ELF sections instead.

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.

@carlocab

Copy link
Copy Markdown
Member

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".

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.

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.

Yes, this is what I was suggesting.

@MikeMcQuaid MikeMcQuaid changed the title Stop counting stale ELF strings when bottling Check ELF relocatability by structure when bottlin Aug 25, 2026
@MikeMcQuaid

Copy link
Copy Markdown
Member Author

@carlocab adjusted code/plan and PR title/body accordingly, thanks!

@MikeMcQuaid MikeMcQuaid changed the title Check ELF relocatability by structure when bottlin Check ELF relocatability by structure when bottling Aug 25, 2026

@carlocab carlocab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Library/Homebrew/keg_relocate.rb Outdated
Comment thread Library/Homebrew/keg_relocate.rb Outdated
@cho-m

cho-m commented Aug 26, 2026

Copy link
Copy Markdown
Member

growing an RPATH or interpreter makes patchelf move the dynamic string table or interpreter, leaving the old prefix string dead in the file, so bottles whose live linkage is fully placeholdered (e.g. harfbuzz) were wrongly pinned to their build cellar

Just mentioning that harfbuzz is more likely from meson shrinking the RPATH rather than patchelf growing the RPATH. I looked into these when trying to find out why bubblewrap was not relocatable:

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)
@MikeMcQuaid

Copy link
Copy Markdown
Member Author

@carlocab thanks for approval!

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

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!

@MikeMcQuaid
MikeMcQuaid added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit ea0ac33 Aug 26, 2026
48 checks passed
@MikeMcQuaid
MikeMcQuaid deleted the stale-elf-strings branch August 26, 2026 18:23
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.

5 participants