Skip to content

Commit fe59bfb

Browse files
committed
test(e2e): add xfail tests proving #803 privilege ordering bug
Two strict xfail tests that demonstrate the root cause of #803: - PathFd::new() runs as uid 998 after drop_privileges, so root-only paths (mode 700) silently fail and Landlock degrades - When ALL paths fail, best_effort silently drops Landlock entirely These tests will pass after the two-phase Landlock fix (open PathFds as root before drop_privileges, restrict_self after).
1 parent 29c4a29 commit fe59bfb

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

‎e2e/python/test_sandbox_landlock.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
from typing import TYPE_CHECKING
2424

25+
import pytest
26+
2527
from openshell._proto import datamodel_pb2, sandbox_pb2
2628

2729
if TYPE_CHECKING:
@@ -273,3 +275,105 @@ def test_landlock_blocks_user_owned_path_outside_policy(
273275
"Expected Landlock to block write to /home/sandbox despite user ownership. "
274276
f"Got: {write_result.stdout.strip()}"
275277
)
278+
279+
280+
# =============================================================================
281+
# Issue #803: Privilege ordering regression tests
282+
#
283+
# These tests verify that Landlock enforcement works correctly even when
284+
# policy paths are only accessible to root. The root cause of #803 is that
285+
# drop_privileges() runs BEFORE sandbox::apply(), so PathFd::new() runs as
286+
# uid 998 and fails to open root-only paths. In best_effort mode, this
287+
# silently drops Landlock entirely.
288+
#
289+
# The fix is two-phase Landlock: open PathFds as root (before drop_privileges),
290+
# then call restrict_self() after drop_privileges.
291+
# =============================================================================
292+
293+
294+
@pytest.mark.xfail(
295+
reason="Issue #803: drop_privileges runs before Landlock PathFd::new(), "
296+
"so root-only policy paths are silently skipped. "
297+
"Will pass after two-phase Landlock fix.",
298+
strict=True,
299+
)
300+
def test_landlock_enforces_with_only_root_accessible_paths(
301+
sandbox: Callable[..., Sandbox],
302+
) -> None:
303+
"""#803: When ALL policy paths are root-only, Landlock must still enforce.
304+
305+
Creates a policy where every path is only accessible to root (mode 700).
306+
With the current bug, PathFd::new() fails for all paths as uid 998,
307+
the zero-rules safety check fires, best_effort catches it, and the
308+
sandbox runs completely unconfined.
309+
310+
This test creates /sandbox owned by sandbox:sandbox but NOT in the policy.
311+
If Landlock is enforced (even with zero successful paths -> should error
312+
in hard mode), /sandbox should be blocked. If Landlock silently failed,
313+
/sandbox is writable.
314+
315+
We use /root (mode 700) as the only policy path. After the two-phase fix,
316+
PathFd::new("/root") will run as root and succeed, so the ruleset will
317+
have one rule and restrict_self() will enforce it.
318+
"""
319+
root_only_filesystem = sandbox_pb2.FilesystemPolicy(
320+
include_workdir=False,
321+
read_only=["/root"],
322+
read_write=[],
323+
)
324+
spec = datamodel_pb2.SandboxSpec(
325+
policy=_landlock_policy(filesystem=root_only_filesystem)
326+
)
327+
with sandbox(spec=spec, delete_on_exit=True) as sb:
328+
# /sandbox is owned by sandbox:sandbox but NOT in this policy.
329+
# If Landlock is properly applied, /sandbox should be blocked.
330+
# If Landlock silently failed (#803), /sandbox is writable.
331+
result = sb.exec_python(_try_write(), args=("/sandbox",))
332+
assert result.exit_code == 0, result.stderr
333+
assert result.stdout.strip() == "EPERM", (
334+
"Expected Landlock to block /sandbox (not in policy). "
335+
"If this is 'OK', Landlock silently failed — the #803 bug is present. "
336+
f"Got: {result.stdout.strip()}"
337+
)
338+
339+
340+
@pytest.mark.xfail(
341+
reason="Issue #803: drop_privileges runs before Landlock PathFd::new(), "
342+
"so root-only paths are silently skipped by best_effort. "
343+
"Will pass after two-phase Landlock fix.",
344+
strict=True,
345+
)
346+
def test_landlock_no_skipped_paths_for_root_accessible_dirs(
347+
sandbox: Callable[..., Sandbox],
348+
) -> None:
349+
"""#803: Policy paths that root can open should not be skipped.
350+
351+
/root is mode 700 root:root. With the current ordering bug,
352+
PathFd::new("/root") runs as uid 998 and gets EACCES, so the path
353+
is silently skipped. After the fix, it should be opened as root
354+
and added to the ruleset successfully.
355+
356+
We verify by checking that the sandbox logs do NOT contain the
357+
'Skipping inaccessible Landlock path' warning for /root.
358+
"""
359+
filesystem_with_root = sandbox_pb2.FilesystemPolicy(
360+
include_workdir=True,
361+
read_only=["/usr", "/etc", "/root"],
362+
read_write=["/sandbox", "/tmp"],
363+
)
364+
spec = datamodel_pb2.SandboxSpec(
365+
policy=_landlock_policy(filesystem=filesystem_with_root)
366+
)
367+
with sandbox(spec=spec, delete_on_exit=True) as sb:
368+
log_result = sb.exec_python(_read_openshell_log())
369+
assert log_result.exit_code == 0, log_result.stderr
370+
log = log_result.stdout
371+
372+
# After the fix, /root should be successfully opened as root
373+
# and added to the ruleset. The log should NOT show it was skipped.
374+
assert "Skipping inaccessible Landlock path" not in log or "/root" not in log, (
375+
"Landlock skipped /root due to permission denied — "
376+
"this confirms the #803 privilege ordering bug. "
377+
"PathFd::new() ran as uid 998 instead of root. "
378+
f"Log:\n{log[:3000]}"
379+
)

0 commit comments

Comments
 (0)