Skip to content

v0.40.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 14:05
· 2 commits to main since this release
Immutable release. Only release title and notes can be modified.
936e485

Performance

  • Incremental process table (Linux): a per-pid classification cache
    (crates/adsum-detect/src/cache/procs.rs) replaces the full re-read of
    every process each tick. A tick now enumerates /proc for liveness,
    sweeps the dead, revalidates cached hits (one cmdline read per detected
    game; the matcher re-runs if the argv fingerprint drifts) and classifies
    only new pids. Cached misses are not re-read; a miss that becomes a hit
    surfaces as an EXEC event or a watcher-loss flush.
    • Steady state: zero cmdline reads, automaton walks and allocations.
      On the bundled database shape (1062 processes): 5.05 → 0.38 ms/tick
      (−92%), 3152 → 3 allocs/tick, 40.3 KB → 54 B/tick of churn.
    • Every watcher loss channel (cn_proc gaps, backlog sheds, socket
      overruns, watcher death/resubscribe) forces one full-read tick; bundle
      swaps invalidate through an epoch in the generation Arc.
    • Watcher disabled/dead, other OSes and one-shot diagnostics keep the
      classic full sweep, byte-for-byte.
  • Zero-allocation /proc enumeration: raw getdents64 into a reused,
    aligned arena replaces std::fs::read_dir. ~3 allocations per entry →
    0 (~29 MB/h of churn removed at 1000 processes), which also keeps the
    glibc arenas and the RSS floor low.
  • Allocation-free snapshot lane: ScannedEntry.id/.name and
    ProcessDetectedEvent::Removed now use AppId/Arc<str>, and the scan
    loop's snapshot Vec is reused across ticks. Per tick: zero
    allocations; the daemon pump's ScannedGame construction is two
    refcount bumps instead of two deep copies.
  • Aux-table arena: the four SortedIndex tables (steam/epic/name/
    nodot) pack keys into one contiguous per-generation arena. Miss-heavy
    lookups are 13–39% faster and per-key malloc headers are gone. Build
    order, first-wins ties and lookups are unchanged (pinned by tests).
  • Steam install-tail matching: match_install_tail_into binary-searches
    a precomputed sorted tail index (rebuilt on dirs mutation) instead of
    walking every install prefix per probe, so large libraries no longer
    multiply each sweep or EXEC storm. Longest-wins and ambiguity refusal
    are preserved; a parity oracle test compares it with the old linear
    walk. refresh_if_stale now fingerprints over the map in place.
  • Borrow pass: five places that copied where a borrow suffices:
    • matching: the command-line AppId fallback no longer wraps the borrowed
      &str in a fresh AppId per process per tick.
    • cache/appid: one AppId shared by refcount instead of two copies.
    • runtime: first-sighting inserts are refcount bumps, not to_string().
    • types/cmd: ActivityCmd.cmd is Cow<'static, str>, so synthesized
      commands borrow their verb (wire format unchanged).
    • bridge/handoff: is_process_alive formats /proc/<pid> into a stack
      buffer.
  • Hoisted locks and buffers: the exclusions RwLock is read once per
    tick/event instead of per process; the dead-pid sweep, is_suspended
    and read_cwd_into reuse caller-owned sets and buffers.
  • Windows/macOS: the snapshot enumerator folds \→/ at fill time so
    match_process's Cow stays Borrowed; sysinfo slots are refilled in
    place and gain compact_slots; process_start_ms refreshes with
    ProcessRefreshKind::nothing() instead of everything().
  • Minor: Exclusions.executables is an FxHashSet; the persist gate
    swaps its dirty flag with AcqRel (was SeqCst); broadcast_raw
    answers both encoding questions in one pass.

Changed

  • File organization: one concern per module. The near-twin module
    pairs are gone — scan.rs (classification) is now matching/ with
    the pipeline in mod.rs and its rule families in normalize (the one
    name-folding rule), paths (what it means for a declared name to name
    a path), args (borrowed command-line token scans) and tiers (the
    aux match tiers + shared hit tail); the tick orchestration moved from
    scanner.rs to sweep.rs; database.rs (bundle swaps) became
    generations.rs, leaving db.rs as the single wire-model module.
    runtime.rs split into runtime/{mod,workers,lanes,cadence,loops} —
    start() reads as lifecycle and the two 150+ line thread closures are
    named bodies (run_scan_loop, run_refresh_loop). The reuse-buffer
    family moved to a top-level scratch.rs with a new
    EnumerateScratch group (pid list, getdents arena, liveness/work
    lists) so the enumeration state no longer rides inside the per-pid
    reader scratch, and the two per-pid caches grouped under cache/
    (appid + procs) with their shared trust model documented once.
    os_matches moved beside OsName in types.rs; first_sightings /
    apply_ignore_list moved beside the lanes that consume them. Public
    paths that embedders use are preserved (adsum_detect::proc_start,
    runtime::ScanGuard/idle_wait, the matching:: re-exports); the
    test tree mirrors the source (tests/unit/matching/,
    tests/unit/cache/, tests/unit/platform/). No behavior change: the
    full suite passes untouched, rustdoc is warning-free.
  • Platform backends behind one seam: the OS-specific code that was
    scattered as #[cfg] islands inside shared files (six reader pairs in
    scan.rs, two enumerations in scanner.rs, the netlink wiring in
    runtime.rs, four arena-release arms in server.rs) now lives under
    crates/adsum-detect/src/platform/ — linux/ (/proc readers,
    getdents64 enumerator, netlink dispatch/watcher), snapshot/ (the
    sysinfo backend for every non-Linux target), macos/ (the .app
    bundle-interior shape — deliberately ungated: the tier is data-driven
    and must run, and be tested, everywhere), windows/ (documented
    empty: nothing Windows-only is left in the crate), plus unified
    memory.rs (arena release) and proc_start.rs (public path
    adsum_detect::proc_start unchanged). The seam is a static-method
    ProcessEnumerator trait (traits/enumerator.rs: begin_tick +
    read_one), so the tick orchestration — BOTH the full sweep and the
    incremental cache — is now 100% cfg-free and runs on every target;
    the incremental mode is gated only by cache_mode_live (watcher
    enabled AND live), which today can only hold on Linux. No behavior
    change: the full test suite passes untouched, and the parse_proc_pid
    suite moved with its function.
  • Watcher opt-in under systemd: the user-unit guide now shows a drop-in
    that resets the namespace-creating sandbox directives so cn_proc can
    deliver, with the filesystem and device isolation loss stated explicitly.
    The default unit keeps its sandbox and polling.

Added

  • SteamAppId tier on Windows and macOS: read_steam_app_id was a
    None stub off Linux, leaving the 57% of database entries with no
    declared executables without their authoritative tier on those
    platforms. sysinfo 0.39 reads the environ block on both (PEB through
    the process handle on Windows, the sysctl arg/env route on macOS); the
    reader uses a System scoped to the call — the shared snapshot never
    accumulates environ copies — and the existing per-pid memo
    (Present/Absent/Stale + invalidation sequence) keeps it to one
    read per process lifetime. Permission-gated processes memoize
    Absent, exactly like an unreadable /proc/<pid>/environ.
  • Steam and Heroic discovery off Linux: fallback Steam roots now
    include the registry's SteamPath (HKCU\Software\Valve\Steam via
    reg query — dependency-free; discovery runs at startup plus every
    120th tick) and the Program Files (x86)/Program Files defaults on
    Windows, and ~/Library/Application Support/Steam on macOS; Heroic's
    cached library is found under %APPDATA%\heroic (Windows) and
    ~/Library/Application Support/heroic (macOS). The
    ADSUM_STEAM_ROOT/ADSUM_HEROIC_ROOT overrides stay exclusive.
    Without these, the Steam-library and -epicapp tiers were silently
    dead off Linux ($HOME/XDG shapes do not exist on Windows).
  • DetectablesBundle carries a monotonic epoch (stamped inside the
    generation Arc, so a reader can never pair it with a foreign
    bundle): the cache-invalidation signal for refreshes and override
    edits, costing one full-read tick per swap.
  • watch() takes an invalidate flag it sets on every loss channel it
    can observe from inside (sequence gap, shed, ENOBUFS); the retry
    loop sets it on both edges of the watch.

Fixed

  • Proc-events watcher: a non-initial user or PID namespace now turns
    the watcher off with one warning instead of retrying a self-test the
    kernel can never satisfy. watcher_live is set only after the self-test
    parses an event and cleared on every exit. A refusal is recognized both
    as a nonzero NLMSG_ERROR and as a PROC_EVENT_NONE ack with an error
    in the event body, and retries back off from 5 seconds to a 5-minute cap
    instead of waiting a fixed 5 minutes. A watch that proved delivery resets
    that backoff, so a late failure retries soon instead of inheriting a stale
    cap.
  • Proc-events listener leak: every exit after a successful subscription
    now sends PROC_CN_MCAST_IGNORE before closing the netlink socket. The
    kernel decrements its global listener count only on that message, so a
    stopped watcher, a failed self-test or a lost receiver no longer leaves
    the count positive. A leaked count let sandboxed sockets receive events
    they were never granted.
  • PID namespace gate: the watcher also reads NSpid from
    /proc/self/status and stays off when it sees several values, which
    means a nested PID namespace. A procfs mounted inside that namespace
    collapses the line to one value, so the self-test remains the final word.
  • Steam demos whose executable is not in the database (VEIN Demo is the
    motivating case) are now detected when the process runs under the AppId's
    own install dir (steamapps/common/<installdir>/). The install dir is
    independent evidence, not an AppId-only match: declared launchers,
    prerequisite installers, Steam helpers and the exclusion list keep their
    current veto.
  • A clean install no longer fails with 226/NAMESPACE when the cache
    directory is missing: the unit creates it before the sandbox mounts
    (CacheDirectory=) instead of listing it in ReadWritePaths=. The
    installer creates the legacy default directories only when the
    installed unit still lists them there.

What's Changed

  • Proc events: own the listener slot, retry state and namespace gates by @yiesko in #35
  • chore(deps): Bump tokio in / by @dependabot[bot] in #33
  • chore(deps): Bump ureq in / by @dependabot[bot] in #34

Full Changelog: v0.39.0...v0.40.0