Skip to content

Commit 8cf9c3b

Browse files
committed
Add Linux Bubblewrap sandbox
- Use `bwrap` to translate shared sandbox rules into rootless namespace execution. - Gate the backend behind `HOMEBREW_SANDBOX_LINUX` while the Linux policy is still experimental. - Keep macOS on its existing `sandbox-exec` path while Linux uses shared executable lookup for `bwrap`. - Auto-install `bubblewrap` from `homebrew/core` when the sandbox is enabled and no system or usable brewed binary is found. - Prefer a usable system `bwrap` from `ORIGINAL_PATHS` over a brewed one so distribution-provided binaries are used when available. - Find brewed `bwrap` from `HOMEBREW_ORIGINAL_BREW_FILE` so integration subprocesses still use the Linux sandbox. - Preserve `Sandbox#run`'s tmpdir cwd inside the `bwrap` namespace and suppress Linux PTY thread warnings after sandboxed children exit. - Expose formula, Homebrew library and Linux runtime paths as read-only binds so sandboxed source builds can run with vendored Ruby. - Keep synthetic test formula installs off the API so sandbox CI does not require network during local formula setup. - Avoid creating optional prefix `var` directories just to configure a test sandbox and remove empty Linux bind placeholders after runs. - Require a working rootless `bwrap` on GitHub Actions and install the system `bubblewrap` package before Linux tests run.
1 parent b5fdb50 commit 8cf9c3b

18 files changed

Lines changed: 912 additions & 54 deletions

File tree

.github/workflows/tests.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,21 @@ jobs:
274274
workflow-key: tests-tests-macos
275275
uninstall: true
276276

277+
- name: Install brew tests Linux dependencies
278+
if: runner.os == 'Linux'
279+
run: |
280+
sudo apt-get update
281+
sudo apt-get install -y --no-install-recommends bubblewrap
282+
# Allow unprivileged user namespace cloning; rootless `bwrap` needs this
283+
# to create its user namespace.
284+
sudo sysctl -w kernel.unprivileged_userns_clone=1
285+
# Ensure the runner can allocate user namespaces instead of hitting a
286+
# per-user namespace limit.
287+
sudo sysctl -w user.max_user_namespaces=28633
288+
# Ubuntu runners may additionally restrict unprivileged user namespaces
289+
# through AppArmor; older kernels may not expose this sysctl.
290+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
291+
277292
# brew tests doesn't like world writable directories
278293
- name: Cleanup permissions
279294
if: runner.os == 'Linux'
@@ -286,6 +301,7 @@ jobs:
286301
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
287302
# These cannot be queried at the macOS level on GitHub Actions.
288303
HOMEBREW_LANGUAGES: en-GB
304+
HOMEBREW_SANDBOX_LINUX: 1
289305
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
290306

291307
- name: Get RSpec JUnit XML filenames
@@ -335,6 +351,7 @@ jobs:
335351
- name: test-bot (macOS arm64)
336352
runs-on: macos-26
337353
env:
354+
HOMEBREW_SANDBOX_LINUX: 1
338355
HOMEBREW_TEST_BOT_ANALYTICS: 1
339356
steps:
340357
- name: Install Homebrew and Homebrew's dependencies
@@ -345,6 +362,7 @@ jobs:
345362
# Slimmed down version from the Homebrew Dockerfile
346363
apt-get update
347364
apt-get install -y --no-install-recommends \
365+
bubblewrap \
348366
bzip2 \
349367
ca-certificates \
350368
curl \

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
2929
&& retry apt-get update --error-on=any \
3030
&& apt-get install -y --no-install-recommends \
3131
acl \
32+
bubblewrap \
3233
bzip2 \
3334
ca-certificates \
3435
curl \

Library/Homebrew/build.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ def normalize_pod2man_outputs!(formula)
260260
# We need to allow formulae from paths here due to how we pass them through.
261261
ENV["HOMEBREW_INTERNAL_ALLOW_PACKAGES_FROM_PATHS"] = "1"
262262

263+
formula_path = ARGV.first
264+
# `build.rb` is handed a concrete formula file; keep reparsing from falling
265+
# back to the API inside the Linux sandbox.
266+
ENV["HOMEBREW_NO_INSTALL_FROM_API"] = "1" if formula_path&.end_with?(".rb")
267+
263268
args = Homebrew::Cmd::InstallCmd.new.args
264269
Context.current = args.context
265270

@@ -268,7 +273,6 @@ def normalize_pod2man_outputs!(formula)
268273

269274
trap("INT", old_trap)
270275

271-
formula_path = ARGV.first
272276
if formula_path&.end_with?(".json")
273277
raise "build.rb received an API JSON file as the formula path: #{formula_path}. " \
274278
"This usually means the formula source was not downloaded from the API. " \

Library/Homebrew/dev-cmd/test.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def run
3737
require "formula_free_port"
3838
require "utils/fork"
3939

40+
optional_prefix_var_dirs = %w[var/cache var/log var/run]
4041
args.named.to_resolved_formulae.each do |f|
4142
# Cannot test uninstalled formulae
4243
unless f.latest_version_installed?
@@ -81,17 +82,18 @@ def run
8182

8283
exec_args << "--HEAD" if f.head?
8384

85+
Sandbox.ensure_sandbox_installed!
8486
if Sandbox.available?
8587
sandbox = Sandbox.new
8688
f.logs.mkpath
8789
sandbox.record_log(f.logs/"test.sandbox.log")
8890
sandbox.allow_write_temp_and_cache
8991
sandbox.allow_write_log(f)
9092
sandbox.allow_write_xcode
91-
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/cache")
9293
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/homebrew/locks")
93-
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/log")
94-
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/run")
94+
optional_prefix_var_dirs.each do |dir|
95+
sandbox.allow_write_path_if_exists HOMEBREW_PREFIX/dir
96+
end
9597
sandbox.deny_all_network unless f.class.network_access_allowed?(:test)
9698
sandbox.run(*exec_args)
9799
else

Library/Homebrew/dev-cmd/tests.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def run
6666
# `return false`.
6767
require "extend/os/dev-cmd/tests"
6868

69+
check_test_environment!
70+
6971
parallel = !args.no_parallel?
7072

7173
only = args.only
@@ -210,6 +212,9 @@ def non_linux_bundle_args(bundle_args)
210212
bundle_args << "--tag" << "~needs_linux" << "--tag" << "~needs_systemd"
211213
end
212214

215+
sig { void }
216+
def check_test_environment!; end
217+
213218
sig { params(files: T::Array[String]).returns(T::Array[String]) }
214219
def os_files(files)
215220
# for generic tests, remove macOS or Linux specific files
@@ -256,6 +261,7 @@ def setup_environment!
256261
HOMEBREW_GITHUB_API_TOKEN
257262
HOMEBREW_CACHE
258263
HOMEBREW_LOGS
264+
HOMEBREW_SANDBOX_LINUX
259265
HOMEBREW_TEMP
260266
]
261267
allowed_test_env << "HOMEBREW_USE_RUBY_FROM_PATH" if Homebrew::EnvConfig.developer?

Library/Homebrew/env_config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,10 @@ module EnvConfig
486486
description: "If set, use Pry for the `brew irb` command.",
487487
boolean: true,
488488
},
489+
HOMEBREW_SANDBOX_LINUX: {
490+
description: "If set, use the `bwrap`(1) sandbox for formula installation and testing on Linux.",
491+
boolean: true,
492+
},
489493
HOMEBREW_SBOM: {
490494
# odeprecated: edit in 5.2.0
491495
description: "If set, Homebrew will write SBOM files and run SBOM-related installation logic. " \

Library/Homebrew/extend/os/linux/dev-cmd/tests.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "utils/github/actions"
5+
46
module OS
57
module Linux
68
module DevCmd
@@ -20,6 +22,19 @@ def os_bundle_args(bundle_args)
2022
def os_files(files)
2123
non_macos_files(files)
2224
end
25+
26+
sig { void }
27+
def check_test_environment!
28+
super
29+
return unless GitHub::Actions.env_set?
30+
31+
require "sandbox"
32+
with_env(HOMEBREW_SANDBOX_LINUX: "1") do
33+
return if ::Sandbox.available?
34+
35+
raise UsageError, "GitHub Actions Linux tests require a working rootless Bubblewrap sandbox."
36+
end
37+
end
2338
end
2439
end
2540
end

0 commit comments

Comments
 (0)