diff --git a/SECURITY_FIXES_FABLE5.1_IGNORED.md b/SECURITY_FIXES_FABLE5.1_IGNORED.md index 5f44887..5bf485d 100644 --- a/SECURITY_FIXES_FABLE5.1_IGNORED.md +++ b/SECURITY_FIXES_FABLE5.1_IGNORED.md @@ -1,4 +1,6 @@ -# Security findings not fixed (Fable 5.1 pass, 2026-09-02) +# Retained security decisions and residual risks + +Reassessed after the 2026-09-12 security pass. This file records every finding from `SECURITY_AUDIT_2026-08-28_GEMINI-3.7-HIGH_TEAMWORK.md` and @@ -11,11 +13,11 @@ bundled catalog's BLAKE3 authority, and a hostile participant can be unplugged. Findings that only make sense against an internet-facing, multi-tenant threat model are noted as such. -Fixed findings are documented in the individual commits on this branch and are -not repeated here. After a cross-review of the three parallel fix branches, -NET-05, Codex [7] and Codex [12] were ported from the second branch and moved -from "not fixed" to "fixed" (see the commits touching `justfile`, -`state_paths.rs` and the bounded wire collections in `lanspread-proto`). +The reassessment found that several availability findings had been dismissed too +aggressively. They are now fixed with finite aggregate, per-origin, retry, and +UI publication budgets. Entries retained below require trusted catalog input, an +explicit local operation, or a same-machine writer; they are not remote LAN-peer +integrity bypasses under this product model. ## Gemini audit @@ -40,59 +42,60 @@ By design; see NET-01. ### EXP2-SEC-01 — Post-unpack manifest verification (partially fixed) - Fixed: symlink/reparse-point audit before promotion, plus `unrar -ol-`. -- Not fixed: hashing every extracted file against the catalog manifest. The - `.eti` archives are BLAKE3-verified at download time, so their extracted - output is only as trustworthy as the catalog operator, who is trusted by - definition. Stream Install already verifies extracted output because it - receives extracted bytes from a peer; the ordinary path extracts locally from - verified archives. +- Retained: hashing every ordinary extracted file against the catalog manifest. + Ordinary Install intentionally consumes every current regular root `.eti`; + acceptance scenario S33 replaces a downloaded archive and requires those new + local bytes to be installed. Stream Install remains exact: it verifies every + path, kind, size, and BLAKE3 digest before promotion. ### EXP2-SEC-02 — RAR bomb / disk exhaustion -Same reasoning: a decompression bomb would have to be published in the catalog. -Games are large by nature (the audit's own numbers are hundreds of gigabytes for -a party catalog), and a preflight would need a new dependency for free-space -queries plus a per-game expected size that the ordinary manifest does not carry. -Disk-full during extraction already fails the transaction and rolls staging -back. +Ordinary archives may be locally replaced or added for S33, so a bomb need not +come from the catalog. The retained boundary is an explicitly selected local +root plus a user-started install. Games are large by nature and the ordinary +manifest does not define expected expanded bytes. Disk-full or extractor failure +rolls the transaction back, but decompressed bytes, CPU time, and free space are +not preflighted. ### EXP2-SEC-05 — Pre-verification chunk writes Uncommitted downloads are never published: `version.ini` is written only after every chunk verifies, and the ownership journal plus recovery path handle a -crash mid-download. Buffering 4 MiB per in-flight chunk in memory to avoid -writing bytes that are about to be verified anyway would raise memory use for no -integrity gain. +crash mid-download. Ordinary chunks can be 128 MiB; buffering complete chunks in +memory to avoid confined temporary writes would materially raise memory use +without changing publication integrity. ### EXP2-SEC-07 — Ambient filesystem calls in `stream_install.rs` -Architectural parity request. The staging directory is created empty and owned -by the transaction, every path is a validated `CanonicalCatalogPath` resolved by -`validate_relative_path` (which now applies the full portable component rules -and rejects symlink escapes). Refactoring the receiver onto capability handles -is a large change with no concrete exploit behind it. +Remote paths, kinds, sizes, and digests are catalog-owned, and the completed +staging tree is audited before transactional promotion. The receiver still uses +ambient path calls inside its owned staging directory. Exploiting that gap +requires a concurrent same-machine writer able to mutate the selected root; it +is retained as defense-in-depth work rather than a remote peer bypass. ### SEC-IPC-01 — Elevated execution of game scripts (partially fixed) -- Fixed: `sanitize_username` now also strips `& | < > ^` so a batch script's - textual `%~4` expansion cannot chain commands. -- Not fixed: running the scripts elevated, and re-hashing them at launch time. - Elevated setup is the feature: the shared games need registry entries, - redistributables and firewall rules installed. The scripts ship inside catalog - archives that are BLAKE3-verified before installation, so "untrusted P2P batch - scripts" mischaracterises them. Re-verifying the file at launch would only - defend against a local actor who can already write to the user's own game - directory, i.e. who already runs code as that user. +- Fixed: launch-time trust binding. A fixed-role elevated worker reloads the + bundled authority, matches the embedded `ContentId`, verifies exact size and + BLAKE3 from a no-follow locked handle, resolves System32 `cmd.exe`, and keeps + path locks alive in the command process. Changed, unmanifested, + reparse-backed, markerless, and streamed-only scripts fail closed. +- Retained: all three scripts still run elevated. This is required for registry + entries, redistributables, and firewall rules. Native Windows lock-transfer + behavior is not proven in this Linux checkout, and a trusted script may still + invoke mutable secondary executables or configuration. - The proposed `[A-Za-z0-9_-]` username allowlist was rejected because it would mangle ordinary names (spaces, umlauts). ### SEC-IPC-04 — Sandboxing `unrar` (partially fixed) - Fixed: `-ol-` and the post-extraction link audit. -- Not fixed: Landlock/AppContainer style OS sandboxing of the sidecar. The input - is a catalog-verified archive; a process sandbox for it is not proportionate - to a LAN launcher. Note that the `-sl-` flag the audit recommends does not - exist (`-sl` is a size filter). +- Not fixed: Landlock/AppContainer style OS sandboxing of the sidecar. Ordinary + S33 input is not necessarily catalog-identical, but it is selected local input + rather than peer-controlled bytes. `unrar` runs with `-ol-`, bounded output + capture and cancellation cleanup, a staging link audit, and bounded remote + Stream Install provider admission. Note that the `-sl-` flag the audit + recommends does not exist (`-sl` is a size filter). ### SEC-IPC-05 — `allow-create-webview-window` capability @@ -109,63 +112,80 @@ for this app. ## Codex scan (`security-report/report.md` numbering) -### [1] Anonymous LAN requesters can monopolize global pools +### [1] Anonymous LAN requesters can monopolize global pools (fixed 2026-09-12) -Global pools (64 connections, 64 control streams, 48 bulk transfers) are the -intended fairness model for a room of peers; per-origin quotas conflict with the -anonymous requester design, and legitimate use is bursty (ten people pulling the -newest game from one seeder). Discovery now has a per-source-IP budget, which -covers the cheapest flood. A participant who saturates a seeder at a LAN party -is visible and can be dealt with in person. +Requester anonymity remains, but resource anonymity does not. One observed IP +may start 8 handshakes per 3 seconds and retain at most 8 connections, 8 of 16 +control tasks, 8 of 48 bulk transfers, 1 of 2 Stream Install providers, and 8 +MiB of the shared 32 MiB response buffer. Raw and Stream Install sends have +absolute deadlines, so incremental progress cannot renew those slots +indefinitely. ### [3] Ordinary install extracts uncatalogued root archives (partially fixed) - Fixed: link audit before promotion. - Not fixed: restricting extraction to the catalog's archive set and verifying - extracted output. The scenario requires the local user to place an extra - `.eti` in their own game directory. See EXP2-SEC-01. + extracted output. This is the behavior exercised by S33: the local user + replaces or adds `.eti` files and expects Ordinary Install to consume the + current set. See EXP2-SEC-01. -### [4] Sybil sources can multiply one chunk deadline into a multi-hour loop +### [4] Sybil retry amplification (fixed) -The retry loop tries each distinct peer identity once per chunk by design, -cancellation is always available, and a real party LAN has a handful of peers, -not 64 hostile identities. A global attempt budget would make large legitimate -downloads give up earlier under flaky Wi-Fi. +One failed chunk may try at most eight distinct peer IDs at distinct endpoint +IPs. All retries share one nonrenewable 20-minute window created after the +initial failure; each in-flight attempt retains the earlier ten-minute limit. +Stream Install separately permits four endpoint IPs under one catalog-sized +total deadline. -### [5] One peer can force continuous rendering of thousands of active calls +### [5] Unbounded active-call rendering (fixed) -The 4096-event author bound is a wire bound; a peer that publishes thousands of -simultaneous calls degrades the ticker for everyone but this is a prank that the -room can see and stop. Semantic caps on open calls per author would be a product -decision (how many calls may one person host?) rather than a security fix. +One remote author may retain at most 128 creator roots. Frontend projection is +limited to 128 nominations and the latest 256 messages, and messages are sorted +once rather than after every append. -### [8] Sybil peers can exhaust aggregate state and unbounded UI publication +### [8] Aggregate state and UI publication (fixed) -Worst case is 64 authenticated identities each holding a 4 MiB author slice, -i.e. a few hundred MiB retained by a desktop process, and only while a hostile -host keeps dozens of identities alive on the party LAN. Aggregate byte budgets -and bounded latest-value UI channels are a redesign of the state layer, not a -fix, and are not warranted by this deployment model. +Committed state now has eight identities per endpoint IP, 16,384 aggregate +library rows, and 16,384 aggregate remote Call-to-Play events. Rejected +revisions retain watermarks instead of being pulled repeatedly. Each heavy UI +view keeps one queued snapshot and one replaceable pending snapshot while small +lifecycle events remain lossless and FIFO. -### [10] Mutable game scripts are launched elevated without trust binding +### [10] Elevated mutable-script trust binding (fixed) -See SEC-IPC-01. +See SEC-IPC-01. Required elevation remains; mutable-path trust binding does not. -### [11] Selected game directories can trigger unbounded monitoring work +### [11] Unbounded selected-root monitoring (fixed) -The monitored directory is the one the user selected on their own machine. -Budgeting traversal of the user's own files against the user is out of scope. +Index reads, games, archive fingerprints, recursive entries, depth, bytes, +elapsed scan time, `version.ini`, launch-settings traversal, and INI reads now +have explicit per-file and aggregate ceilings. Monitor failures retain the +previous complete snapshot and back off exponentially per selected root. -### [13] Catalog preflight can read outside the package root +### [13] Catalog preflight can read outside the package root (fixed 2026-09-12) -Publisher tooling run by the catalog operator over their own package tree; the -disclosed bytes are a bounded, trimmed `version.ini` echoed in the operator's -own error message. +Full-selection preflight now rejects a linked or non-directory package root +before the bounded, non-link `version.ini` read, and mismatch diagnostics omit +package-controlled observed contents. ## Summary -| Status | Findings | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| Fixed | NET-02, NET-03, NET-04, NET-05, EXP2-SEC-03, EXP2-SEC-04, EXP2-SEC-06, SEC-IPC-02, SEC-IPC-03, SEC-DB-01, Codex [2], [6], [7], [9], [12], [14], [15] | -| Partially fixed | NET-01, EXP2-SEC-01, SEC-IPC-01, SEC-IPC-04, Codex [3], [10] | -| Not fixed | NET-06, EXP2-SEC-02, EXP2-SEC-05, EXP2-SEC-07, SEC-IPC-05, SEC-FE-01, Codex [1], [4], [5], [8], [11], [13] | +| Status | Findings | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Fixed | NET-02, NET-03, NET-04, NET-05, EXP2-SEC-03, EXP2-SEC-04, EXP2-SEC-06, SEC-IPC-02, SEC-IPC-03, SEC-DB-01, Codex [1], [2], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15] | +| Partially fixed | NET-01, EXP2-SEC-01, SEC-IPC-01, SEC-IPC-04, Codex [3] | +| Accepted/retained | NET-06, EXP2-SEC-02, EXP2-SEC-05, EXP2-SEC-07, SEC-IPC-05, SEC-FE-01 | + +The retained items have these practical boundaries: + +| Retained risk | Boundary | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Anonymous membership and metadata/content access | Intended LAN behavior; responder pinning, exact catalog identity, and active per-origin quotas protect integrity and availability. | +| Ordinary archive and extracted-output authority | Explicit local install and S33 mutation; links are blocked, but the current archive set, expanded bytes, and decompression cost are local user authority. | +| Pre-verification temporary writes | Confined ownership journals and a final sentinel prevent publication; failed chunks can still consume bounded temporary disk space. | +| Ambient Stream Install staging calls | Remote paths, sizes, and digests are exact; a concurrent same-machine root mutation remains the escape precondition. | +| Elevated execution | Only exact catalog-authorized scripts launch, but those trusted scripts intentionally receive administrator authority and may invoke mutable dependencies. | +| External `unrar` | No OS sandbox; local mutated archives remain user-level extractor input. | +| Main-window companion creation | Used for fixed local log windows; CSP and no known renderer injection path limit reachability. | +| User regex CPU | The pattern is local input; remote-controlled log lines can only supply the haystack. | +| Distributed saturation | One IP cannot monopolize active pools; cooperating hosts on several IPs can still reach the finite global limits. | diff --git a/SECURITY_REVIEW_2026-09-12.md b/SECURITY_REVIEW_2026-09-12.md new file mode 100644 index 0000000..0998892 --- /dev/null +++ b/SECURITY_REVIEW_2026-09-12.md @@ -0,0 +1,114 @@ +# Security review — 2026-09-12 + +## Scope and threat boundary + +This pass reviewed the full Lanspread workspace after the Fable 5.1 remediation. +The product is a desktop launcher for LAN parties, not an Internet-facing +multi-tenant service. Remote LAN peers and their network traffic are untrusted. +The bundled catalog is trusted authority. A user-selected local game root and +explicit local install actions remain user authority. The three Windows launch +scripts intentionally run elevated. + +## Reassessment of previously ignored findings + +The earlier file dismissed several finite-resource findings too broadly. This +pass fixed anonymous pool monopolization, retry multiplication, oversized remote +state, heavy UI publication, selected-root ingestion, elevated-script trust +binding, and catalog-preflight traversal. The remaining accepted decisions and +their exact limits are recorded in `SECURITY_FIXES_FABLE5.1_IGNORED.md`. + +The main retained decisions are: + +- anonymous LAN membership, metadata browsing, and catalog-content downloads; +- Ordinary Install consuming the current root `.eti` set, including the S33 + local-mutation workflow; +- transactional pre-verification disk writes; +- no OS sandbox around `unrar`; +- local-user regex CPU cost and the fixed companion-window capability; and +- required elevation for catalog-authorized setup, game, and server scripts. + +These are proportionate for the application only because remote paths and +content remain catalog-bound, local mutations require an explicit local action, +and remote work now has finite global and per-origin budgets. + +## Findings fixed in this pass + +| Finding | Final boundary | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| mDNS advertised-address quota bypass | Candidate quotas use the observed response IP. One source gets 8 active-or-cooling candidates. | +| Unbounded mDNS dependency state | Cache records are capped at 1,024 globally and 128 per source; timers are deduplicated and capped at 4,096. | +| Anonymous request pool monopolization | One IP gets 8 handshake attempts per 3 seconds, 8/64 connections, 8/16 control tasks, 8/48 bulk tasks, and 1/2 Stream Install providers. | +| Renewable bulk-send deadlines | Raw chunks use one absolute ten-minute window; Stream Install uses a catalog-sized absolute window plus ten-minute inactivity through FIN. | +| Ordinary Sybil retry multiplication | At most 8 distinct identities at distinct IPs share one 20-minute retry window after the initial failure. | +| Stream Install drip and retry multiplication | At most 4 endpoint IPs share `10 min + 2 x catalog bytes / 1 MiB/s`; only written nonempty file bytes refresh inactivity. | +| Eager control-response allocation | An 8 MiB endpoint-IP budget and shared 32 MiB connector budget are acquired after manual four-byte length validation and held through decode. | +| Aggregate remote state | At most 16,384 remote library rows and 16,384 remote Call-to-Play events; rejected revisions retain watermarks. | +| Peer identity amplification | At most 8 authenticated identities may reside at one endpoint IP, within the global 64-peer limit. | +| Unbounded full-view publication | Remote-library and Call-to-Play views keep one queued and one replaceable pending snapshot; lifecycle events remain FIFO. | +| Call-to-Play projection cost | One author retains 128 creator roots; the UI projects 128 nominations and 256 messages and sorts messages once. | +| Selected-root ingestion | Index, traversal, depth, byte, elapsed-time, bounded `version.ini`, and 64 MiB aggregate settings-read limits fail without publishing partial state. | +| Mutable elevated Windows scripts | A fixed-role elevated worker reloads embedded catalog authority, verifies the locked script, uses System32 `cmd.exe`, and transfers locks into the child. | +| Catalog preflight root traversal | Package roots must be regular non-link directories before a bounded leaf read; diagnostics omit observed package contents. | +| Developer recipe shell injection | Just positional arguments and exported values are passed as quoted shell data. | +| Unused frontend shell authority | The main-window `shell:allow-open` capability was removed. | + +## Independent follow-up review + +A fresh source-to-sink review found and closed six additional gaps: response +semaphore head-of-line blocking, half-open handshake fairness, an unbounded +public readiness-sentinel read, aggregate settings-file reads, repeated monitor +backoff reset after a root switch, and an over-strict ten-minute provider egress +deadline. The corrected provider deadline is ten minutes plus one catalog copy +at 1 MiB/s. The handshake limiter drops excess Initials because issuing a Retry +token would bypass s2n's next connection-attempt callback. + +The reviewer read back the follow-up range and found no remaining concrete +source-level bypass or regression. + +## Dependency scan + +`just security-audit` refreshed RustSec and scanned 578 locked Rust packages, +then audited the Deno lockfile. It found no known vulnerabilities in either +graph. RustSec reported seven informational warnings: + +- six unmaintained build/transitive crates (`proc-macro-error` and five `unic-*` + crates); and +- `glib 0.18.5` unsoundness limited to `VariantStrIter` methods. + +The affected `glib` API has no call site in Lanspread or its locked dependants, +and the GTK 0.18 branch is constrained by the current Tauri Linux backend. The +warnings are dependency maintenance work rather than reachable findings. + +## Residual limits + +- Native Windows execution was unavailable. Linux-visible worker authority, + parsing, digest, role, and handoff tests pass, but the Windows lock-transfer + test has not run. +- The absent production scripts may invoke mutable secondary executables or + configuration as administrator. The production payload must be audited before + claiming transitive administrator-code provenance. +- Ordinary Install deliberately accepts locally changed or additional `.eti` + archives. Their expanded output and decompression cost are not catalog + authority; links are skipped and audited before promotion. +- Stream Install staging still uses ambient path calls. Exploitation requires a + concurrent same-machine writer able to mutate the selected root. +- Per-origin fairness cannot stop cooperating hosts or one host using several + usable IP addresses; finite global limits remain authoritative. + +## Validation + +- `just fmt`: passed. +- `just clippy`: passed with warnings denied. +- `just test`: passed, including 541 peer tests and all workspace targets. +- `just frontend-test`: 94 tests passed. +- `just build-fixture`: passed. +- `just peer-cli-build`: passed. +- `just peer-cli-tests`: all four catalog profiles passed; the container + scenarios could not start because this host denies Podman's `newuidmap` + capability, including outside the sandbox. +- `just mdns-vendor-test`: 106 tests passed with socket access. +- `just security-audit`: no known Rust or Deno vulnerabilities; seven accepted + RustSec informational warnings. + +Physical-LAN multicast behavior and native Windows execution remain runtime +validation boundaries. diff --git a/crates/lanspread-peer/ARCHITECTURE.md b/crates/lanspread-peer/ARCHITECTURE.md index ad88df3..32d6e50 100644 --- a/crates/lanspread-peer/ARCHITECTURE.md +++ b/crates/lanspread-peer/ARCHITECTURE.md @@ -60,9 +60,12 @@ When a peer is discovered: emit UI membership events. Advertised addresses must be unicast with a nonzero port; multicast, broadcast, and unspecified targets are dropped before any handshake packet is sent. The mDNS ingress queue and active - candidate negotiations are each capped at 64, and one source IP may hold at - most 8 active-or-cooling candidates so a single host cannot fill every slot - by rotating claimed peer IDs and ports. + candidate negotiations are each capped at 64, and one observed response IP + may hold at most 8 active-or-cooling candidates. The advertised A/AAAA target + is never used as provenance, so one host cannot escape the budget by rotating + target addresses, peer IDs, or ports. The vendored mDNS daemon also caps + cached records at 1,024 globally and 128 per response IP, with at most 4,096 + deduplicated timers. 2. Reserve a candidate negotiation lease before queueing or awaiting work, then establish a TLS-pinned connection to that exact endpoint. Missing or non-v8 records are rejected, and neither an ephemeral QUIC source port nor a payload @@ -103,14 +106,20 @@ When a peer is discovered: work for five seconds, and runs at most eight pinned pulls and eight hint sends concurrently. Child work is drained lexically on shutdown. - The server admits at most 64 unauthenticated handshakes, 64 established - connection scopes, and 32 control streams per connection. Each control stream - accepts one frame followed by request EOF and sends at most one response. - Inbound request frames are capped at 64 KiB (the QUIC receive window per - server stream matches it), response frames at 8 MiB, and control I/O has - ten-second deadlines. Wire collections (library games, Call to Play author - events) are deserialized through a bounded visitor that materializes at most - one element past the semantic limit and discards the rest, so an oversize - frame cannot claim more memory than a valid one before validation rejects it. + connection scopes, and 32 control streams per connection. One observed IP may + start at most 8 handshakes during one 3-second handshake window, before TLS + and established-connection leases exist; the source ledger itself holds at + most 256 IPs. One observed IP may retain at most 8 connections, 8 of the 16 + global control tasks, 8 of the 48 global bulk tasks, and 1 of the 2 Stream + Install providers. Each control stream accepts one frame followed by request + EOF and sends at most one response. Inbound request frames are capped at 64 + KiB (the QUIC receive window per server stream matches it), response frames at + 8 MiB, and control I/O has ten-second deadlines. Response buffering permits at + most 8 MiB per endpoint IP within one shared 32 MiB pool. Wire collections + (library games, Call to Play author events) are deserialized through a bounded + visitor that materializes at most one element past the semantic limit and + discards the rest, so an oversize frame cannot claim more memory than a valid + one before validation rejects it. ### Call to Play replication @@ -215,6 +224,10 @@ pull. - presence of `local/` as a directory - If the fingerprint is unchanged, reuse the cached local summary. - Only run a recursive scan for new or changed games. +- Snapshot and recursive work has explicit entry, game, depth, byte, file-size, + and elapsed-time ceilings. Failure retains the previous complete state. Poll + failures back off from one second to five minutes per attempted root; a root + switch resets that delay once. ## Local State and Recovery @@ -231,6 +244,9 @@ Downloaded and installed are independent predicates: - Install and update transactions unpack into staging, then overwrite the first discovered game-provided `account_name.txt` and `language.txt` files under the staged tree from launcher settings before promoting it to `local/`. +- Launch-settings discovery uses one bounded traversal, reads at most 1 MiB per + INI and 64 MiB in aggregate under the same ten-minute clock, and finishes all + candidate reads before its first write or marker publication. Reserved per-game paths: @@ -325,8 +341,10 @@ Most scans become O(number of game dirs), with full recursion only when needed. open/request/receive/check lifecycle of each ordinary chunk. - An integrity failure quarantines `(PeerId, content_id)` in shared memory for the rest of that peer runtime, independent of address changes. Transport - failures remain retryable without quarantine. Each failed chunk may try every - distinct eligible peer identity once; there is no separate numeric retry cap. + failures remain retryable without quarantine. Each failed chunk may try at + most eight distinct authenticated identities at distinct endpoint IPs. All + retries share one nonrenewable 20-minute window created after the initial + failure; an individual attempt still has the earlier ten-minute limit. - A no-transfer local shortcut is allowed only when settled ownership records the exact expected committed content ID. A matching `version.ini` alone, or a legacy/pre-content-ID record, is not catalog-content proof. @@ -343,9 +361,10 @@ Most scans become O(number of game dirs), with full recursion only when needed. ### Ordinary install extraction -- `install`/`update` extract every direct regular root `.eti` archive with the - external `unrar` unpacker into the staging directory. The archives were - BLAKE3-verified against the catalog when downloaded. +- `install`/`update` extract every current direct regular root `.eti` archive + with the external `unrar` unpacker into the staging directory. Downloaded + archives were BLAKE3-verified, but the ordinary path deliberately accepts + later local replacements and extra archives for the S33 workflow. - `unrar` runs with `-ol-`, so symbolic-link entries are skipped, and the staging tree is walked without following links before promotion. Any symlink (or reparse point on Windows) aborts the transaction and rolls staging back, @@ -359,6 +378,9 @@ Most scans become O(number of game dirs), with full recursion only when needed. output and only when its direct regular root `.eti` set exactly equals the catalog archive set. Missing or extra archives stop before the extraction provider receives authority. +- Provider egress has one nonrenewable deadline of ten minutes plus the exact + catalog streamed bytes at 1 MiB/s, so large valid games are finite without a + fixed ten-minute whole-transfer cutoff. - The Stream Install request names the exact catalog `ContentId`. All path-bearing frames, including archive names, use `CanonicalCatalogPath` and the fallible checked decoder distinguishes malformed framing from an explicit @@ -371,8 +393,12 @@ Most scans become O(number of game dirs), with full recursion only when needed. early corruption signal, not an authority boundary. - Integrity failures quarantine the source for that catalog content and retry a fresh transaction from another eligible peer. Transport failures retry without - quarantine; local I/O and cancellation stop. Every failed attempt rolls back - before the next source begins. + quarantine; local I/O and cancellation stop. At most four distinct endpoint + IPs are attempted. Every attempt shares a catalog-sized total deadline of ten + minutes plus two copies of the expected streamed bytes at 1 MiB/s. The + ten-minute inactivity timer refreshes only after a nonempty, catalog-checked + file chunk is written. Every failed attempt rolls back before the next source + begins. - After complete catalog verification, account/language/persona settings are rewritten in staging before promotion. The one-shot launch-settings marker is written only after successful promotion; if it cannot be written, first play