Skip to content

Harden build prefix relocation of poured bottles - #23665

Closed
MikeMcQuaid wants to merge 2 commits into
mainfrom
relocation-observability
Closed

Harden build prefix relocation of poured bottles#23665
MikeMcQuaid wants to merge 2 commits into
mainfrom
relocation-observability

Conversation

@MikeMcQuaid

Copy link
Copy Markdown
Member

- `Keg#relocate_build_prefix` NUL-pads raw prefix strings in pinned
  bottles at pour time but had no tests beyond one happy path; the
  hardening cases in the plan each get a spec, and three found bugs.
- Patching wrote the file atomically, giving it a new inode, so any
  hardlinked names kept the unpatched content. Files are now grouped by
  inode, patched once and re-linked afterwards, as text relocation
  already does.
- A prefix longer than the bottled one failed midway with a size
  mismatch after earlier files had already been rewritten; it is now
  refused up front.
- Linkers suffix-merge strings, so a dynamic string table entry can be
  referenced from its interior (`libfoo.so` inside
  `/old/prefix/libfoo.so`); trailing NUL padding shifted such references
  onto garbage. Strings with interior loader references are now padded
  with extra path separators after the prefix, which path resolution
  ignores, keeping every reference's offset valid. Other strings keep
  NUL padding.
- Also covered: several occurrences per string and several strings per
  file, the sharball skip, re-signing of patched files and prefix
  strings inside Mach-O load commands.

This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
- `HOMEBREW_RELOCATE_BUILD_PREFIX` was read straight from `ENV` in two
  places; it is now a hidden `env_config` boolean so it is typed and
  testable while staying undocumented until the feature is hardened.
- A bottle poured with its build prefix patched in place now records
  `relocated_build_prefix` and `relocated_files` in its tab, so a
  relocated keg can be told apart from a pristine one when diagnosing
  bug reports. `Keg#relocate_build_prefix` returns the patched files
  to make that possible.
- The "building from source" warning for a bottle built for another
  prefix now states how many characters longer or shorter the local
  prefix is than the bottle's, which decides whether in-place
  patching could ever apply, without mentioning the hidden variable.

This change is part of [`plans/relocatable-bottles.md`](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/plans/relocatable-bottles.md)
Copilot AI lite review requested due to automatic review settings August 26, 2026 20:00
@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

This PR hardens Homebrew’s “build prefix relocation” path for poured bottles, improving correctness and observability when patching bottles built for a different (typically longer) prefix. It extends tab metadata to record relocation details, adds a hidden env-config knob, and strengthens relocation logic/tests (including ELF string-table edge cases and Mach-O load-command patching).

Changes:

  • Teach Keg#relocate_build_prefix to return patched file paths, handle multi-occurrence substitutions, and preserve suffix-merged ELF string table references.
  • Record build-prefix relocation state in the installed formula’s tab (build prefix + list of patched files) and gate relocation behind Homebrew::EnvConfig.relocate_build_prefix?.
  • Add/adjust tests and documentation around relocation behavior and diagnostics.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Library/Homebrew/test/tab_spec.rb Extends Tab JSON serialization coverage for relocation fields and ensures bottle hash excludes them.
Library/Homebrew/test/os/mac/keg_spec.rb Adds macOS coverage that relocation patches Mach-O load-command strings (raw/unresolved).
Library/Homebrew/test/keg_relocate/binary_relocation_spec.rb Adds coverage for multi-occurrence replacement, hardlinks, sharball skipping, prefix-length refusal, codesigning, and ELF suffix-merge behavior.
Library/Homebrew/test/formula_installer_spec.rb Verifies pour warnings report prefix-length deltas (longer/shorter).
Library/Homebrew/test/bottle_specification_spec.rb Ensures longer bottle cellar is accepted when build-prefix relocation is enabled.
Library/Homebrew/tab/tab.rb Adds relocated_build_prefix and relocated_files to Tab, includes them in to_json, and keeps them out of to_bottle_hash.
Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi Adds Sorbet RBI for relocate_build_prefix?.
Library/Homebrew/plans/relocatable-bottles.md Updates plan documentation for relocation constraints and suffix-merge padding strategy.
Library/Homebrew/keg_relocate.rb Refactors/strengthens build-prefix relocation, including inode grouping and ELF dynamic string reference handling.
Library/Homebrew/formula_installer.rb Improves incompatible-bottle warning messaging; records relocation results to the Tab when relocation occurs.
Library/Homebrew/env_config.rb Adds hidden HOMEBREW_RELOCATE_BUILD_PREFIX env var wiring.
Library/Homebrew/bottle_specification.rb Uses Homebrew::EnvConfig.relocate_build_prefix? to allow longer-prefix bottles when relocation is enabled.
Files not reviewed (1)
  • Library/Homebrew/sorbet/rbi/dsl/homebrew/env_config.rbi: File type not supported

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +271 to +277
inode_groups = if files
# Metadata-driven pour: only the files recorded at bottle time carry raw
# prefix strings, so skip the whole-keg scan.
hardlinks = Set.new
files.each do |relative_path|
file = path/relative_path
next if !file.file? || file.symlink?
# Hardlinks share an inode, so patch each inode only once.
next unless hardlinks.add? file.stat.ino

candidates << file
end
files.map { |relative_path| path/relative_path }
.select { |file| file.file? && !file.symlink? }
.group_by { |file| file.stat.ino }
.values
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.

2 participants