Merge branch 'main' into vulns-identify

This commit is contained in:
Andrew Nesbitt authored and GitHub committed 2026-07-28 15:28:27 +01:00
commit 119bb74bbd
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
+2
View File
@@ -1162,6 +1162,8 @@ class Tap
sig { returns(T::Hash[String, T.untyped]) }
def to_hash
require "trust"
hash = {
"name" => name,
"user" => user,
+53
View File
@@ -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|
+10 -2
View File
@@ -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
+1
View File
@@ -2,6 +2,7 @@
# frozen_string_literal: true
require "keg"
require "macho"
RSpec.describe Keg do
subject(:keg) { described_class.new(keg_path) }
+56 -7
View File
@@ -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
+16 -10
View File
@@ -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'
+2
View File
@@ -235,6 +235,8 @@ USED_BY_HOMEBREW_VARS=(
GOBIN
GOPATH
LANG
LC_ALL
LC_CTYPE
NODENV_ROOT
PATH
PYENV_ROOT