Allow root-home collections after privilege drop

This commit is contained in:
Nick Sweeting 2026-08-09 16:09:59 -04:00
parent f264c22d5d
commit c3aa7762cd
No known key found for this signature in database
2 changed files with 53 additions and 3 deletions

View File

@ -4,6 +4,7 @@ import os
import platform
import pwd
import socket
import stat
import subprocess
import sys
from contextlib import contextmanager, suppress
@ -164,6 +165,8 @@ ROOT_HANDOFF_NAMES = (
"index.sqlite3-wal",
"archive",
"cache",
"custom_plugins",
"custom_templates",
"lib",
"logs",
"personas",
@ -208,6 +211,16 @@ def root_should_handoff_data_dir(
return is_root and account_uid is not None and (not data_dir_owner_exists or data_dir_uid in (0, account_uid))
def root_parent_can_grant_group_traversal(*, parent_uid: int, parent_gid: int, parent_mode: int, account_gid: int) -> bool:
"""Return whether adding archivebox traversal preserves existing parent access."""
if parent_uid != 0 or parent_mode & stat.S_IXOTH:
return False
if parent_gid == account_gid:
return not bool(parent_mode & stat.S_IXGRP)
return not bool(parent_mode & stat.S_IRWXG)
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(
@ -218,7 +231,27 @@ def handoff_root_owned_data_dir() -> None:
):
return
for path in root_data_dir_handoff_paths(DATA_DIR, sys.argv):
handoff_paths = root_data_dir_handoff_paths(DATA_DIR, sys.argv)
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)
for path in handoff_paths:
try:
os.chown(path, ARCHIVEBOX_ACCOUNT.pw_uid, ARCHIVEBOX_ACCOUNT.pw_gid, follow_symlinks=False)
except (FileNotFoundError, PermissionError):

View File

@ -1,6 +1,11 @@
from pathlib import Path
from archivebox.config.permissions import is_root_identity, root_should_handoff_data_dir, select_archivebox_user
from archivebox.config.permissions import (
is_root_identity,
root_parent_can_grant_group_traversal,
root_should_handoff_data_dir,
select_archivebox_user,
)
def test_root_identity_includes_real_or_effective_root():
@ -115,16 +120,20 @@ def test_existing_collection_handoff_is_bounded_to_known_top_level_paths(tmp_pat
database = tmp_path / "index.sqlite3"
archive = tmp_path / "archive"
custom_plugins = tmp_path / "custom_plugins"
custom_templates = tmp_path / "custom_templates"
nested = archive / "large-existing-snapshot"
errors_log = tmp_path / "logs" / "errors.log"
database.touch()
nested.mkdir(parents=True)
custom_plugins.mkdir()
custom_templates.mkdir()
errors_log.parent.mkdir()
errors_log.touch()
paths = root_data_dir_handoff_paths(tmp_path, ["archivebox", "status"])
assert paths == (tmp_path, database, archive, errors_log.parent, errors_log)
assert paths == (tmp_path, database, archive, custom_plugins, custom_templates, errors_log.parent, errors_log)
assert nested not in paths
assert all(path == tmp_path or path.parent in (tmp_path, errors_log.parent) for path in paths)
@ -139,3 +148,11 @@ def test_root_handoff_never_selects_filesystem_root():
from archivebox.config.permissions import root_data_dir_handoff_paths
assert root_data_dir_handoff_paths(Path("/"), ["archivebox", "init"]) == ()
def test_root_private_parent_grants_only_archivebox_group_traversal():
assert root_parent_can_grant_group_traversal(parent_uid=0, parent_gid=0, parent_mode=0o700, account_gid=911)
assert not root_parent_can_grant_group_traversal(parent_uid=0, parent_gid=0, parent_mode=0o701, account_gid=911)
assert not root_parent_can_grant_group_traversal(parent_uid=0, parent_gid=911, parent_mode=0o710, account_gid=911)
assert not root_parent_can_grant_group_traversal(parent_uid=0, parent_gid=100, parent_mode=0o750, account_gid=911)
assert not root_parent_can_grant_group_traversal(parent_uid=1000, parent_gid=1000, parent_mode=0o700, account_gid=911)