Skip to content

v1.8.0 — hardening release

Choose a tag to compare

@Rayrsn Rayrsn released this 26 Sep 06:43
· 45 commits to main since this release

This is the hardening release. Everything in it is about making farhand
trustworthy under a skeptical eye: the security claims are enforced by machines
rather than prose, the unsafe surface is documented and fenced, and the two
monolithic source files are gone.

Security defaults that used to be opt-in

  • fhd now refuses to start without an authentication token unless
    --allow-unauthenticated is passed explicitly. Empty tokens are rejected as
    a misconfiguration, and non-loopback binds warn loudly.
  • Token comparison is constant time (SHA-256 digests) across HELLO/RUN,
    STATUS, HISTORY, and CLEAN — no length or prefix leakage.
  • Frame memory now grows only as bytes actually arrive (64 KiB incremental
    reads) instead of trusting the length header, with a 1 MiB pre-authentication
    cap. A malicious header can no longer induce a large allocation.
  • New --max-connections (default 32) and --max-queued-runs (default 16).
  • Environment forwarding blocks infrastructure credentials (AWS_, GITHUB_,
    NPM_, DATABASE_, …) and credential suffixes (*_TOKEN, *_SECRET,
    *_API_KEY, …). fh --print-env lists exactly which names would be sent —
    values are never shown.

A real data-corruption fix

Workspace CoW cloning shelled out to cp -c -R and fell back to hardlinks.
A hardlinked file modified in place — a formatter, git checkout, an editor
save — silently corrupted the shared CAS object or seed workspace behind it.
Cloning is now pure Rust (clonefile / FICLONE / copy, preserving modes,
mtimes, and symlinks) and the hardlink fallback is gone entirely.

Correctness and operations

  • fh now runs on Windows at all. Windows gives the main thread a 1 MiB
    stack where Linux gives 8 MiB, and the client needed more than that before
    doing any work — it aborted with STATUS_STACK_OVERFLOW, even
    fh --version. The work now runs on a thread with an explicit stack, and CI
    runs both binaries under ulimit -s 1024 so it cannot regress silently.

  • CAS garbage collection (--cas-ttl-days, default 30 / --cas-max-gb):
    storage growth was previously unbounded. Eviction is LRU-by-use.

  • Collision-free run IDs (were millis ^ pid, which collided for runs
    started in the same millisecond and clobbered STATUS entries).

  • Blocking filesystem work moved off the async runtime via spawn_blocking.

  • A queued run whose client disconnects now frees its slot immediately.

CI, MSRV, and receipts

  • MSRV corrected from a false 1.75 to 1.88, and now enforced in CI. The old
    claim was untrue twice: the dependency graph needs 1.88, and our own
    is_multiple_of needs 1.87. We briefly pinned time to reach 1.85, then
    rejected it because that version carries RUSTSEC-2026-0009 — the
    advisory-fixed dependency and the honest floor won.
  • CI is now a 3-OS matrix plus MSRV, coverage (Codecov), cargo-deny, RustSec
    audit, and a fuzz smoke pass; nightly adds ThreadSanitizer, a deep fuzz run,
    and benchmark trends. cargo-deny is green with zero exceptions. All
    third-party actions are pinned by commit SHA.
  • Real cargo-fuzz targets (fuzz_read_frame, fuzz_unpack_tar,
    fuzz_wire_paths) asserting protocol invariants, with committed seed corpora
    for known attack vectors (traversal tar, absolute-path tar, lying header).
  • All 11 unsafe blocks carry SAFETY contracts; five crates now
    #![forbid(unsafe_code)] outright and CI fails on undocumented unsafe.
  • Cancellation is now proven, not assumed. The previous disconnect test
    dropped the connection, slept 500 ms, and asserted nothing. It now runs a
    two-level process tree remotely and requires both the direct child and the
    grandchild to die, plus proof the cancelled run released its concurrency
    permit. It was validated by mutating the daemon to kill only the direct
    child — the test fails with the grandchild surviving, which is precisely the
    orphan-process bug the design forbids.

Architecture

  • fhd/src/lib.rs: 2,209 → 1,232 lines, split into active, exec,
    stream, and session modules.
  • fh/src/main.rs: 1,883 → 1,609 lines, with the clap surface in cli.rs,
    RunParams replacing 18 positional arguments, one shared perform_handshake,
    and watch mode in its own function.

All refactoring moved code verbatim — no behavior changes — and the per-suite
test counts were verified after every step.

Upgrade notes

  1. fhd now needs a token. Start it with --token <token> or
    FARHAND_TOKEN=<token>, or pass --allow-unauthenticated if you are
    deliberately running tokenless on a trusted loopback interface.
  2. Build with Rust 1.88 or newer (edition 2021).

Full details in CHANGELOG.md.
Assets include archives for Linux (x86_64/aarch64 musl), macOS (x86_64/aarch64),
and Windows, each with a .sha256 checksum.