mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Skip unavailable sandbox sysctls
- Check `/proc/sys` entries before calling `sysctl` so kernels that omit keys do not block the Bubblewrap probe. - Skip unwritable proc sysctl files, including read-only mounts. - Keep attempted `sysctl` writes best effort and surface failed `bwrap` probe output for debugging.
This commit is contained in:
@@ -37,16 +37,30 @@ homebrew-setup-sandbox() {
|
||||
|
||||
# These settings mirror SANDBOX_SYSCTL_SETTINGS in
|
||||
# Library/Homebrew/extend/os/linux/sandbox.rb; keep both in sync.
|
||||
if [[ $(sysctl -n "kernel.unprivileged_userns_clone" || echo 0) != "1" ]]
|
||||
local proc_sys_root="${HOMEBREW_PROC_SYS:-/proc/sys}"
|
||||
local sysctl_value
|
||||
local unprivileged_userns_clone_sysctl="${proc_sys_root}/kernel/unprivileged_userns_clone"
|
||||
if [[ -e "${unprivileged_userns_clone_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "kernel.unprivileged_userns_clone")" &&
|
||||
[[ "${sysctl_value}" != "1" ]] &&
|
||||
[[ -w "${unprivileged_userns_clone_sysctl}" ]]
|
||||
then
|
||||
sysctl -w kernel.unprivileged_userns_clone=1
|
||||
sysctl -w kernel.unprivileged_userns_clone=1 || true
|
||||
fi
|
||||
if [[ $(sysctl -n "user.max_user_namespaces" || echo 0) -lt 28633 ]]
|
||||
local max_user_namespaces_sysctl="${proc_sys_root}/user/max_user_namespaces"
|
||||
if [[ -e "${max_user_namespaces_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "user.max_user_namespaces")" &&
|
||||
[[ "${sysctl_value}" -lt 28633 ]] &&
|
||||
[[ -w "${max_user_namespaces_sysctl}" ]]
|
||||
then
|
||||
sysctl -w user.max_user_namespaces=28633
|
||||
sysctl -w user.max_user_namespaces=28633 || true
|
||||
fi
|
||||
|
||||
if [[ $(sysctl -n "kernel.apparmor_restrict_unprivileged_userns" || echo 0) != "0" ]]
|
||||
local apparmor_restrict_unprivileged_userns_sysctl="${proc_sys_root}/kernel/apparmor_restrict_unprivileged_userns"
|
||||
if [[ -e "${apparmor_restrict_unprivileged_userns_sysctl}" ]] &&
|
||||
sysctl_value="$(sysctl -n "kernel.apparmor_restrict_unprivileged_userns")" &&
|
||||
[[ "${sysctl_value}" != "0" ]] &&
|
||||
[[ -w "${apparmor_restrict_unprivileged_userns_sysctl}" ]]
|
||||
then
|
||||
sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
require "fileutils"
|
||||
require "env_config"
|
||||
require "system_command"
|
||||
require "utils/popen"
|
||||
require "utils/github/actions"
|
||||
|
||||
@@ -121,6 +122,7 @@ module OS
|
||||
|
||||
module ClassMethods
|
||||
extend T::Helpers
|
||||
include SystemCommand::Mixin
|
||||
include Utils::Output::Mixin
|
||||
|
||||
requires_ancestor { T.class_of(::Sandbox) }
|
||||
@@ -317,12 +319,16 @@ module OS
|
||||
|
||||
sig { params(bubblewrap: ::Pathname).returns(T::Boolean) }
|
||||
def bubblewrap_sandbox_available?(bubblewrap)
|
||||
system(
|
||||
bubblewrap.to_s,
|
||||
*BUBBLEWRAP_TEST_ARGS,
|
||||
out: File::NULL,
|
||||
err: File::NULL,
|
||||
) == true
|
||||
result = system_command(
|
||||
bubblewrap,
|
||||
args: BUBBLEWRAP_TEST_ARGS,
|
||||
print_stderr: false,
|
||||
)
|
||||
return true if result.success?
|
||||
|
||||
opoo "bubblewrap test probe failed"
|
||||
$stderr.print result.merged_output
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
require "open3"
|
||||
|
||||
require "cmd/shared_examples/args_parse"
|
||||
@@ -8,15 +9,27 @@ require "cmd/setup-sandbox"
|
||||
|
||||
RSpec.describe Homebrew::Cmd::SetupSandbox do
|
||||
let(:setup_sandbox_script) { HOMEBREW_LIBRARY_PATH/"cmd/setup-sandbox.sh" }
|
||||
let(:proc_sys_root) { mktmpdir }
|
||||
|
||||
it_behaves_like "parseable arguments"
|
||||
|
||||
def run_setup_sandbox_shell(script, env = {})
|
||||
Bundler.with_unbundled_env do
|
||||
Open3.capture3({ "GITHUB_ACTIONS" => nil, "HOMEBREW_LINUX" => "1" }.merge(env), "/bin/bash", "-c", script)
|
||||
Open3.capture3(
|
||||
{ "GITHUB_ACTIONS" => nil, "HOMEBREW_LINUX" => "1", "HOMEBREW_PROC_SYS" => proc_sys_root.to_s }
|
||||
.merge(env),
|
||||
"/bin/bash", "-c", script
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def touch_proc_sys(path)
|
||||
file = proc_sys_root/path
|
||||
FileUtils.mkdir_p(file.dirname)
|
||||
FileUtils.touch(file)
|
||||
file
|
||||
end
|
||||
|
||||
it "does nothing on non-Linux systems" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "HOMEBREW_LINUX" => nil)
|
||||
source "#{setup_sandbox_script}"
|
||||
@@ -29,6 +42,9 @@ RSpec.describe Homebrew::Cmd::SetupSandbox do
|
||||
end
|
||||
|
||||
it "applies the sandbox sysctl settings when they are unset" do
|
||||
touch_proc_sys "kernel/unprivileged_userns_clone"
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() { [[ "$1" == "-n" ]] && { echo 0; return; }; printf 'sysctl %s\\n' "$*"; }
|
||||
@@ -43,6 +59,10 @@ RSpec.describe Homebrew::Cmd::SetupSandbox do
|
||||
end
|
||||
|
||||
it "leaves already-configured sysctls unchanged" do
|
||||
touch_proc_sys "kernel/unprivileged_userns_clone"
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
touch_proc_sys "kernel/apparmor_restrict_unprivileged_userns"
|
||||
|
||||
stdout, _stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() {
|
||||
@@ -64,6 +84,53 @@ RSpec.describe Homebrew::Cmd::SetupSandbox do
|
||||
expect(stdout).to be_empty
|
||||
end
|
||||
|
||||
it "skips missing sysctls and read-only sysctl writes" do
|
||||
touch_proc_sys("user/max_user_namespaces").chmod(0444)
|
||||
|
||||
stdout, stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl_log="#{proc_sys_root}/sysctl.log"
|
||||
sysctl() {
|
||||
printf '%s\\n' "$*" >> "$sysctl_log"
|
||||
if [[ "$1" == "-n" && "$2" == "user.max_user_namespaces" ]]
|
||||
then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
printf 'unexpected sysctl %s\\n' "$*" >&2
|
||||
return 1
|
||||
}
|
||||
homebrew-setup-sandbox
|
||||
cat "$sysctl_log"
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to eq("-n user.max_user_namespaces\n")
|
||||
expect(stderr).to be_empty
|
||||
end
|
||||
|
||||
it "does not hide sysctl write errors" do
|
||||
touch_proc_sys "user/max_user_namespaces"
|
||||
|
||||
stdout, stderr, status = run_setup_sandbox_shell <<~SH
|
||||
source "#{setup_sandbox_script}"
|
||||
sysctl() {
|
||||
if [[ "$1" == "-n" ]]
|
||||
then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
echo 'sysctl: setting key "user.max_user_namespaces", ignoring: Read-only file system' >&2
|
||||
return 1
|
||||
}
|
||||
homebrew-setup-sandbox
|
||||
SH
|
||||
|
||||
expect(status.success?).to be true
|
||||
expect(stdout).to be_empty
|
||||
expect(stderr).to eq("sysctl: setting key \"user.max_user_namespaces\", ignoring: Read-only file system\n")
|
||||
end
|
||||
|
||||
it "installs Bubblewrap on GitHub Actions when it is missing" do
|
||||
stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "GITHUB_ACTIONS" => "true")
|
||||
source "#{setup_sandbox_script}"
|
||||
|
||||
@@ -60,9 +60,10 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
let(:bubblewrap) { bubblewrap_dir/"bwrap" }
|
||||
let(:fallback_bubblewrap_dir) { mktmpdir }
|
||||
let(:fallback_bubblewrap) { fallback_bubblewrap_dir/"bwrap" }
|
||||
let(:bubblewrap_test_args) do
|
||||
let(:successful_result) { instance_double(SystemCommand::Result, success?: true) }
|
||||
let(:failed_result) { instance_double(SystemCommand::Result, success?: false, merged_output: "") }
|
||||
let(:bubblewrap_probe_args) do
|
||||
[
|
||||
bubblewrap.to_s,
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
@@ -71,8 +72,14 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true",
|
||||
{ err: :out }
|
||||
"true"
|
||||
]
|
||||
end
|
||||
let(:bubblewrap_test_args) do
|
||||
[
|
||||
bubblewrap.to_s,
|
||||
*bubblewrap_probe_args,
|
||||
{ err: :out },
|
||||
]
|
||||
end
|
||||
|
||||
@@ -97,20 +104,11 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
end
|
||||
|
||||
it "probes unprivileged namespace support once" do
|
||||
expect(sandbox_class).to receive(:system).once.with(
|
||||
bubblewrap.to_s,
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true",
|
||||
out: File::NULL,
|
||||
err: File::NULL
|
||||
).and_return(true)
|
||||
expect(sandbox_class).to receive(:system_command).once.with(
|
||||
bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(successful_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(true)
|
||||
expect(sandbox_class.state).to eq(:available)
|
||||
@@ -122,34 +120,16 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
FileUtils.chmod "+x", fallback_bubblewrap
|
||||
sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir, fallback_bubblewrap_dir)
|
||||
|
||||
expect(sandbox_class).to receive(:system).with(
|
||||
bubblewrap.to_s,
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true",
|
||||
out: File::NULL,
|
||||
err: File::NULL
|
||||
).and_return(false)
|
||||
expect(sandbox_class).to receive(:system).with(
|
||||
fallback_bubblewrap.to_s,
|
||||
"--unshare-user",
|
||||
"--unshare-ipc",
|
||||
"--unshare-pid",
|
||||
"--unshare-uts",
|
||||
"--unshare-cgroup-try",
|
||||
"--ro-bind", "/", "/",
|
||||
"--proc", "/proc",
|
||||
"--dev", "/dev",
|
||||
"true",
|
||||
out: File::NULL,
|
||||
err: File::NULL
|
||||
).and_return(true)
|
||||
expect(sandbox_class).to receive(:system_command).with(
|
||||
bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(failed_result)
|
||||
expect(sandbox_class).to receive(:system_command).with(
|
||||
fallback_bubblewrap,
|
||||
args: bubblewrap_probe_args,
|
||||
print_stderr: false,
|
||||
).and_return(successful_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(true)
|
||||
end
|
||||
@@ -164,13 +144,23 @@ RSpec.describe Sandbox, :needs_linux do
|
||||
end
|
||||
|
||||
it "reports bubblewrap sandbox probe failures" do
|
||||
allow(sandbox_class).to receive(:system).and_return(false)
|
||||
allow(sandbox_class).to receive(:system_command).and_return(failed_result)
|
||||
|
||||
expect(sandbox_class.available?).to be(false)
|
||||
expect(sandbox_class.state).to eq(:unavailable)
|
||||
expect(sandbox_class.failure_reason).to include("cannot create a rootless sandbox")
|
||||
end
|
||||
|
||||
it "prints bubblewrap sandbox probe failure output" do
|
||||
expect(sandbox_class).to receive(:system_command)
|
||||
.and_return(instance_double(SystemCommand::Result, success?: false,
|
||||
merged_output: "bwrap stdout\nbwrap stderr\n"))
|
||||
expect(sandbox_class).to receive(:opoo).with("bubblewrap test probe failed")
|
||||
|
||||
expect { sandbox_class.available? }
|
||||
.to output("bwrap stdout\nbwrap stderr\n").to_stderr
|
||||
end
|
||||
|
||||
it "does not treat generic bubblewrap sandbox probe failures as nested" do
|
||||
FileUtils.touch fallback_bubblewrap
|
||||
FileUtils.chmod "+x", fallback_bubblewrap
|
||||
|
||||
Reference in New Issue
Block a user