Harden isolated release builds in Flatpak
This commit is contained in:
@@ -73,6 +73,9 @@ MAX_CATALOG_SOURCE_REPOSITORIES = 128
|
||||
MAX_TRUSTED_RUNTIME_ENTRIES = 10_000
|
||||
MAX_TRUSTED_GIT_ENTRIES = 500_000
|
||||
DURABLE_REMOTE = "origin"
|
||||
FLATPAK_BUILDER_PROXY = Path("/usr/bin/flatpak-spawn")
|
||||
HOST_BUBBLEWRAP = Path("/usr/bin/bwrap")
|
||||
FLATPAK_HOST_BUS = Path("/run/flatpak/bus")
|
||||
|
||||
|
||||
class ReleaseExecutionError(RuntimeError):
|
||||
@@ -1165,16 +1168,34 @@ def _build_selected_wheels_from_sources(
|
||||
tempfile.mkdtemp(prefix=f".{repo}-build-", dir=artifacts)
|
||||
)
|
||||
os.chmod(staging, 0o700, follow_symlinks=False)
|
||||
source_staging = staging / "source"
|
||||
wheel_staging = staging / "out"
|
||||
source_staging.mkdir(mode=0o700)
|
||||
staged = git(
|
||||
frozen_path,
|
||||
"checkout-index",
|
||||
"--all",
|
||||
"--force",
|
||||
f"--prefix={source_staging.absolute()}/",
|
||||
timeout=120,
|
||||
)
|
||||
if staged.returncode != 0:
|
||||
shutil.rmtree(staging)
|
||||
raise ReleaseExecutionBlocked(
|
||||
f"Could not stage the verified tracked source tree for {repo}."
|
||||
)
|
||||
wheel_staging.mkdir(mode=0o700)
|
||||
command = isolated_wheel_builder_command(
|
||||
source=frozen_path,
|
||||
output=staging,
|
||||
source=source_staging,
|
||||
output=wheel_staging,
|
||||
)
|
||||
if _run_isolated_wheel_builder(command) != 0:
|
||||
shutil.rmtree(staging)
|
||||
raise ReleaseExecutionBlocked(
|
||||
f"Server-owned wheel staging failed for {repo}: "
|
||||
"the isolated no-network worker did not complete successfully"
|
||||
)
|
||||
created = _single_isolated_wheel(staging)
|
||||
created = _single_isolated_wheel(wheel_staging)
|
||||
destination = artifacts / created.name
|
||||
if destination.exists() or destination.is_symlink():
|
||||
raise ReleaseExecutionBlocked(
|
||||
@@ -1191,8 +1212,7 @@ def _build_selected_wheels_from_sources(
|
||||
f"Isolated wheel identity does not match selected source {repo}."
|
||||
)
|
||||
os.chmod(destination, 0o600, follow_symlinks=False)
|
||||
created.unlink()
|
||||
staging.rmdir()
|
||||
shutil.rmtree(staging)
|
||||
result[repo] = destination
|
||||
return result
|
||||
|
||||
@@ -1228,12 +1248,12 @@ def isolated_wheel_builder_command(*, source: Path, output: Path) -> tuple[str,
|
||||
"/tmp",
|
||||
"--dir",
|
||||
"/tmp/home",
|
||||
"--ro-bind",
|
||||
str(source.absolute()),
|
||||
"/src",
|
||||
"--bind",
|
||||
str(output.absolute()),
|
||||
"/out",
|
||||
"--bind",
|
||||
str(source.absolute()),
|
||||
"/src",
|
||||
"--chdir",
|
||||
"/tmp",
|
||||
"--clearenv",
|
||||
@@ -1288,12 +1308,10 @@ def isolated_wheel_builder_command(*, source: Path, output: Path) -> tuple[str,
|
||||
|
||||
|
||||
def isolated_builder_launcher() -> tuple[str, ...]:
|
||||
flatpak_spawn = Path("/usr/bin/flatpak-spawn")
|
||||
direct_bwrap = Path("/usr/bin/bwrap")
|
||||
if _trusted_system_executable(flatpak_spawn):
|
||||
return (str(flatpak_spawn), "--host", "/usr/bin/bwrap")
|
||||
if _trusted_system_executable(direct_bwrap):
|
||||
return (str(direct_bwrap),)
|
||||
if _trusted_flatpak_builder_proxy():
|
||||
return (str(FLATPAK_BUILDER_PROXY), "--host", str(HOST_BUBBLEWRAP))
|
||||
if _trusted_system_executable(HOST_BUBBLEWRAP):
|
||||
return (str(HOST_BUBBLEWRAP),)
|
||||
raise ReleaseExecutionBlocked(
|
||||
"Catalog generation requires a trusted bubblewrap worker; no supported "
|
||||
"isolated builder is available."
|
||||
@@ -1313,10 +1331,109 @@ def _trusted_system_executable(path: Path) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _trusted_flatpak_builder_proxy() -> bool:
|
||||
"""Trust only Flatpak's immutable SDK proxy and a root-owned host bwrap."""
|
||||
|
||||
if os.environ.get("container") != "flatpak":
|
||||
return False
|
||||
try:
|
||||
flatpak_info = Path("/.flatpak-info").lstat()
|
||||
flatpak_runtime = FLATPAK_HOST_BUS.parent.lstat()
|
||||
host_bus = FLATPAK_HOST_BUS.lstat()
|
||||
proxy = FLATPAK_BUILDER_PROXY.lstat()
|
||||
ancestors = (Path("/usr"), Path("/usr/bin"))
|
||||
ancestor_metadata = tuple(path.lstat() for path in ancestors)
|
||||
read_only = all(
|
||||
os.statvfs(path).f_flag & os.ST_RDONLY
|
||||
for path in (*ancestors, FLATPAK_BUILDER_PROXY)
|
||||
)
|
||||
except (AttributeError, OSError):
|
||||
return False
|
||||
if not (
|
||||
stat.S_ISREG(flatpak_info.st_mode)
|
||||
and flatpak_info.st_uid == os.getuid()
|
||||
and flatpak_info.st_mode & 0o077 == 0
|
||||
and stat.S_ISDIR(flatpak_runtime.st_mode)
|
||||
and flatpak_runtime.st_uid == os.getuid()
|
||||
and flatpak_runtime.st_mode & 0o077 == 0
|
||||
and stat.S_ISSOCK(host_bus.st_mode)
|
||||
and host_bus.st_uid == os.getuid()
|
||||
and host_bus.st_mode & 0o022 == 0
|
||||
and stat.S_ISREG(proxy.st_mode)
|
||||
and proxy.st_mode & 0o022 == 0
|
||||
and proxy.st_mode & 0o111 != 0
|
||||
and all(
|
||||
stat.S_ISDIR(metadata.st_mode)
|
||||
and metadata.st_mode & 0o022 == 0
|
||||
for metadata in ancestor_metadata
|
||||
)
|
||||
and read_only
|
||||
):
|
||||
return False
|
||||
return _trusted_host_bubblewrap()
|
||||
|
||||
|
||||
def _trusted_host_bubblewrap() -> bool:
|
||||
try:
|
||||
check = subprocess.run( # noqa: S603 - immutable allowlisted proxy.
|
||||
(
|
||||
str(FLATPAK_BUILDER_PROXY),
|
||||
"--host",
|
||||
"/usr/bin/stat",
|
||||
"-c",
|
||||
"%f|%u",
|
||||
str(HOST_BUBBLEWRAP),
|
||||
),
|
||||
cwd=Path("/"),
|
||||
env=_flatpak_proxy_environment(),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
close_fds=True,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
output = check.stdout.strip()
|
||||
if check.returncode != 0 or len(output) > 64:
|
||||
return False
|
||||
try:
|
||||
mode_text, uid_text = output.split("|", maxsplit=1)
|
||||
mode = int(mode_text, 16)
|
||||
uid = int(uid_text, 10)
|
||||
except ValueError:
|
||||
return False
|
||||
return (
|
||||
stat.S_ISREG(mode)
|
||||
and uid == 0
|
||||
and mode & 0o022 == 0
|
||||
and mode & 0o111 != 0
|
||||
)
|
||||
|
||||
|
||||
def _flatpak_proxy_environment() -> dict[str, str]:
|
||||
return {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"DBUS_SESSION_BUS_ADDRESS": f"unix:path={FLATPAK_HOST_BUS}",
|
||||
}
|
||||
|
||||
|
||||
def _run_isolated_wheel_builder(command: tuple[str, ...]) -> int:
|
||||
environment = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
}
|
||||
if command[:1] == (str(FLATPAK_BUILDER_PROXY),):
|
||||
environment = _flatpak_proxy_environment()
|
||||
process = subprocess.Popen( # noqa: S603 - absolute trusted launcher only.
|
||||
command,
|
||||
cwd=Path("/"),
|
||||
env=environment,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
|
||||
Reference in New Issue
Block a user