Skip to content

feat(python): ergonomic layer — read_path + 4 presets + dedicated docs page#54

Open
dzerik wants to merge 12 commits into
multikernel:mainfrom
dzerik:follow-up-b-pr3-ergonomic
Open

feat(python): ergonomic layer — read_path + 4 presets + dedicated docs page#54
dzerik wants to merge 12 commits into
multikernel:mainfrom
dzerik:follow-up-b-pr3-ergonomic

Conversation

@dzerik
Copy link
Copy Markdown
Contributor

@dzerik dzerik commented May 21, 2026

Third and final PR of RFC #43. Builds on the already-merged C ABI (#44) and
Python wrapper (#46) — no C ABI or Rust core changes, Python and docs
only.

The minimal audit recipe now fits in five lines:

from sandlock.presets import AuditPathsHandler, COMMON_PATH_SYSCALLS

audit = AuditPathsHandler(callback=lambda path, _ctx: print(f"open {path}"))
sb.run_with_handlers(cmd, [(s, audit) for s in COMMON_PATH_SYSCALLS])

What's added

HandlerCtx.read_path(arg=None, max_len=4096) -> str | None — resolves a
path-bearing syscall argument. With arg=None (default), the path-argument
index is inferred from ctx.syscall_nr via a name-keyed table. Multi-path
syscalls (renameat2, rename, linkat, link, symlinkat, symlink)
and unknown syscalls raise ValueError — pass arg= explicitly in those
cases. Architecture-agnostic: the table is keyed by name and resolved at
first use via sandlock_syscall_nr() from the C ABI.

sandlock.presets — new module, imported explicitly (root surface stays
minimal):

  • COMMON_PATH_SYSCALLS — the nine modern path-bearing syscalls a generic
    file-operation handler is typically registered against.
  • AuditPathsHandler(callback, max_len=4096)on_exception=CONTINUE,
    calls callback(path, ctx) on every intercepted syscall and returns
    Continue.
  • PathDenyHandler(deny: list[str], errno=EPERM, max_len=4096)
    on_exception=KILL, fnmatch patterns; path is None → continue_() (a
    deny-list does not claim "everything else is allowed").
  • PathAllowListHandler(allow: list[str], errno=EACCES, max_len=4096)
    on_exception=KILL, fnmatch patterns; path is None → errno() (an
    allow-list fails closed when the path cannot be classified).
  • LogSyscallsHandler(logger=None)on_exception=CONTINUE, default
    logger is logging.getLogger("sandlock.audit").info.

The deliberate asymmetry between PathDenyHandler and
PathAllowListHandler on path is None is documented in both class-level
docstrings and in the doc page — each class explains the security rationale.

docs/python-handlers.md — new page, single source of truth for the
Python wrapper. The Python section in docs/extension-handlers.md is
reduced to a one-line pointer; the Threading & safety and Ownership rules
subsections are moved verbatim. The Rust/C ABI sections in
extension-handlers.md are untouched.

Out of scope (by RFC #43)

  • Async handler wrappers — handlers stay synchronous per RFC Q1=A.
  • pytest fixtures — pulling pytest knowledge into upstream is unjustified.
  • Helpers for multi-path syscalls beyond read_path(arg=...) — callers
    spell the two args themselves.
  • Auto-registration helpers — COMMON_PATH_SYSCALLS + list comprehension is
    enough.
  • Helpers for non-path arguments (e.g. read_buf) — added if a concrete
    need appears.

Test plan

  • 62 Python tests pass (unit + integration) — up from 35 baseline before
    PR 3.
  • End-to-end test exercises the success-criterion recipe: a real
    python3 -c "..." child opens a unique probe file 3 times, the
    AuditPathsHandler callback observes exactly 3 matching paths.
  • End-to-end test for LogSyscallsHandler against the live trampoline.
  • Destructive verification per load-bearing assertion — for each
    class-level claim (e.g. on_exception=KILL for security handlers,
    path is None asymmetry, default errno, custom errno), a single
    mutation of the production code makes the corresponding test fail.
  • Cross-arch resolution: every test that needs a syscall number uses
    sandlock_syscall_nr() rather than a hard-coded constant.

Commit organization

Seven feature commits (read_path, four presets in order, e2e test, docs)
plus five polish commits addressing the deep self-review:

test(python): skip multi-path test when renameat2 absent on host
test(python): e2e test for LogSyscallsHandler against live trampoline
docs(python): warn read_path() raises ValueError under KILL policy
docs(python): AuditPathsHandler — warn callback can run concurrently
chore(python): clarify _reverse_path_table kernel-absent invariant
docs: dedicated python-handlers.md page; extension-handlers becomes pointer
test(python): end-to-end recipe with AuditPathsHandler + COMMON_PATH_SYSCALLS
feat(python): LogSyscallsHandler
feat(python): PathAllowListHandler
feat(python): PathDenyHandler
feat(python): sandlock.presets — AuditPathsHandler + COMMON_PATH_SYSCALLS
feat(python): HandlerCtx.read_path with name-keyed syscall table

Closes the RFC #43 three-PR chain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant