Merge pull request #23202 from Homebrew/install-step-04-sandbox-hardening

Sandbox formula install steps
This commit is contained in:
Mike McQuaid
2026-07-19 19:24:52 +00:00
committed by GitHub
8 changed files with 40 additions and 20 deletions
+2 -3
View File
@@ -23,11 +23,10 @@ module Homebrew
post_install_steps_defined = f.post_install_steps_defined?
post_install_defined = f.post_install_defined?
f.run_post_install_steps if post_install_steps_defined
if post_install_defined
if post_install_steps_defined || post_install_defined
fi = FormulaInstaller.new(f, **{ debug: args.debug?, quiet: args.quiet?, verbose: args.verbose? }.compact)
fi.post_install
elsif !post_install_steps_defined
else
opoo "#{f}: no `post_install` method was defined in the formula!"
end
end
+4 -2
View File
@@ -1598,6 +1598,7 @@ class Formula
def run_post_install_steps
return if post_install_steps.empty?
prefix_returns_versioned_prefix = @prefix_returns_versioned_prefix
@prefix_returns_versioned_prefix = T.let(true, T.nilable(T::Boolean))
begin
@@ -1605,7 +1606,7 @@ class Formula
Homebrew::InstallSteps::Runner.new(context: self).run(post_install_steps)
end
ensure
@prefix_returns_versioned_prefix = T.let(false, T.nilable(T::Boolean))
@prefix_returns_versioned_prefix = prefix_returns_versioned_prefix
end
end
@@ -1639,7 +1640,8 @@ class Formula
ENV.activate_extensions!
with_logging("post_install") do
post_install
run_post_install_steps if post_install_steps_defined?
post_install if post_install_defined?
end
end
end
+1 -2
View File
@@ -1020,8 +1020,7 @@ on_request: installed_on_request?, options:)
end
else
formula.install_etc_var
formula.run_post_install_steps if formula.post_install_steps_defined?
post_install if formula.post_install_defined?
post_install if formula.post_install_steps_defined? || formula.post_install_defined?
end
keg.prepare_debug_symbols if debug_symbols?
@@ -25,12 +25,12 @@ conversions can peel supported repeated statements out of larger hooks. Runtime
handling runs formula steps first and then runs `post_install` last for the
remaining Ruby work. Cask `*flight_steps` still replace the matching legacy
flight block because cask artifacts already carry replacement semantics and
warn when both forms are present. Post-install or postflight steps are not
sandboxed for this iteration because they run only Homebrew-owned structured
operations. The runner shape leaves room to sandbox future step types that
invoke non-Homebrew code. Future cask work should sandbox all `*flight` run
scripts from non-Homebrew and non-system sources, for example scripts shipped
by upstream artifacts.
warn when both forms are present. Formula post-install steps run in the same
sandboxed subprocess as the remaining `post_install` hook, preserving its
filesystem and network restrictions for structured Ruby operations and any
commands they invoke. Future cask work should sandbox all `*flight` run scripts
from non-Homebrew and non-system sources, for example scripts shipped by
upstream artifacts.
The final target is not to keep legacy hooks and structured steps side by side.
Once `homebrew/core` and `homebrew/cask` have been converted, all
@@ -345,6 +345,12 @@ is stripped during metadata serialisation.
non-Homebrew code and should be ready for future sandboxing. Land RuboCop
autocorrection and tap-wide conversions in a separate follow-up after the
new DSL methods are available in a stable Homebrew release.
- [x] PR 4.1, formula install-step sandboxing.
Commit: `Sandbox formula install steps`.
Scope: run structured formula steps inside the existing post-install child
process so macOS Seatbelt and Linux Bubblewrap apply the same filesystem and
network policy as legacy `post_install` hooks. This must land before any tap
migrations use filesystem-mutating steps.
- PR 5, default config and template writes (four-PR workflow above).
Estimated existing formulae/casks affected: about `112` formulae write or
patch default configuration/data files, and a subset of the `68` file-prep
@@ -7,14 +7,14 @@ require "cmd/shared_examples/args_parse"
RSpec.describe Homebrew::Cmd::Postinstall do
it_behaves_like "parseable arguments"
it "runs post-install steps before the remaining `post_install` hook" do
it "runs post-install steps through `FormulaInstaller`" do
cmd = described_class.new(["foo"])
formula = instance_double(Formula, install_etc_var: nil, post_install_steps_defined?: true,
post_install_defined?: true, to_s: "foo")
post_install_defined?: false, to_s: "foo")
installer = instance_double(FormulaInstaller)
allow(cmd.args.named).to receive(:to_resolved_formulae).and_return([formula])
expect(formula).to receive(:run_post_install_steps).ordered
expect(formula).not_to receive(:run_post_install_steps)
expect(FormulaInstaller).to receive(:new)
.with(formula, debug: false, quiet: false, verbose: false)
.ordered
@@ -103,7 +103,7 @@ RSpec.describe FormulaInstaller do
end
describe "#finish" do
it "runs post-install steps before the remaining `post_install` hook" do
it "runs structured post-install work through the post-install subprocess" do
formula = formula "finish-install-steps" do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
@@ -128,7 +128,7 @@ RSpec.describe FormulaInstaller do
summary: "summary",
verbose?: false,
)
allow(formula).to receive_messages(post_install_steps_defined?: true, post_install_defined?: true,
allow(formula).to receive_messages(post_install_steps_defined?: true, post_install_defined?: false,
runtime_dependencies: [])
allow(CacheStoreDatabase).to receive(:use).with(:linkage)
allow(Homebrew::EnvConfig).to receive(:sbom?).and_return(false)
@@ -138,7 +138,7 @@ RSpec.describe FormulaInstaller do
allow(tab).to receive(:write)
expect(formula).to receive(:install_etc_var).ordered
expect(formula).to receive(:run_post_install_steps).ordered
expect(formula).not_to receive(:run_post_install_steps)
expect(installer).to receive(:post_install).ordered
installer.finish
+14
View File
@@ -1153,6 +1153,20 @@ RSpec.describe Formula do
)
end
specify "#run_post_install runs install steps before the remaining hook" do
f = formula do
T.bind(self, T.class_of(Formula))
url "foo-1.0"
end
allow(Tab).to receive(:for_formula).with(f).and_return(f.build)
allow(f).to receive_messages(post_install_steps_defined?: true, post_install_defined?: true)
expect(f).to receive(:run_post_install_steps).ordered
expect(f).to receive(:post_install).ordered
f.run_post_install
end
specify "#post_install_steps" do
f = formula do
T.bind(self, T.class_of(Formula))
+1 -1
View File
@@ -1166,7 +1166,7 @@ end
Any initialization steps that aren't necessarily part of the install process can be located in a `post_install` block, such as setup commands or data directory creation. This block can be re-run separately with `brew postinstall <formula>`.
For simple file preparation, prefer [`post_install_steps`](/rubydoc/Formula.html#post_install_steps-class_method). These steps are stored in the JSON API and do not require evaluating formula Ruby. A `post_install_steps` block may only contain the supported step calls with literal arguments. It cannot call the wider formula DSL or arbitrary Ruby code.
For simple file preparation, prefer [`post_install_steps`](/rubydoc/Formula.html#post_install_steps-class_method). These steps are stored in the JSON API and do not require evaluating formula Ruby. A `post_install_steps` block may only contain the supported step calls with literal arguments. It cannot call the wider formula DSL or arbitrary Ruby code. Homebrew executes the steps with the same sandbox policy as `post_install`.
```ruby
class Foo < Formula