Repository navigation
v0.38.0 - 91db30a03ae3993807a31f097fb764bf2a919610
·
82 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Added
- Detection traceability: every first sighting logs which matcher won
(automaton,cwd-joined,proton-automaton,steam-app-id,
steam-library,exe-stem,folder) plus process age in ms at
classification; every clear carries its reason (sdk-clear,
abrupt-close,process-vanished,scan-absent,yielded); SDK takeovers and
generic resumes name both sides of the slot. Snapshots gain additive
detectionSource/detectLatencyMsfields per card ("sdk"for
client-published cards, absent on old ones). Tracing only, on
lifecycle transitions — no hot-path cost, no retained state. Guide:
docs/detection-traceability.md.
Security
- Bumped
rustlsto 0.23.45 in thefuzzandtools/updater
workspaces (RUSTSEC-2026-0285, TLS 1.3 handshake); the main lock was
already clean. All three locks are now audited in CI. - The bridge now validates the WebSocket
Originheader like the game
transport already did: absentOrigin(native clients) and Discord's
own pages pass, anything else is closed at connect before READY unless
listed in--bridge-allowed-origins/RSRPC_BRIDGE_ALLOWED_ORIGINS.
A malicious page can no longer read local activities or send bridge
commands throughws://127.0.0.1:1337; frames pipelined past the
close are dropped as unregistered. Client ids are now unique
process-wide across both bridge servers (per-server counters could
alias registrations), and messages from unregistered ids never reach
control handling. - The installer is fail-closed: a missing signature manifest or a
missingminisignbinary aborts the install instead of continuing on
SHA256 alone (explicit--binaryfiles stay exempt). The systemd unit
ships as a signed release asset covered bySHA256SUMS.txtand is
verified the same way before anything is installed from it. - Presence snapshots are owner-only: the temp file is created with an
explicit0600mode on Unix (the rename carries it to the final
path), so app ids, pids and ports stay private under a permissive
umask. Every write uses its own temp, created exclusively without
following symlinks, so a planted link is refused and concurrent
daemons sharing a slot still write apart. Slot selection stays
best-effort — concurrent daemons share slots, last writer wins —
and stale crash temps are swept under the same staleness rule.
Performance
- The per-tick, per-publish and per-connection allocations left behind by
the earlier memory fix (#2) are gone, under one rule: one owned
materialization at the boundary, zero deep copies after it (61 commits,
59 files, +6394/-1349). Same games detected, same order, same log lines. - Ids (
AppId,SocketId) areArc<str>newtypes withDeref<str>, so
cloning one is a refcount bump: every path that copied a client id,
socket id or app id now shares it. - Payloads that reach more than one consumer cross as
Bytes/Utf8Bytes/Arc<T>and are encoded once; the MessagePack
derivation is lazy and shared instead of rebuilt per consumer. - Scratch buffers (
ExecScratch,MatchScratch, the/procstat and
environ paths) are owned by the loop and reused across ticks, which is
where roughly 400 allocations every 5 s per process came from. The scan
tick classifies every process on the machine, so this is the path that
decides the resident set, not a hot path in the usual sense. - Collections that outlive their build are
Box<[T]>and shrunk;
transient ones deliberately are not. Entries carry packed[u32;2]
index pairs, and the automaton is built in a single pass with exact
reservations. - Structural caps on the fetched database, measured rather than guessed:
the bundled snapshot is 24,299 entries, and the maxima within an entry are
16 executables, 8 aliases, 30 Steam SKUs and 204-character names. Every
cap sits an order of magnitude above that. A body decoding past one is
rejected asDatabaseTooLarge,
distinct from a parse failure, and the caller keeps the database it
already holds. The 64 MiB body limit bounded the bytes; it never
bounded what they decode into — one entry with tens of thousands of
executables is a megabyte of JSON becoming one automaton pattern and
one index pair each. - The census now reports the resident high-water mark beside the current
RSS and the live/free heap split from the allocator, so a growth can be
told from a floor that held while the peak climbed. Both were verified
against the kernel before being written: thekBunit in
/proc/self/statusis really KiB, confirmed by comparing resident pages
againstVmRSS. - Two deliberate deviations. The
SET_ACTIVITYecho emitsdata
keys in declaration order instead of theBTreeMap's alphabetical
order (the echo contract already allows key-order variance and every
test compares parsed values), and the IPC close-clear nonce now carries
the value the client sent instead of its JSON text — that one is a
fix, see below.
Changed
- Folded
rsrpc-stateintorsrpc-bridge(12 crates down to 11): the
snapshot slots had exactly one consumer, so the crate boundary only
added hops.rsrpc-telemetrystays separate: four crates share it,
and folding it into the bridge would point their dependencies the
wrong way.BindPaths=/tmpin the unit stays as is: tightening was
investigated, but BindPaths takes no globs, the sockets only exist at
runtime, and the fan-out symlinks need the host path writable. - Regression nets now pin the memory bounds: replay-cache pruning and
broadcast fan-out never exceedMAX_CACHED_ACTIVITIES, and the
flood-guard table stays within its cap. - Shared package metadata and dependency pins now live in the workspace
root ([workspace.package]+[workspace.dependencies]): edition,
MSRV, license, repository, authors and 15 dependency versions change
once instead of per crate.Cargo.lockis byte-identical, and per-crate
versions are untouched.authorsnames the historical author and the
current maintainer (spikehd,yieskoW).tokio-utilstays pinned
per crate on purpose: inheritance cannot overridedefault-features. - CI now runs the test suite on Windows and macOS as well as Linux,
audits all three lockfiles, and validates non-Rust artifacts
(node --checkon the plugin,systemd-analyze verifyon the unit,
typecheck of the fuzz workspace). A new fuzz workflow smoke-checks
targets on PRs and runs the engine nightly (10 minutes per target:
ipc_frame,activity_full,trim_db,db_parse), with corpus
caching between runs. Newipc_framefuzz target drives the IPC
reader (handle_streamwith a mock facilitator and a split
byte-duplex), asserting every response byte belongs to a complete
frame (708k runs clean locally). - All GitHub Actions references are pinned to commit SHAs (tags kept as
comments), closing the mutable-tag supply-chain window on the release
pipeline especially. - Unix IPC pumps are capped at 16 concurrent peers like Windows (which
keeps 32): connection floods past the cap drop peers at accept instead
of parking blocking threads without bound, leaving room in the
32-thread blocking pool for unrelated work. - Split the two biggest modules internally, same crates and same
behavior:crates/rsrpc-detect/src/server.rsis now foundation plus
runtime(threads, shutdown, join),database(bundle ownership and
swaps),scanner(enumeration and classification),steam
(library roots) alongside the existingbundle/db/refresh/scan
modules;crates/rsrpc-bridge/src/bridge.rsis now construction plus pumps
alongsideconsumer(fan-out and pruning),router(activity
decisions),replay(cache and ghost-reap pid resolution),snapshot
(persistence) andcontrol(SET_USER/RESET_USER).Sharedfields
are crate-visible for the new units; wire protocols unchanged. One
breaking change for library callers:rsrpc_proc_events::watchnow
takes a shutdown flag (watch(events, stop)), so the watcher thread
joins on directed shutdown instead of blocking on netlink. - CodeQL now scans the workspace as one job with the v4 action inputs, and
re-scans whenever any workflow changes rather than only its own. - Dependabot maintains the three cargo workspaces and the pinned actions;
CI declares the least permissions each workflow needs, and the runners
are pinned by image label instead of floating onubuntu-latest. - The three workspaces share one grouped
cargoentry in CI instead of
three near-duplicates.
Fixed
- Hostile timestamps saturate instead of overflowing:
normalize_timestamp
usessaturating_mul, so extreme inputs floor ati64::MINin both
debug and release builds. - Payload builders (
empty_cached,generic_payload) returnNoneon
encode failure instead of shipping empty frames: callers log, count a
dropped broadcast and skip. The hand-written clear JSON is gone — clears
serialize from the same struct as everything else — and the emergency
error frames escape the command name instead of interpolating it raw. - The flood-guard key builds once per call instead of twice (an
AppId
key would allocate identically, since callers hold&str). - Scanner threads now belong to the daemon lifecycle:
ProcessServer
gained directedshutdown()+join()(plusrsrpc-*thread names),
every long sleep became an interruptible park, the netlink watcher
honors a stop flag (including its self-test), andDaemon::run_until
shuts down, joins and drops the scanner on success and bind failures.
Future abandonment (dropping the future viaselect!/timeout/abort)
signals shutdown through a cancellation guard instead, so threads exit
on their own within about one poll interval. Whenrun_untilreturns,
no rsRPC thread is still running, so library callers may run another
daemon afterwards (crates/rsrpc-detect/src/server.rs,
crates/rsrpc-proc-events/src/lib.rs,
crates/rsrpc-core/src/daemon.rs). - The module split broke non-Linux builds (the Linux-only
/proc
reader was imported unconditionally: E0432 on every Windows/macOS
job). The reader import and its Linux-only companions are now gated
on the target, and the whole workspace checks warning-free for
Windows again. --binary … --no-systemdoutside a checkout died demanding a
signature manifest for a unit the user asked not to install. Every
unit step (resolution, validation, directory creation, copy) is now
skipped in--no-systemdmode — binary-only install, exit 0 — and
the auto-update drop-in and closing log line follow the same flag
(scripts/install.sh).- Configured extra origins normalize once in the builder (lowercase,
no trailing slash, no default port), sohttps://client/or
https://CLIENTmatches the serializedOriginthe browser sends
instead of failing with only aRefused originlog. The
per-connection check stays an exact comparison. - A failed
/procdirectory entry can no longer abort the scan tick:
the skip is now explicit (the surrounding filter already excluded
errors; the invariant is documented, behavior unchanged). - Steam prefix matching works on Windows: library keys and queries are
canonicalized once (lowercase, backslash to slash, leading slash), so
nativeC:\...paths meet the cached keys. Without this no prefix
ever matched there and Steam games went undetected. The library cache
schema bumped to v2 for the same reason: v1 caches holding backslash
keys are ignored once and rescanned instead of reused empty. - The crash-temp sweep only removes snapshot temps: it now requires the
snapshot filename prefix too, so other programs'*.tmp-*files in
the shared temp dir are never touched. - Downloading or updating a game on Steam opened a presence for a game
nobody was playing. Steam ends an update by running the app's script
phase and setting up the Proton prefix, and hands both the game's
environment —SteamAppIdincluded. Under Proton the environ is hidden
from a service context, so the id reached the scanner by way of the
command line, about a second after Steam committed thousands of updated
files. ASteamAppIdnow only corroborates when the process runs an
executable the database entry declares, matched on a path-component
boundary; an entry with no declared executables keeps the id on its own,
which is the case the tier exists for. Verified in the field with the
deployed binary: the log names Steam's ownreaperand no presence
appears. - A declared name that merely appears in the path no longer reads as
the game. The automaton matches substrings, so.../hl2.exe.txtand
.../hl2.exe.evilwere detected as Half-Life 2 with no store id
involved. The executable that fired now has to name the end of the
probed string on a boundary. - The two tiers disagreed about spelling: the store-id tier compared the
raw declared name while the automaton compared the normalized one, so
an entry declaring only>hl2.exeorbin\gmad.exewas rejected by the
id tier and accepted by the automaton. Both now corroborate through the
same allocation-free predicate. - A launcher is not the game. The database flags these with
is_launcher, and Steam runs them first: clicking Play on Baldur's Gate
III launcheslarilauncher.exe, which carries the game's AppId before
the game has started — the same shape as the update above, one tier over.
Twenty executables across thirteen entries were in that position. The
>marker is a different mechanism ("the arguments carry the launcher")
and is unaffected. - A macOS
.appbundle names the binary inside it. Sixty-one
executables across fifty-six entries are declared as the bundle
(factorio.app), but the process macOS runs is the binary within
.../Factorio.app/Contents/MacOS/factorio. Those games were detected
before the boundary rule and stopped being detected after it; a declared
.appat a component boundary now matches when Apple's bundle interior
follows it. - A refusal is remembered for the whole automaton walk. A declared name
seen in a path without naming it is specific evidence that the process
is not the game, and the stem and folder heuristics must not answer over
it — but the evidence belongs to the path, not to the automaton that read
it, so a second pass or a stripped 64-bit variant no longer erases it.
It also no longer stands between the two automata: Hearts of Iron IV
declareshoi4for Linux andhoi4.exefor Windows, and the Linux name
was hiding the Wine game the Proton probe was about to name. - A rejected automaton match no longer hides the match that does name
the process: the probes walk their automaton's overlapping matches and a
refusal costs that match, not the variant. Without this, Minecraft's
shortjavapattern ended inside Illarion's longer
illarion/jre/bin/java.exeand the correct hit was never tried. - The IPC close-clear nonce now carries the value the client sent instead
of its JSON text:"abc"used to be echoed back as"\"abc\"", and a
null nonce as the literal string"null".
Commits in v0.38.0 (v0.36.1..HEAD)
- docs: add a platform support section to the readme (b1ae74b)
- docs: rewrite the compatible-clients section, fix build dir (0ed7d85)
- docs: fix sober screenshot caption to ipc reporting (7fbf9fb)
- docs: add a project status section (661a115)
- fix(detect): shut down and join scanner threads on teardown (0f874a2)
- refactor(detect): split server and bridge into focused modules (686ecf7)
- refactor: privatize new internal modules (d462647)
- docs: note the watch signature break in the changelog (d477a4d)
- fix(ipc): cap Unix pumps, gate bridge origin and sender (84f6741)
- docs: document state_file and bridge_allowed_origins fields (d3839ee)
- ci: pin actions, fail-closed installer, signed unit (2d23c1f)
- ci: test all platforms, fuzz workflow, audit every lock (2e665e3)
- fix(protocol): harden payload builders and timestamp math (96172a0)
- chore: centralize workspace package metadata and dependencies (4067acf)
- refactor(bridge): fold rsrpc-state crate into the bridge (4f24555)
- fix(unit): harden the systemd sandbox (576611c)
- docs: release 0.37.0 changelog (312c408)
- chore: release v0.37.0 (4b1325d)
- ci: auto-review draft PRs with CodeRabbit (c96e911)
- fix(detect): compile on Windows and macOS again (7df40c0)
- fix(install): skip unit handling under --no-systemd (248bb95)
- fix(bridge): normalize configured extra origins once (5ce93a4)
- fix(detect): skip failed /proc entries explicitly (d434e1f)
- fix(bridge): write snapshots owner-only (2934bba)
- fix(fuzz): exercise the IPC reader, not just the encoder (c5c9a6a)
- docs: record review fixes in the 0.37.0 changelog (645f237)
- fix(bridge): harden snapshot temp files (6ad5340)
- fix(steam): match prefixes in canonical slash form (3f03955)
- ci: stub the service binary for unit verification (dda4469)
- docs: record round-two review fixes in the 0.37.0 changelog (e684fe5)
- fix(bridge): scope the temp sweep to snapshot names (7bd36bf)
- fix(steam): invalidate v1 caches with backslash prefixes (503ab51)
- fix(tests): serialize the unlocked daemon boot test (d27a908)
- docs: record round-three fixes in the 0.37.0 changelog (724ff1d)
- Observability: detection provenance, clear reasons and snapshot tracing (#4) (c530a4c)
- Memory: ownership, refcounting and buffer reuse across the pipeline (#6) (1bba72e)
- ci: pin the runner image labels instead of ubuntu-latest (40a76ab)
- ci(codeql): scan the workspace with CodeQL, without a build (b8d8897)
- ci(deps): let Dependabot maintain cargo and the pinned actions (671c1e6)
- ci: declare the least permissions each workflow needs (fb3ef08)
- ci(codeql): rescan when any workflow changes, not just this one (d281689)
- chore(deps): Bump minisign-verify from 0.2.5 to 0.3.0 (#18) (b081b5a)
- chore(deps): Bump the minor-and-patch group with 9 updates (#14) (36af267)
- chore(deps): Bump serde_with from 3.22.0 to 3.24.0 in /fuzz (#13) (94a84ad)
- chore(deps): Bump thiserror from 2.0.20 to 2.0.21 in /tools/updater (#12) (f780b28)
- chore(deps): Bump thiserror from 2.0.20 to 2.0.21 in /fuzz (#11) (6fab16f)
- chore(deps): Bump ureq from 3.4.0 to 3.4.2 in /tools/updater (#10) (6e6aff0)
- chore(deps): Bump tokio from 1.53.1 to 1.53.2 in /fuzz (#9) (8be4ed9)
- ci(deps): bump upload-artifact to v7 and action-gh-release (4ce2e05)
- ci(codeql): use the v4 input name for the languages list (2663256)
- ci(codeql): move both action steps to v4, together (6b36ca1)
- chore(deps): bump hotpath to 0.28.6 across all three workspaces (f3353ca)
- chore(deps): move the update digests to hex, and sha2 to 0.11 (55d1508)
- ci(deps): one cargo entry for all three workspaces, grouped by name (7144379)
- chore(deps): Bump ureq in /fuzz (#20) (eadcb0f)
- chore(deps): Bump serde_with in /tools/updater (#21) (19d0f8d)
- chore(deps): Bump libc in /fuzz (#22) (419a606)
- fix(detect): a store id is only evidence when it names the game (cb33676)
- docs: keep the new public items from linking to a private one (b078350)
- test: pin the Steam reaper as the false positive it actually was (060bb67)
- fix(detect): require a path boundary for an automaton exe hit (1105eab)
- fix(detect): keep probing a variant's other matches after a refusal (d38ee07)
- test(detect): prove the AppId tier answers, not a fallback (bbdd411)
- docs(detect): state the limits of the corroboration rules (2862d6d)
- test(detect): expect the tier the platform actually runs first (2bb1d4f)
- fix(detect): corroborate the store id with the automaton's own rule (a063a37)
- fix(detect): close the corroboration gaps a review found in this branch (3482b25)
- test(detect): make the new tests platform-aware (e77b976)
- fix(detect): a refusal belongs to the walk, not to the last probe (d0ece25)
- fix(detect): a native refusal must not hide the Wine game after it (b4f7f2c)
- fix(detect): a declared .app bundle names the binary inside it (37251aa)
- chore: release v0.38.0 (91db30a)
What's Changed
- Observability: detection provenance, clear reasons and snapshot tracing by @yiesko in #4
- Memory: ownership, refcounting and buffer reuse across the pipeline by @yiesko in #6
- chore(deps): Bump minisign-verify from 0.2.5 to 0.3.0 by @dependabot[bot] in #18
- chore(deps): Bump the minor-and-patch group with 9 updates by @dependabot[bot] in #14
- chore(deps): Bump serde_with from 3.22.0 to 3.24.0 in /fuzz by @dependabot[bot] in #13
- chore(deps): Bump thiserror from 2.0.20 to 2.0.21 in /tools/updater by @dependabot[bot] in #12
- chore(deps): Bump thiserror from 2.0.20 to 2.0.21 in /fuzz by @dependabot[bot] in #11
- chore(deps): Bump ureq from 3.4.0 to 3.4.2 in /tools/updater by @dependabot[bot] in #10
- chore(deps): Bump tokio from 1.53.1 to 1.53.2 in /fuzz by @dependabot[bot] in #9
- chore(deps): Bump ureq in /fuzz by @dependabot[bot] in #20
- chore(deps): Bump serde_with in /tools/updater by @dependabot[bot] in #21
- chore(deps): Bump libc in /fuzz by @dependabot[bot] in #22
- Detect: a name is evidence only when it names the process by @yiesko in #23
New Contributors
- @dependabot[bot] made their first contribution in #18
Full Changelog: v0.36.1...v0.38.0