mirror of
https://github.com/Homebrew/brew.git
synced 2026-10-28 09:34:42 +08:00
Merge branch 'main' into vulns-identify
This commit is contained in:
8 files changed
+164
-29
No files matched your search
@@ -54,10 +54,19 @@ class Sandbox
|
||||
ACCESS_NET_BIND_UDP = 0x04
|
||||
ACCESS_NET_CONNECT_SEND_UDP = 0x08
|
||||
SCOPE_ABSTRACT_UNIX_SOCKET = 0x01
|
||||
# ABI 3 is the first version that can restrict both cross-directory
|
||||
# renames and truncation:
|
||||
# ABI 2 is the first version that can permit cross-directory renames and
|
||||
# links, which earlier ABIs always deny for sandboxed processes:
|
||||
# https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#previous-limitations
|
||||
MINIMUM_ABI = 3
|
||||
MINIMUM_ABI = 2
|
||||
# File truncation cannot be restricted before ABI 3, letting sandboxed
|
||||
# processes truncate files outside allowed write paths. Every profile
|
||||
# needs write isolation, so unlike `@deny_all_network` there is no
|
||||
# profile-specific check that could gate a warning:
|
||||
# https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#previous-limitations
|
||||
MINIMUM_TRUNCATE_ABI = 3
|
||||
# TCP bind and connect restrictions were added in ABI 4:
|
||||
# https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#network-flags
|
||||
MINIMUM_NETWORK_ABI = 4
|
||||
# UDP controls required to block all network access were added in ABI 10:
|
||||
# https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#network-flags
|
||||
MINIMUM_FULL_NETWORK_ABI = 10
|
||||
@@ -80,8 +89,8 @@ class Sandbox
|
||||
:ACCESS_FS_IOCTL_DEV,
|
||||
:ACCESS_NET_BIND_TCP, :ACCESS_NET_CONNECT_TCP, :ACCESS_NET_BIND_UDP,
|
||||
:ACCESS_NET_CONNECT_SEND_UDP, :SCOPE_ABSTRACT_UNIX_SOCKET, :MINIMUM_ABI,
|
||||
:MINIMUM_FULL_NETWORK_ABI, :WRITE_ACCESS_FS, :FILE_WRITE_ACCESS_FS, :FILE_READ_ACCESS_FS,
|
||||
:READ_ACCESS_FS
|
||||
:MINIMUM_TRUNCATE_ABI, :MINIMUM_NETWORK_ABI, :MINIMUM_FULL_NETWORK_ABI,
|
||||
:WRITE_ACCESS_FS, :FILE_WRITE_ACCESS_FS, :FILE_READ_ACCESS_FS, :READ_ACCESS_FS
|
||||
|
||||
class << self
|
||||
# Landlock cannot restrict chmod, chown, extended attributes or timestamp
|
||||
@@ -321,8 +330,13 @@ class Sandbox
|
||||
end
|
||||
|
||||
if @deny_all_network && abi < MINIMUM_FULL_NETWORK_ABI
|
||||
network_restrictions = if abi >= MINIMUM_NETWORK_ABI
|
||||
"Applying the network restrictions supported by this kernel."
|
||||
else
|
||||
"This kernel cannot restrict network access."
|
||||
end
|
||||
opoo "Landlock ABI #{MINIMUM_FULL_NETWORK_ABI} or later is required to deny all network access; " \
|
||||
"found ABI #{abi}. Applying the network restrictions supported by this kernel."
|
||||
"found ABI #{abi}. #{network_restrictions}"
|
||||
end
|
||||
|
||||
attributes, handled_access_fs, allowed_write_access_fs = ruleset_attributes(abi)
|
||||
@@ -357,7 +371,7 @@ class Sandbox
|
||||
device_path_rules.each do |path, allowed_access|
|
||||
next unless File.exist?(path)
|
||||
|
||||
add_path_rule(ruleset_fd, path, allowed_access)
|
||||
add_path_rule(ruleset_fd, path, allowed_access & handled_access_fs)
|
||||
end
|
||||
|
||||
error_pipe_path = @error_pipe_path
|
||||
@@ -389,8 +403,8 @@ class Sandbox
|
||||
sig { params(abi: Integer).returns([String, Integer, Integer]) }
|
||||
def ruleset_attributes(abi)
|
||||
allowed_access_fs = WRITE_ACCESS_FS
|
||||
allowed_access_fs |= ACCESS_FS_REFER if abi >= 2
|
||||
allowed_access_fs |= ACCESS_FS_TRUNCATE if abi >= 3
|
||||
allowed_access_fs |= ACCESS_FS_REFER if abi >= MINIMUM_ABI
|
||||
allowed_access_fs |= ACCESS_FS_TRUNCATE if abi >= MINIMUM_TRUNCATE_ABI
|
||||
handled_access_fs = allowed_access_fs
|
||||
# IOCTL_DEV is available from ABI 5 and deliberately remains absent from
|
||||
# allowed path rules, denying device ioctls opened inside the sandbox:
|
||||
@@ -402,7 +416,7 @@ class Sandbox
|
||||
# Optional ruleset fields are appended as `__u64` members, so only pass
|
||||
# the prefix needed for features supported by the running kernel.
|
||||
attributes = [handled_access_fs]
|
||||
if @deny_all_network && abi >= 4
|
||||
if @deny_all_network && abi >= MINIMUM_NETWORK_ABI
|
||||
handled_access_net = ACCESS_NET_BIND_TCP | ACCESS_NET_CONNECT_TCP
|
||||
handled_access_net |= ACCESS_NET_BIND_UDP | ACCESS_NET_CONNECT_SEND_UDP if abi >= MINIMUM_FULL_NETWORK_ABI
|
||||
attributes << handled_access_net
|
||||
|
||||
@@ -1162,6 +1162,8 @@ class Tap
|
||||
|
||||
sig { returns(T::Hash[String, T.untyped]) }
|
||||
def to_hash
|
||||
require "trust"
|
||||
|
||||
hash = {
|
||||
"name" => name,
|
||||
"user" => user,
|
||||
|
||||
@@ -39,6 +39,59 @@ RSpec.describe "Bash" do
|
||||
end
|
||||
end
|
||||
|
||||
describe "setup-locale" do
|
||||
it "uses the macOS locale charmap rather than the locale name", :needs_macos do
|
||||
setup_locale = [
|
||||
"/bin/bash", "-c", <<~BASH, "bash", (HOMEBREW_LIBRARY_PATH/"utils/os.sh").to_s
|
||||
source "$1"
|
||||
locale() {
|
||||
[[ "${LC_CTYPE:-${LANG:-}}" == "UTF-8" ]] && printf "UTF-8" || printf "US-ASCII"
|
||||
}
|
||||
setup-locale
|
||||
printf "%s" "${LC_ALL-unset}"
|
||||
BASH
|
||||
]
|
||||
invalid_stdout, invalid_stderr, invalid_status = Open3.capture3(
|
||||
{ "LANG" => "C.utf8", "LC_CTYPE" => nil, "LC_ALL" => nil }, *setup_locale
|
||||
)
|
||||
valid_stdout, valid_stderr, valid_status = Open3.capture3(
|
||||
{ "LANG" => nil, "LC_CTYPE" => "UTF-8", "LC_ALL" => nil }, *setup_locale
|
||||
)
|
||||
|
||||
expect([invalid_stdout, invalid_stderr, invalid_status.success?,
|
||||
valid_stdout, valid_stderr, valid_status.success?])
|
||||
.to eq(["en_US.UTF-8", "", true, "unset", "", true])
|
||||
end
|
||||
|
||||
it "restores filtered Linux locale variables and removes their copies" do
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
{ "LANG" => nil, "LC_CTYPE" => nil, "LC_ALL" => nil },
|
||||
"/bin/bash", "-c", <<~'BASH', "bash", (HOMEBREW_LIBRARY_PATH/"utils/os.sh").to_s
|
||||
source "$1"
|
||||
HOMEBREW_MACOS=
|
||||
HOMEBREW_LANG=C
|
||||
HOMEBREW_LC_CTYPE=C
|
||||
HOMEBREW_LC_ALL=C.UTF-8
|
||||
locale() {
|
||||
if [[ "$1" == "charmap" ]]
|
||||
then
|
||||
[[ "${LC_ALL:-}" == "C.UTF-8" ]] && printf "UTF-8" || printf "US-ASCII"
|
||||
else
|
||||
printf "locale -a called\n" >&2
|
||||
printf "C.UTF-8\n"
|
||||
fi
|
||||
}
|
||||
setup-locale
|
||||
printf "%s\n" "${LANG-unset}" "${LC_CTYPE-unset}" "${LC_ALL-unset}" \
|
||||
"${HOMEBREW_LANG-unset}" "${HOMEBREW_LC_CTYPE-unset}" "${HOMEBREW_LC_ALL-unset}"
|
||||
BASH
|
||||
)
|
||||
|
||||
expect([stdout, stderr, status.success?])
|
||||
.to eq(["C\nC\nC.UTF-8\nunset\nunset\nunset\n", "", true])
|
||||
end
|
||||
end
|
||||
|
||||
describe "every `.sh` file" do
|
||||
it "has valid Bash syntax" do
|
||||
Pathname.glob("#{HOMEBREW_LIBRARY_PATH}/**/*.sh").each do |path|
|
||||
|
||||
@@ -7,10 +7,18 @@ require "cmd/tap-info"
|
||||
RSpec.describe Homebrew::Cmd::TapInfo do
|
||||
it_behaves_like "parseable arguments"
|
||||
|
||||
it "gets information for a given Tap", :integration_test, :needs_network do
|
||||
it "gets information for a given Tap", :integration_test do
|
||||
setup_test_tap
|
||||
|
||||
expect { brew "tap-info", "--json=v1", "--installed" }
|
||||
# Run without the Sorbet runtime so this exercises the same `require` graph as
|
||||
# a real `brew tap-info`, which loads fewer files.
|
||||
brew_env = {
|
||||
"HOMEBREW_SORBET_RUNTIME" => nil,
|
||||
"HOMEBREW_SORBET_RECURSIVE" => nil,
|
||||
"HOMEBREW_NO_GITHUB_API" => "1",
|
||||
}
|
||||
|
||||
expect { brew "tap-info", "--json=v1", "--installed", brew_env }
|
||||
.to output(%r{https://github\.com/Homebrew/homebrew-foo}).to_stdout
|
||||
.and not_to_output.to_stderr
|
||||
.and be_a_success
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "keg"
|
||||
require "macho"
|
||||
|
||||
RSpec.describe Keg do
|
||||
subject(:keg) { described_class.new(keg_path) }
|
||||
|
||||
@@ -63,13 +63,19 @@ RSpec.describe Sandbox::Landlock do
|
||||
expect(described_class.failure_reason).to include("Fiddle")
|
||||
end
|
||||
|
||||
it "returns false for an ABI without truncate restrictions" do
|
||||
it "is available for an ABI without truncate restrictions" do
|
||||
allow(described_class).to receive(:landlock_create_ruleset).with(nil, 0, 1).and_return(2)
|
||||
|
||||
expect(described_class).to be_available
|
||||
end
|
||||
|
||||
it "returns false for an ABI that always denies cross-directory renames" do
|
||||
allow(described_class).to receive(:landlock_create_ruleset).with(nil, 0, 1).and_return(1)
|
||||
|
||||
expect(described_class).not_to be_available
|
||||
expect(described_class.state).to eq(:unsupported_abi)
|
||||
expect(described_class.abi_version).to eq(2)
|
||||
expect(described_class.failure_reason).to eq("Landlock ABI 3 or later is required; found ABI 2.")
|
||||
expect(described_class.abi_version).to eq(1)
|
||||
expect(described_class.failure_reason).to eq("Landlock ABI 2 or later is required; found ABI 1.")
|
||||
end
|
||||
|
||||
it "only raises when explicitly configuring unavailable Landlock" do
|
||||
@@ -116,13 +122,13 @@ RSpec.describe Sandbox::Landlock do
|
||||
allow(File).to receive(:exist?).with("/dev/tty").and_return(false)
|
||||
end
|
||||
|
||||
it "rejects an ABI without complete write restrictions" do
|
||||
allow(described_class).to receive_messages(abi_version: 2,
|
||||
failure_reason: "Landlock ABI 3 or later is required; found ABI 2.")
|
||||
it "rejects an ABI without cross-directory rename restrictions" do
|
||||
allow(described_class).to receive_messages(abi_version: 1,
|
||||
failure_reason: "Landlock ABI 2 or later is required; found ABI 1.")
|
||||
expect(described_class).not_to receive(:landlock_create_ruleset)
|
||||
|
||||
expect { landlock.apply! }
|
||||
.to raise_error(RuntimeError, "Landlock ABI 3 or later is required; found ABI 2.")
|
||||
.to raise_error(RuntimeError, "Landlock ABI 2 or later is required; found ABI 1.")
|
||||
end
|
||||
|
||||
it "restricts writes and network access using the supported ABI" do
|
||||
@@ -319,6 +325,49 @@ RSpec.describe Sandbox::Landlock do
|
||||
expect(attributes.unpack("Q3")).to eq([65_522, 3, 1])
|
||||
end
|
||||
end
|
||||
|
||||
context "with the oldest supported ABI" do
|
||||
before do
|
||||
allow(landlock).to receive(:open_path).and_return(18)
|
||||
allow(described_class).to receive_messages(abi_version: 2, landlock_create_ruleset: 17, landlock_add_rule: 0,
|
||||
set_no_new_privileges: 0, landlock_restrict_self: 0)
|
||||
allow(landlock).to receive(:close_file_descriptor)
|
||||
end
|
||||
|
||||
it "does not warn without network denial" do
|
||||
landlock.command(["true"], tmpdir.to_s)
|
||||
|
||||
expect { landlock.apply! }.not_to output.to_stderr
|
||||
end
|
||||
|
||||
it "warns that network access cannot be restricted" do
|
||||
sandbox.deny_all_network
|
||||
landlock.command(["true"], tmpdir.to_s)
|
||||
|
||||
expect { landlock.apply! }
|
||||
.to output(/found ABI 2\. This kernel cannot restrict network access/).to_stderr
|
||||
end
|
||||
|
||||
it "omits unsupported access rights from device path rules" do
|
||||
allow(File).to receive(:exist?).with("/dev/full").and_return(true)
|
||||
landlock.command(["true"], tmpdir.to_s)
|
||||
|
||||
expect(landlock).to receive(:open_path).with("/dev/full").and_return(19)
|
||||
expect(described_class).to receive(:landlock_add_rule)
|
||||
.with(17, 1, [2, 19].pack("Ql"), 0).and_return(0)
|
||||
|
||||
landlock.apply!
|
||||
end
|
||||
|
||||
it "handles only the supported filesystem access rights" do
|
||||
sandbox.deny_all_network
|
||||
landlock.command(["true"], tmpdir.to_s)
|
||||
|
||||
attributes, = landlock.ruleset_attributes(2)
|
||||
|
||||
expect(attributes.unpack("Q*")).to eq([16_370])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#command" do
|
||||
|
||||
@@ -99,24 +99,30 @@ then
|
||||
fi
|
||||
|
||||
# Force UTF-8 to avoid encoding issues for users with broken locale settings.
|
||||
# The locale is checked by name to avoid forking `locale charmap`. When
|
||||
# bin/brew has filtered the environment no locale variables survive so a UTF-8
|
||||
# one must be set.
|
||||
# Validate the active locale's charmap rather than trusting its name before
|
||||
# selecting a usable fallback.
|
||||
setup-locale() {
|
||||
local utf8_locale_regex='\.([Uu][Tt][Ff]-?8)(@|$)'
|
||||
local locales c_utf_regex en_us_regex utf_regex
|
||||
if [[ "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" =~ ${utf8_locale_regex} ]]
|
||||
if [[ -z "${HOMEBREW_MACOS}" ]]
|
||||
then
|
||||
# The locale is already UTF-8.
|
||||
:
|
||||
elif [[ -n "${HOMEBREW_MACOS}" ]]
|
||||
[[ -z "${HOMEBREW_LANG:-}" ]] || export LANG="${HOMEBREW_LANG}"
|
||||
[[ -z "${HOMEBREW_LC_CTYPE:-}" ]] || export LC_CTYPE="${HOMEBREW_LC_CTYPE}"
|
||||
[[ -z "${HOMEBREW_LC_ALL:-}" ]] || export LC_ALL="${HOMEBREW_LC_ALL}"
|
||||
fi
|
||||
unset HOMEBREW_LANG HOMEBREW_LC_CTYPE HOMEBREW_LC_ALL
|
||||
|
||||
if [[ -n "${HOMEBREW_MACOS}" ]]
|
||||
then
|
||||
export LC_ALL="en_US.UTF-8"
|
||||
if [[ -z "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" ]] || [[ "$(locale charmap)" != "UTF-8" ]]
|
||||
then
|
||||
export LC_ALL="en_US.UTF-8"
|
||||
fi
|
||||
else
|
||||
if ! command -v locale >/dev/null
|
||||
then
|
||||
export LC_ALL=C
|
||||
else
|
||||
elif [[ "$(locale charmap)" != "UTF-8" ]]
|
||||
then
|
||||
locales="$(locale -a)"
|
||||
c_utf_regex='\bC\.(utf8|UTF-8)\b'
|
||||
en_us_regex='\ben_US\.(utf8|UTF-8)\b'
|
||||
|
||||
Reference in new issue
Block a user