From 9793aa4aef6ee136efcabb68e2d0614dce027b1a Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Tue, 11 Aug 2026 15:16:56 -0700 Subject: [PATCH] Keep root-installed uv tools importable --- .github/workflows/pip.yml | 23 ++++++++++++++++++ archivebox/config/permissions.py | 41 ++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 750cc6d2..3945b668 100755 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -164,3 +164,26 @@ jobs: VIRTUAL_ENV="$smoke_env" uv run --no-cache --active --no-project --no-sync archivebox version VIRTUAL_ENV="$smoke_env" uv run --no-cache --active --no-project --no-sync archivebox init VIRTUAL_ENV="$smoke_env" uv run --no-cache --active --no-project --no-sync archivebox status + + if [[ "${{ runner.os }}" == "Linux" ]]; then + root_tool_dir="/root/archivebox-package-smoke-${{ matrix.artifact }}-${{ matrix.python }}" + sudo rm -rf "$root_tool_dir" + sudo mkdir -p "$root_tool_dir/data" + + root_install_args=() + for dependency_artifact in "${dependency_artifacts[@]}"; do + root_install_args+=(--with "$dependency_artifact") + done + sudo UV_TOOL_DIR="$root_tool_dir/tools" UV_TOOL_BIN_DIR="$root_tool_dir/bin" \ + uv tool install --no-cache --python "${{ matrix.python }}" \ + "${root_install_args[@]}" "$artifact" + + sudo env PATH="$root_tool_dir/bin:$PATH" \ + bash -c "cd '$root_tool_dir/data' && archivebox init" + sudo stat -c %U "$root_tool_dir/data" | grep -qx archivebox + if sudo find "$root_tool_dir/data" -xdev -user root -print -quit | grep -q .; then + echo "Root-owned files remain in $root_tool_dir/data" >&2 + exit 1 + fi + sudo rm -rf "$root_tool_dir" + fi diff --git a/archivebox/config/permissions.py b/archivebox/config/permissions.py index c374ee09..694c67d4 100644 --- a/archivebox/config/permissions.py +++ b/archivebox/config/permissions.py @@ -221,6 +221,26 @@ def root_parent_can_grant_group_traversal(*, parent_uid: int, parent_gid: int, p return not bool(parent_mode & stat.S_IRWXG) +def grant_archivebox_group_traversal(path: Path) -> None: + """Let the archivebox account traverse private root-owned parents.""" + + if not IS_ROOT or ARCHIVEBOX_ACCOUNT is None: + return + + for parent in path.resolve().parents: + if parent == Path("/"): + continue + parent_stat = parent.stat(follow_symlinks=False) + if root_parent_can_grant_group_traversal( + parent_uid=parent_stat.st_uid, + parent_gid=parent_stat.st_gid, + parent_mode=parent_stat.st_mode, + account_gid=ARCHIVEBOX_ACCOUNT.pw_gid, + ): + os.chown(parent, -1, ARCHIVEBOX_ACCOUNT.pw_gid, follow_symlinks=False) + os.chmod(parent, stat.S_IMODE(parent_stat.st_mode) | stat.S_IXGRP, follow_symlinks=False) + + def handoff_root_owned_data_dir() -> None: account_uid = ARCHIVEBOX_ACCOUNT.pw_uid if ARCHIVEBOX_ACCOUNT is not None else None if not root_should_handoff_data_dir( @@ -235,21 +255,9 @@ def handoff_root_owned_data_dir() -> None: if not handoff_paths: return - # A root user's collection commonly lives below /root, which the archivebox - # account cannot traverse after EUID is dropped. Grant only that group the - # missing execute bit on root-owned parents; never walk collection contents. - for parent in DATA_DIR.resolve().parents: - if parent == Path("/"): - continue - parent_stat = parent.stat(follow_symlinks=False) - if root_parent_can_grant_group_traversal( - parent_uid=parent_stat.st_uid, - parent_gid=parent_stat.st_gid, - parent_mode=parent_stat.st_mode, - account_gid=ARCHIVEBOX_ACCOUNT.pw_gid, - ): - os.chown(parent, -1, ARCHIVEBOX_ACCOUNT.pw_gid, follow_symlinks=False) - os.chmod(parent, stat.S_IMODE(parent_stat.st_mode) | stat.S_IXGRP, follow_symlinks=False) + # Never walk collection contents. This only grants execute-only traversal + # on private parents such as /root so the handed-off data remains reachable. + grant_archivebox_group_traversal(DATA_DIR) for path in handoff_paths: try: @@ -264,6 +272,9 @@ def handoff_root_owned_data_dir() -> None: def drop_privileges(): """If running as root, drop privileges to the data dir owner or archivebox user.""" + # Root-owned uv tools commonly live below /root. Keep the installed package + # importable after dropping EUID without changing ownership of the tool env. + grant_archivebox_group_traversal(Path(__file__)) handoff_root_owned_data_dir() # Always run ArchiveBox as the user that owns the data dir, or as the