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
| State | Meaning | Where the proof is |
|---|---|---|
| fixed | the code refuses, closes or declares it; a test fails if it comes back | tests/unit/test_security.py in the CLI, the test named in the row |
| accepted | it 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 time | the row; the not covered line of run and hello |
| open | needs a person: a GitHub or PyPI setting, or a design decision | Hardening §3; the roadmap |
| designed | the launcher or a component closes it; until then every row is a Boundary for a child and a Setting for whoever applied it | Policies §2 |
The register
| # | Weight | Risk | Where | State | What was done, or why it stays |
|---|---|---|---|---|---|
| SR-01 | High | a path a policy lists as writable was also executable, so a file the child wrote there could run | run.py, landlock.py | fixed 25 Oct 2026 | the write right set excludes execute (landlock.access_write); only the exec list grants it. Test: test_a_writable_path_is_not_executable |
| SR-02 | High | a 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 link | run.py | fixed 25 Oct 2026 | run 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-03 | High | network: "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 likewise | run.py | fixed 25 Oct 2026 | a 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-04 | High | max_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 alone | limits.py, run.py | fixed 25 Oct 2026 | refused 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-05 | High | the 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.yml | fixed 25 Oct 2026, untested until the next push to main | the 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-06 | Medium | hello 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 policy | hello.py | fixed 25 Oct 2026 | the 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-07 | Medium | the 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 host | experiments/browser/build_guest.py | fixed 25 Oct 2026 | every 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-08 | Medium | Unix-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 file | Landlock itself | accepted, declared | nothing 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-09 | Medium | hello 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 earned | hello.py | fixed 25 Oct 2026 | an 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-10 | Medium | a file descriptor opened before the policy stayed usable after it: Landlock binds new opens, not what is already open | run.py | fixed 25 Oct 2026 | the child closes every descriptor above 2 before the policy applies. Test: test_inherited_descriptors_are_closed |
| SR-11 | Medium | the child inherits every environment variable the applying process has, API keys and proxy credentials included; v0 has no row for the environment | the v0 schema | open: a design decision, declared | proposed: 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-12 | Low | the 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.js | fixed 25 Oct 2026 | both 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-13 | Low | a policy file with the same key twice kept the last value silently, so the page showed two values and the hash covered one | policy.py | fixed 25 Oct 2026 | refused on load. Test: test_duplicate_keys_are_refused |
| SR-14 | Low | paths 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 ids | policy.py | fixed 25 Oct 2026 | check 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-15 | Low | policy translate --to kubernetes wrote paths into YAML unquoted, so a path could carry YAML; --to docker could not express a path containing : | translate.py | fixed 25 Oct 2026 | paths are written as JSON strings in the YAML; the docker form notes the : case. Test: test_kubernetes_quotes_paths |
| SR-16 | Low | a boolean passed as a port number | policy.py | fixed 25 Oct 2026 | refused. Test: test_ports_cannot_be_booleans |
| SR-17 | Low | policy hash and check on a missing file ended in a traceback | cli.py | fixed 25 Oct 2026 | exit 2 with the error |
| SR-18 | Low | the bench's local static server resolved .. in a URL above the folder it served | experiments/browser/boot_browser.mjs | fixed 25 Oct 2026 | the resolved path must stay inside out/www. Test: test_static_server_refuses_path_traversal |
| SR-19 | Low | the playground wrote a runtime's output into the page as HTML | experiments/browser/playground.js | fixed 25 Oct 2026 | text nodes only |
| SR-20 | Low | this site loaded the storyboards kit's script without the integrity string the kit publishes for it | explain/storyboards/*/index.html | fixed 25 Oct 2026 | integrity and crossorigin on the tag; the gate checks every third-party script tag carries them |
| SR-21 | Low | the skill told an agent to install from a moving branch | .claude/skills/sharness-hello/SKILL.md | fixed 25 Oct 2026 | install from a named commit |
| SR-22 | Info | SHARNESS_AI_POLICY_HASH in the child's environment is a statement by whoever started it, not something the child can verify | run.py | documented | said in experiments/README.md; a harnessed app may print it, never rely on it |
| SR-23 | Info | what could not be applied (no cgroup, no namespace) was only visible with --verbose | run.py | fixed 25 Oct 2026 | printed on stderr every run |
| SR-24 | Medium | main and dev are unprotected, one account administers both repositories, and a push to main publishes to PyPI | GitHub settings | open: settings | the list on Hardening §3. Nothing in a repository can set this |
| SR-26 | Low | telemetry. 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 off | assets/telemetry.js | accepted by its owner (sherpa), for the editor to confirm (R11); noted here 26 Oct 2026 | the 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-27 | Info | the 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 behalf | experiments/browser/netwatch.js | documented 26 Oct 2026 | the page and the write-up say so; a reader should not read "the page vetoes" as the design's Boundary |
| SR-25 | Low | the 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, declared | what 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.
| # | Risk | Where it is said | State |
|---|---|---|---|
| SD-01 | a 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 kind | Policies §2; every hello run | designed: the launcher above the agent |
| SD-02 | the hash the person supplies is only a boundary if the person obtained it independently of the agent | Threat model §3 | documented; the mockup and the playground let the person compute it |
| SD-03 | v0 names ports, not hosts: any host on an allowed port | Policies §10 | designed: sharness.net |
| SD-04 | UDP is not seen by Landlock; without the namespace DNS resolves | Experiments E03 | designed: the namespace from the launcher; declared every run |
| SD-05 | RLIMIT_AS caps address space, not resident memory; the memory cgroup is the real wall and needs a writable cgroup tree | Containment §1 | documented; declared when the cgroup could not be made |
| SD-06 | an 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 reach | run.py | accepted: the alternative is no namespace at all; the designed launcher runs as a separate uid and does not need it |
| SD-07 | a per-child cgroup that a lingering grandchild keeps alive is left for the kernel to free | limits.py | accepted, low |
| SD-08 | /etc listed whole as readable includes files only root should read, when the child is root | the example policies | documented; narrowing is in the advice |
| SD-09 | the example profiles list /etc as executable, which nothing needs | experiments/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-10 | seccomp, per-component uids, the execute allowlist for the process tree, mutual TLS: the rows that make the containment claim strongest are not built | Containment §4 | designed; E05 to E08 |
| SD-11 | root and the kernel are trusted; a kernel bug from inside a policy is out of scope; a microVM around the harness is the answer | Why | out of scope, by decision |
| SD-12 | the chroot form of E09 runs the guest's binaries on the host kernel as root; it is a test bench, and now a pinned one | Linux in the browser | accepted: 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.