ruvector/crates/ruvector-context
ruv 4c6da3b30d fix(context): make the index root private and enforce it
Four rounds of security review kept finding the same shape: a sensitive
thing placed under a NAME in the attacker-writable root, defended by a
check on that name, and the next round swapped the next name. The scratch
name moved inside a private staging directory -- and then the staging
directory was itself a name in the root, so it was swappable too. The
review demonstrated 274 victim rows readable outside the root after
erase_scope reported success.

There is no fixed point on that path, because every component under the
root is re-resolved on every syscall and this crate holds none of them by
descriptor. Two things terminate it: descriptor-anchored I/O, which needs
an fd- or directory-relative open in the vector engine and is therefore an
engine-level change, or removing the hostile directory from the threat
model. This takes the second.

It also settles a finding that made the rest academic: under a normal umask
shards were published 0644 inside a 0755 root, so a different-uid attacker
could read every tenant's vectors with no exploit at all. The staging
directory was protecting a shard for the milliseconds it was being built
and then publishing it world-readable.

The root is now created 0700 by mkdir itself, shards and the lock are
created 0600, and open refuses a root that is group- or other-accessible,
reporting the offending mode without the path. An existing root is
inspected and never modified, so an operator's deliberate permissions are
reported rather than silently widened or narrowed. Observed on disk under
both umask 022 and umask 000: root 0700, lock 0600, shard 0600, staging
0700.

The staging directory, the inode identity check, the lone-regular-file
requirement and the reserved-name sweep are all kept, but they are now
defence in depth against operator error rather than the boundary itself,
and their docs say so. The nlink documentation is corrected in particular:
a link count of one means only that no second name existed at the instant
of the lstat, not that the file is this index's, and the private root
rather than that check is what prevents a second name.

An attacker running as the same uid remains conceded, and content forgery
remains out of scope for want of a MAC over stored vectors. Both are stated
in the crate docs, the tests, and ADR-334.

Note for reviewers: tempfile::tempdir() creates 0755, so the new check
refused every existing test root and the suite now builds its roots at
0700 explicitly. That default is exactly the class of mistake that produced
the world-readable shards.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
2026-08-22 18:21:20 -04:00
..
src fix(context): make the index root private and enforce it 2026-08-22 18:21:20 -04:00
tests fix(context): make the index root private and enforce it 2026-08-22 18:21:20 -04:00
Cargo.toml fix(context): close symlink-redirected shard creation and quarantine denial 2026-08-22 17:32:23 -04:00