Skip to content

Commit dd8119e

Browse files
committed
Harden sandboxed install phases
- Deny real home reads unless Homebrew paths live there - Deny sensitive home paths when full-home denial is skipped - Run cask completion generation with a fresh fake `HOME` - Keep cask completion generation offline before running it - Keep test workflow matrix legs running after failures
1 parent 10a163a commit dd8119e

12 files changed

Lines changed: 271 additions & 35 deletions

File tree

.github/workflows/tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ jobs:
186186
needs: syntax
187187
if: github.event_name != 'push'
188188
strategy:
189+
fail-fast: false
189190
matrix:
190191
include:
191192
- name: update-test (Linux)
@@ -212,6 +213,7 @@ jobs:
212213
runs-on: ${{ matrix.runs-on }}
213214
timeout-minutes: 30
214215
strategy:
216+
fail-fast: false
215217
matrix:
216218
include:
217219
- name: tests (online)
@@ -326,6 +328,7 @@ jobs:
326328
runs-on: ${{ matrix.runs-on }}
327329
container: ${{ matrix.container }}
328330
strategy:
331+
fail-fast: false
329332
matrix:
330333
include:
331334
- name: test-bot (Linux arm64)
@@ -414,6 +417,7 @@ jobs:
414417
if: github.repository_owner == 'Homebrew' && github.event_name != 'push'
415418
runs-on: ${{ matrix.runs-on }}
416419
strategy:
420+
fail-fast: false
417421
matrix:
418422
include:
419423
- name: bundle and services (Linux)
@@ -463,6 +467,7 @@ jobs:
463467
name: ${{ matrix.name }}
464468
runs-on: ${{ matrix.runs-on }}
465469
strategy:
470+
fail-fast: false
466471
matrix:
467472
include:
468473
- name: analytics (Linux)

Library/Homebrew/cask/artifact/abstract_artifact.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "env_config"
66
require "sandbox"
77
require "tempfile"
8+
require "tmpdir"
89
require "utils/output"
910

1011
module Cask
@@ -199,6 +200,7 @@ def cask_sandbox
199200
Sandbox.new.tap do |sandbox|
200201
sandbox.allow_read(path: cask.staged_path, type: :subpath)
201202
sandbox.allow_write_temp_and_cache
203+
sandbox.deny_read_home
202204
sandbox.deny_all_network
203205
end
204206
end
@@ -207,9 +209,11 @@ def cask_sandbox
207209
params(
208210
env: T::Hash[String, T.any(String, T::Boolean, PATH)],
209211
args: T::Array[T.any(String, Pathname)],
212+
home: String,
210213
).returns(T::Array[T.any(String, Pathname)])
211214
}
212-
def cask_sandbox_command(env, args)
215+
def cask_sandbox_command(env, args, home:)
216+
env = { "HOME" => home }.merge(env)
213217
["/usr/bin/env", *env.map { |key, value| "#{key}=#{value}" }, *args]
214218
end
215219

Library/Homebrew/cask/artifact/generated_completion.rb

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,22 @@ def generate_completion_output(completion_commands, shell_parameter, env)
136136
end
137137

138138
Tempfile.create("homebrew-cask-completions", HOMEBREW_TEMP) do |output|
139-
sandbox.run(
140-
*cask_sandbox_command(
141-
env,
142-
[
143-
"/bin/sh",
144-
"-c",
145-
"output=$1; shift; exec \"$@\" > \"$output\"#{" 2>/dev/null" unless ENV["HOMEBREW_STDERR"]}",
146-
"sh",
147-
output.path,
148-
*(completion_commands + Array(shell_parameter)),
149-
],
150-
),
151-
)
139+
Dir.mktmpdir("homebrew-cask-home") do |home|
140+
sandbox.run(
141+
*cask_sandbox_command(
142+
env,
143+
[
144+
"/bin/sh",
145+
"-c",
146+
"output=$1; shift; exec \"$@\" > \"$output\"#{" 2>/dev/null" unless ENV["HOMEBREW_STDERR"]}",
147+
"sh",
148+
output.path,
149+
*(completion_commands + Array(shell_parameter)),
150+
],
151+
home:,
152+
),
153+
)
154+
end
152155
output.read
153156
end
154157
end

Library/Homebrew/dev-cmd/test.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def run
9191
sandbox.allow_write_log(f)
9292
sandbox.allow_write_xcode
9393
sandbox.allow_write_path(HOMEBREW_PREFIX/"var/homebrew/locks")
94+
sandbox.deny_read_home
9495
optional_prefix_var_dirs.each do |dir|
9596
sandbox.allow_write_path_if_exists HOMEBREW_PREFIX/dir
9697
end

Library/Homebrew/extend/os/linux/sandbox.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def bubblewrap_sandbox_available?(bubblewrap)
281281
sig { params(args: T.any(String, ::Pathname)).void }
282282
def run(*args)
283283
@prepared_writable_paths = T.let([], T.nilable(T::Array[::Pathname]))
284+
@masked_read_paths = T.let([], T.nilable(T::Array[::Pathname]))
284285
old_report_on_exception = T.let(Thread.report_on_exception, T.nilable(T::Boolean))
285286
Thread.report_on_exception = false
286287
super
@@ -292,6 +293,8 @@ def run(*args)
292293
nil
293294
end
294295
@prepared_writable_paths = nil
296+
@masked_read_paths&.reverse_each { |path| FileUtils.rm_rf(path) }
297+
@masked_read_paths = nil
295298
end
296299

297300
private
@@ -328,6 +331,16 @@ def bubblewrap_args(tmpdir)
328331
args += ["--ro-bind", path, path]
329332
end
330333

334+
denied_read_paths.each do |path|
335+
next unless File.exist?(path)
336+
337+
args += if File.directory?(path)
338+
["--bind", masked_read_path, path]
339+
else
340+
["--ro-bind", File::NULL, path]
341+
end
342+
end
343+
331344
args += ["--bind", tmpdir, tmpdir, "--chdir", tmpdir]
332345

333346
args
@@ -367,6 +380,23 @@ def denied_write_paths
367380
end.uniq
368381
end
369382

383+
sig { returns(T::Array[String]) }
384+
def denied_read_paths
385+
profile.rules.filter_map do |rule|
386+
next if rule.allow || !rule.operation.start_with?("file-read")
387+
388+
filter = rule.filter
389+
filter.path if filter && [:literal, :subpath].include?(filter.type)
390+
end.uniq
391+
end
392+
393+
sig { returns(String) }
394+
def masked_read_path
395+
path = ::Pathname.new(Dir.mktmpdir("homebrew-sandbox-deny-read", HOMEBREW_TEMP))
396+
@masked_read_paths&.<< path
397+
path.to_s
398+
end
399+
370400
sig { params(path: String, type: Symbol).void }
371401
def prepare_writable_path(path, type)
372402
pathname = ::Pathname.new(path)

Library/Homebrew/formula.rb

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,12 +1659,18 @@ def run_post_install
16591659
PATH: PATH.new(ORIGINAL_PATHS),
16601660
}
16611661

1662-
with_env(new_env) do
1663-
ENV.clear_sensitive_environment!
1664-
ENV.activate_extensions!
1662+
Dir.mktmpdir("#{name}-postinstall-") do |home|
1663+
postinstall_home = Pathname(home)
1664+
new_env[:HOME] = postinstall_home.to_s
1665+
setup_home postinstall_home
16651666

1666-
with_logging("post_install") do
1667-
post_install
1667+
with_env(new_env) do
1668+
ENV.clear_sensitive_environment!
1669+
ENV.activate_extensions!
1670+
1671+
with_logging("post_install") do
1672+
post_install
1673+
end
16681674
end
16691675
end
16701676
ensure
@@ -3245,8 +3251,7 @@ def run_test(keep_tmp: false)
32453251
PATH: PATH.new(ENV.fetch("PATH"), HOMEBREW_PREFIX/"bin"),
32463252
HOMEBREW_TERM: ENV.fetch("TERM", nil),
32473253
HOMEBREW_PATH: nil,
3248-
}.merge(common_stage_test_env)
3249-
test_env[:_JAVA_OPTIONS] += " -Djava.io.tmpdir=#{HOMEBREW_TEMP}"
3254+
}
32503255

32513256
ENV.clear_sensitive_environment!
32523257
Utils::Git.set_name_email!
@@ -3255,6 +3260,8 @@ def run_test(keep_tmp: false)
32553260
staging.retain! if keep_tmp
32563261
@testpath = T.let(staging.tmpdir, T.nilable(Pathname))
32573262
test_env[:HOME] = @testpath
3263+
test_env.merge!(common_stage_test_env(T.must(@testpath)))
3264+
test_env[:_JAVA_OPTIONS] += " -Djava.io.tmpdir=#{HOMEBREW_TEMP}"
32583265
setup_home T.must(@testpath)
32593266
begin
32603267
with_logging("test") do
@@ -3706,15 +3713,15 @@ def exec_cmd(cmd, args, out, log_filename)
37063713
end
37073714

37083715
# Common environment variables used at both build and test time.
3709-
sig { returns(T::Hash[Symbol, String]) }
3710-
def common_stage_test_env
3716+
sig { params(home: Pathname).returns(T::Hash[Symbol, String]) }
3717+
def common_stage_test_env(home)
37113718
{
37123719
_JAVA_OPTIONS: "-Duser.home=#{HOMEBREW_CACHE}/java_cache",
37133720
GOCACHE: "#{HOMEBREW_CACHE}/go_cache",
37143721
GOPATH: "#{HOMEBREW_CACHE}/go_mod_cache",
37153722
CARGO_HOME: "#{HOMEBREW_CACHE}/cargo_cache",
37163723
PIP_CACHE_DIR: "#{HOMEBREW_CACHE}/pip_cache",
3717-
CURL_HOME: ENV.fetch("CURL_HOME") { Dir.home },
3724+
CURL_HOME: ENV.fetch("CURL_HOME") { home.to_s },
37183725
PYTHONDONTWRITEBYTECODE: "1",
37193726
}
37203727
end
@@ -3733,7 +3740,7 @@ def stage(interactive: false, debug_symbols: false, &_block)
37333740

37343741
unless interactive
37353742
stage_env[:HOME] = env_home
3736-
stage_env.merge!(common_stage_test_env)
3743+
stage_env.merge!(common_stage_test_env(env_home))
37373744
end
37383745

37393746
setup_home env_home

Library/Homebrew/formula_installer.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,7 +1136,11 @@ def build
11361136
sandbox.allow_read_if_exists path: formula_path
11371137
formula.logs.mkpath
11381138
sandbox.record_log(formula.logs/"build.sandbox.log")
1139-
sandbox.allow_write_path(Dir.home) if interactive?
1139+
if interactive?
1140+
sandbox.allow_write_path(Dir.home)
1141+
else
1142+
sandbox.deny_read_home
1143+
end
11401144
sandbox.allow_write_temp_and_cache
11411145
sandbox.allow_write_log(formula)
11421146
sandbox.allow_cvs
@@ -1383,6 +1387,7 @@ def post_install
13831387
sandbox.allow_write_log(formula)
13841388
sandbox.allow_write_xcode
13851389
sandbox.deny_write_homebrew_repository
1390+
sandbox.deny_read_home
13861391
sandbox.allow_write_cellar(formula)
13871392
sandbox.deny_all_network unless formula.network_access_allowed?(:postinstall)
13881393
Keg.keg_link_directories.each do |dir|

Library/Homebrew/sandbox.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,63 @@ def allow_read(path:, type: :literal)
177177
add_rule allow: true, operation: "file-read*", filter: path_filter(path, type)
178178
end
179179

180+
sig { params(path: T.any(String, Pathname), type: Symbol).void }
181+
def deny_read(path:, type: :literal)
182+
add_rule allow: false, operation: "file-read*", filter: path_filter(path, type)
183+
end
184+
185+
sig { params(path: T.any(String, Pathname)).void }
186+
def deny_read_path(path)
187+
deny_read path:, type: :subpath
188+
end
189+
190+
sig { void }
191+
def deny_read_home
192+
home = Pathname(Dir.home(ENV.fetch("USER"))).realpath
193+
if [
194+
HOMEBREW_PREFIX,
195+
HOMEBREW_REPOSITORY,
196+
HOMEBREW_CACHE,
197+
HOMEBREW_TEMP,
198+
ENV.fetch("GITHUB_WORKSPACE", nil),
199+
ENV.fetch("RUNNER_WORKSPACE", nil),
200+
ENV.fetch("RUNNER_TEMP", nil),
201+
].compact.any? do |path|
202+
path = Pathname(path)
203+
[path.expand_path, path.exist? ? path.realpath : nil].compact.any? { |pathname| pathname.ascend.include?(home) }
204+
end
205+
[
206+
".ssh",
207+
".aws",
208+
".azure",
209+
".config/gcloud",
210+
".docker",
211+
".gnupg",
212+
".kube",
213+
".netrc",
214+
".npmrc",
215+
".pypirc",
216+
".gem/credentials",
217+
"Documents",
218+
"Movies",
219+
"Music",
220+
"Pictures",
221+
"Library/Keychains",
222+
"Library/Mobile Documents",
223+
"Library/CloudStorage",
224+
"Dropbox",
225+
"Google Drive",
226+
"OneDrive",
227+
].each do |path|
228+
path = home/path
229+
deny_read_path path if path.exist?
230+
end
231+
return
232+
end
233+
234+
deny_read_path home
235+
end
236+
180237
sig { params(path: T.nilable(T.any(String, Pathname)), type: Symbol).void }
181238
def allow_read_if_exists(path:, type: :literal)
182239
return unless path

0 commit comments

Comments
 (0)