fix(p8-security): make --copy-as directory ownership airtight; harden tests/logs

- file_ensure_directory_secure() now chowns a final directory it creates under
  --copy-as and fails on error; the symlink parent-creation call site propagates
  it.  The is_dir branch fails when the confined parent cannot be opened under
  --copy-as.  Closes the residual wrong-owner gap for synthesized/symlink
  parent directories.
- file_restore_symlink_metadata() early NULL return is copy-as-aware.
- Preserve errno across the implicit-parent failure cleanup.
- Neutral skip messages (the clamp, not --no-super, may be responsible).
- Daemon copy-as test tolerates the non-root privilege refusal; usage text lists
  --copy-as.
This commit is contained in:
2026-09-12 14:33:42 +02:00
parent b216ed31fb
commit ea0a0e2eaf
5 changed files with 41 additions and 13 deletions
+9 -4
View File
@@ -343,10 +343,13 @@ class TestDaemonRejection:
assert result.returncode != 0
assert _tree_file_count(AUTH_MODULE) == 0
def _assert_ownership_refused(self, daemon, module, flags):
def _assert_ownership_refused(self, daemon, module, flags,
accept=("client-chosen ownership",)):
"""A daemon module without `client owner = yes` refuses every
client-chosen ownership / super-user request at the config handshake,
before any data lands."""
before any data lands. `accept` lists the log phrases that count as the
refusal (a non-root daemon refuses --copy-as earlier, at the privilege
check, so the caller accepts that phrase too)."""
log_path = os.path.join(TEST_DATA_DIR, "fastsyncd.log")
before = os.path.getsize(log_path) if os.path.exists(log_path) else 0
before_files = self._tree_files()
@@ -358,7 +361,7 @@ class TestDaemonRejection:
with open(log_path, "rb") as f:
f.seek(before)
tail = f.read().decode("utf-8", "replace")
assert "client-chosen ownership" in tail, (
assert any(phrase in tail for phrase in accept), (
f"daemon did not log the ownership refusal: {tail[-400:]!r}"
)
@@ -368,7 +371,9 @@ class TestDaemonRejection:
so even a root daemon must not honor an arbitrary client-selected owner
by default. The refusal happens at the config handshake, before any data
lands."""
self._assert_ownership_refused(daemon, "files", ["--copy-as=@65534:@65534"])
self._assert_ownership_refused(
daemon, "files", ["--copy-as=@65534:@65534"],
accept=("client-chosen ownership", "requires a privileged receiver"))
def test_super_refused_by_daemon(self, daemon):
"""An explicit --super is a super-user activity request, so a daemon