Skip to content

v0.38.0 - 91db30a03ae3993807a31f097fb764bf2a919610

Choose a tag to compare

@github-actions github-actions released this 11 Oct 14:34
· 82 commits to main since this release
Immutable release. Only release title and notes can be modified.
91db30a

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 / detectLatencyMs fields 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 rustls to 0.23.45 in the fuzz and tools/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 Origin header like the game
    transport already did: absent Origin (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 through ws://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
    missing minisign binary aborts the install instead of continuing on
    SHA256 alone (explicit --binary files stay exempt). The systemd unit
    ships as a signed release asset covered by SHA256SUMS.txt and is
    verified the same way before anything is installed from it.
  • Presence snapshots are owner-only: the temp file is created with an
    explicit 0600 mode 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) are Arc<str> newtypes with Deref<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 /proc stat 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 as DatabaseTooLarge,
    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: the kB unit in
    /proc/self/status is really KiB, confirmed by comparing resident pages
    against VmRSS.
  • Two deliberate deviations. The SET_ACTIVITY echo emits data
    keys in declaration order instead of the BTreeMap'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-state into rsrpc-bridge (12 crates down to 11): the
    snapshot slots had exactly one consumer, so the crate boundary only
    added hops. rsrpc-telemetry stays separate: four crates share it,
    and folding it into the bridge would point their dependencies the
    wrong way. BindPaths=/tmp in 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 exceed MAX_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.lock is byte-identical, and per-crate
    versions are untouched. authors names the historical author and the
    current maintainer (spikehd, yieskoW). tokio-util stays pinned
    per crate on purpose: inheritance cannot override default-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 --check on the plugin, systemd-analyze verify on 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. New ipc_frame fuzz target drives the IPC
    reader (handle_stream with 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.rs is now foundation plus
    runtime (threads, shutdown, join), database (bundle ownership and
    swaps), scanner (enumeration and classification), steam
    (library roots) alongside the existing bundle/db/refresh/scan
    modules; crates/rsrpc-bridge/src/bridge.rs is now construction plus pumps
    alongside consumer (fan-out and pruning), router (activity
    decisions), replay (cache and ghost-reap pid resolution), snapshot
    (persistence) and control (SET_USER/RESET_USER). Shared fields
    are crate-visible for the new units; wire protocols unchanged. One
    breaking change for library callers: rsrpc_proc_events::watch now
    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 on ubuntu-latest.
  • The three workspaces share one grouped cargo entry in CI instead of
    three near-duplicates.

Fixed

  • Hostile timestamps saturate instead of overflowing: normalize_timestamp
    uses saturating_mul, so extreme inputs floor at i64::MIN in both
    debug and release builds.
  • Payload builders (empty_cached, generic_payload) return None on
    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 directed shutdown() + join() (plus rsrpc-* thread names),
    every long sleep became an interruptible park, the netlink watcher
    honors a stop flag (including its self-test), and Daemon::run_until
    shuts down, joins and drops the scanner on success and bind failures.
    Future abandonment (dropping the future via select!/timeout/abort)
    signals shutdown through a cancellation guard instead, so threads exit
    on their own within about one poll interval. When run_until returns,
    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-systemd outside 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-systemd mode — 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), so https://client/ or
    https://CLIENT matches the serialized Origin the browser sends
    instead of failing with only a Refused origin log. The
    per-connection check stays an exact comparison.
  • A failed /proc directory 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
    native C:\... 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 — SteamAppId included. 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. A SteamAppId now 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 own reaper and 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.txt and
    .../hl2.exe.evil were 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.exe or bin\gmad.exe was 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 launches larilauncher.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 .app bundle 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
    .app at 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
    declares hoi4 for Linux and hoi4.exe for 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
    short java pattern ended inside Illarion's longer
    illarion/jre/bin/java.exe and 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

Full Changelog: v0.36.1...v0.38.0