sharness.ai / security / known risks · this page as markdown

Known risks

The register, as of 25 Oct 2026. One row per risk the project knows about, whatever its state. Fixed means on the CLI's dev branch with a test that pins it; accepted means it stays, with the reason written down; open means a decision or a setting is still needed; designed means the launcher or a component will close it and nothing does today. The weight is the reviewer's judgement of what it would cost someone relying on the code as documented. A risk that is accepted is as much a part of this page as one that is fixed: the aim is that an outside review finds nothing here it has to tell the project.

How to read the states

StateMeaningWhere the proof is
fixedthe code refuses, closes or declares it; a test fails if it comes backtests/unit/test_security.py in the CLI, the test named in the row
acceptedit stays, because fixing it costs more than it buys at this stage, or because nothing below the launcher can close it; the reason is in the row and the code says so at run timethe row; the not covered line of run and hello
openneeds a person: a GitHub or PyPI setting, or a design decisionHardening §3; the roadmap
designedthe launcher or a component closes it; until then every row is a Boundary for a child and a Setting for whoever applied itPolicies §2

The register

#WeightRiskWhereStateWhat was done, or why it stays
SR-01Higha path a policy lists as writable was also executable, so a file the child wrote there could runrun.py, landlock.pyfixed 25 Oct 2026the write right set excludes execute (landlock.access_write); only the exec list grants it. Test: test_a_writable_path_is_not_executable
SR-02Higha writable path that is a symlink binds the rule to its target, so the policy's page grants less than the kernel does; a cloned repository can carry such a linkrun.pyfixed 25 Oct 2026run refuses before forking when a writable path resolves elsewhere, and the child re-checks on the descriptor the rule binds to. Read and exec paths may still resolve elsewhere (/lib64 on a merged-/usr system) and are reported. Test: test_a_symlinked_writable_path_is_refused_before_anything_runs
SR-03Highnetwork: "none" ran with the network open when no namespace could be made and the kernel's Landlock had no TCP rules (ABI 1 to 3); port lists likewiserun.pyfixed 25 Oct 2026a row that cannot be enforced is a refusal (exit 126, nothing ran); --best-effort runs anyway and says so on stderr. Test: test_network_none_without_namespace_or_tcp_rules_is_refused
SR-04Highmax_pids did nothing for a root child without a pids cgroup, because the kernel exempts uid 0 from RLIMIT_NPROC; the measured result came from the cgroup alonelimits.py, run.pyfixed 25 Oct 2026refused unless a pids cgroup was made, or --best-effort. limits.nproc_binds says when the rlimit counts; the cgroup v2 controllers are enabled before use. Test: test_max_pids_as_root_without_a_cgroup_is_refused
SR-05Highthe release pipeline ran code from outside the project in the job that held the PyPI publishing token: a composite action that fetched an installer over the network and installed unpinned packages.github/workflows/workflow.ymlfixed 25 Oct 2026, untested until the next push to mainthe bump is a few lines of shell in the workflow; build and publish are separate jobs; only GitHub's and pypa's actions, pinned to commits; PEP 740 attestations. A workflow_dispatch dry run builds and stops. Supply chain §3
SR-06Mediumhello started the runner with python -m, which puts the current directory first on the import path, so a sharness_ai/ folder in the workspace would have been imported by the process that applies the policy, before any policyhello.pyfixed 25 Oct 2026the runner uses -P -s (no cwd, no user site); the probes already used -I; the guest's wrapper does the same. Test: test_a_planted_package_in_the_workspace_is_not_imported
SR-07Mediumthe browser bench downloaded the guest's packages and BIOS without checking them, the BIOS from a moving branch; the chroot form runs those binaries as root on the hostexperiments/browser/build_guest.pyfixed 25 Oct 2026every download pinned by sha256 and refused on mismatch, cached copies re-verified every build, the BIOS fetched from one commit, https only. Test: test_guest_build_pins_every_download
SR-08MediumUnix-domain sockets by path are not covered. Landlock has no rule for connecting to a pathname socket and an empty network namespace does not stop it either, so under network: "none" a child can still reach a Docker daemon, an X server or a local MCP server by its socket fileLandlock itselfaccepted, declarednothing below the launcher closes it; a mount namespace (hide the sockets) or a separate uid (no permission on them) will. run says it on stderr every time, policy check says it in its advice, hello prints it as not covered. Test: test_unix_socket_gap_is_declared
SR-09Mediumhello counted any failure of a probe as the policy's refusal, so an unreachable network, a read-only filesystem or a small machine produced a pass the policy had not earnedhello.pyfixed 25 Oct 2026an unambiguous refusal (EACCES, MemoryError) counts; an ambiguous one is checked against a control run without the policy and reported inconclusive (?) when the control fails too. The control runs only when needed
SR-10Mediuma file descriptor opened before the policy stayed usable after it: Landlock binds new opens, not what is already openrun.pyfixed 25 Oct 2026the child closes every descriptor above 2 before the policy applies. Test: test_inherited_descriptors_are_closed
SR-11Mediumthe child inherits every environment variable the applying process has, API keys and proxy credentials included; v0 has no row for the environmentthe v0 schemaopen: a design decision, declaredproposed: process.env with "inherit", "clean" or an allowlist of names, in the next policy version (a new key leaves every existing hash unchanged; old versions refuse the unknown key, which is right). Until then run says it on stderr every time and policy check in its advice. The designed end state removes the need: sharness.llm and sharness.kms hold the secrets, so no other process needs them in its environment
SR-12Lowthe Python and the browser hashed differently for integers above 2^53 (JavaScript rounds them) and for strings with lone surrogates (Python cannot encode them and raised a traceback)policy.py, playground.jsfixed 25 Oct 2026both refused in canonical(); the examples are hashed by the JavaScript canonicaliser under test. Tests: test_integers_beyond_2_53_are_refused, test_lone_surrogates_are_refused_not_a_traceback, test_the_browser_canonicaliser_agrees_on_the_examples
SR-13Lowa policy file with the same key twice kept the last value silently, so the page showed two values and the hash covered onepolicy.pyfixed 25 Oct 2026refused on load. Test: test_duplicate_keys_are_refused
SR-14Lowpaths with .., ., a trailing slash or a doubled slash were accepted, so a path could read as one place and bind another; control characters were accepted in paths and idspolicy.pyfixed 25 Oct 2026check refuses paths that are not normalised or not printable, and bounds the id. Tests: test_paths_must_be_normalised, test_paths_with_control_characters_are_refused, test_id_is_bounded_and_printable
SR-15Lowpolicy translate --to kubernetes wrote paths into YAML unquoted, so a path could carry YAML; --to docker could not express a path containing :translate.pyfixed 25 Oct 2026paths are written as JSON strings in the YAML; the docker form notes the : case. Test: test_kubernetes_quotes_paths
SR-16Lowa boolean passed as a port numberpolicy.pyfixed 25 Oct 2026refused. Test: test_ports_cannot_be_booleans
SR-17Lowpolicy hash and check on a missing file ended in a tracebackcli.pyfixed 25 Oct 2026exit 2 with the error
SR-18Lowthe bench's local static server resolved .. in a URL above the folder it servedexperiments/browser/boot_browser.mjsfixed 25 Oct 2026the resolved path must stay inside out/www. Test: test_static_server_refuses_path_traversal
SR-19Lowthe playground wrote a runtime's output into the page as HTMLexperiments/browser/playground.jsfixed 25 Oct 2026text nodes only
SR-20Lowthis site loaded the storyboards kit's script without the integrity string the kit publishes for itexplain/storyboards/*/index.htmlfixed 25 Oct 2026integrity and crossorigin on the tag; the gate checks every third-party script tag carries them
SR-21Lowthe skill told an agent to install from a moving branch.claude/skills/sharness-hello/SKILL.mdfixed 25 Oct 2026install from a named commit
SR-22InfoSHARNESS_AI_POLICY_HASH in the child's environment is a statement by whoever started it, not something the child can verifyrun.pydocumentedsaid in experiments/README.md; a harnessed app may print it, never rely on it
SR-23Infowhat could not be applied (no cgroup, no namespace) was only visible with --verboserun.pyfixed 25 Oct 2026printed on stderr every run
SR-24Mediummain and dev are unprotected, one account administers both repositories, and a push to main publishes to PyPIGitHub settingsopen: settingsthe list on Hardening §3. Nothing in a repository can set this
SR-26Lowtelemetry. Since v0.5.11 every page runs assets/telemetry.js: one encrypted event per page per session to a write-only lane, and a geolocation lookup the reader's browser makes to a third party (ipapi.co, then ipwho.is), which therefore sees the reader's IP address. The lane's token and key are public, so rows can be forged or flooded. Both are declared on Telemetry; Do Not Track and Global Privacy Control switch it offassets/telemetry.jsaccepted by its owner (sherpa), for the editor to confirm (R11); noted here 26 Oct 2026the register's view: the lookup is the one place a reader's address leaves for a party the project does not run; dropping region and city, or making the lookup opt-in, would remove it at the cost of the country counts. Recorded, not changed: the row is the sherpa's
SR-27Infothe bench's page-side network. Since the CLI's dev of 25 Oct 2026 the guest in the tab is on a network the page provides (netwatch.js): the guest speaks plain HTTP to the page, the page upgrades to TLS and applies a hostname allowlist outside the guest. It demonstrates sharness.net's job; it is not the component, and the guest cannot verify the TLS the page speaks on its behalfexperiments/browser/netwatch.jsdocumented 26 Oct 2026the page and the write-up say so; a reader should not read "the page vetoes" as the design's Boundary
SR-25Lowthe playground loads Pyodide from jsDelivr with no integrity string (Pyodide fetches its own dozens of files, so one string cannot cover it)code/playground/accepted, declaredwhat a compromised copy could do: show a wrong hash or a wrong result in the Pyodide tab of this one page. What it could not do: reach anything of the reader's beyond that tab, or touch the Linux tab, which is served from this site. The three-way hash exists so one lying runtime is visible. Self-hosting Pyodide at an immutable path is the fix if the risk is ever judged worth 10 MB per release; see Supply chain §4

Risks the design carries, not the code

These are in the design pages already; they are repeated here so the register is one list.

#RiskWhere it is saidState
SD-01a policy a process applies to its own children is a Setting for that process: it could skip it. Every measured row today is of this kindPolicies §2; every hello rundesigned: the launcher above the agent
SD-02the hash the person supplies is only a boundary if the person obtained it independently of the agentThreat model §3documented; the mockup and the playground let the person compute it
SD-03v0 names ports, not hosts: any host on an allowed portPolicies §10designed: sharness.net
SD-04UDP is not seen by Landlock; without the namespace DNS resolvesExperiments E03designed: the namespace from the launcher; declared every run
SD-05RLIMIT_AS caps address space, not resident memory; the memory cgroup is the real wall and needs a writable cgroup treeContainment §1documented; declared when the cgroup could not be made
SD-06an unprivileged user namespace, used to get an empty network namespace without root, gives the child capabilities inside that namespace and widens the kernel surface it can reachrun.pyaccepted: the alternative is no namespace at all; the designed launcher runs as a separate uid and does not need it
SD-07a per-child cgroup that a lingering grandchild keeps alive is left for the kernel to freelimits.pyaccepted, low
SD-08/etc listed whole as readable includes files only root should read, when the child is rootthe example policiesdocumented; narrowing is in the advice
SD-09the example profiles list /etc as executable, which nothing needsexperiments/policies/accepted for now: changing a profile changes its pinned hash, quoted on this site; policy check advises against it; the next profile revision drops it
SD-10seccomp, per-component uids, the execute allowlist for the process tree, mutual TLS: the rows that make the containment claim strongest are not builtContainment §4designed; E05 to E08
SD-11root and the kernel are trusted; a kernel bug from inside a policy is out of scope; a microVM around the harness is the answerWhyout of scope, by decision
SD-12the chroot form of E09 runs the guest's binaries on the host kernel as root; it is a test bench, and now a pinned oneLinux in the browseraccepted: documented as needing root; the pins are SR-07

Adding to this page

A new finding gets the next SR- number and a row the day it is found, in whatever state it is in. A fixed row keeps its number for ever; nothing is deleted. The CLI's SECURITY.md points here, and every review on Reviews cites rows here.