Repository navigation
fix(skills): prevent symlink swaps during catalog loads - #1438
tsarlandie-oai wants to merge 1 commit into
Conversation
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@jamadeo is on triage rotation this week and is the reviewer for this one. Labels come from the PR labeler. Please review the change. Automated triage. Labels are a first pass and the assigned maintainer has the final say. |
jokemanfire
left a comment
There was a problem hiding this comment.
Reviewed the read path against the walk, and the ordering claim holds up:
- every ancestor is opened from its parent with
open_dir_nofollow, and the leaf is opened withfollow(No), so a component swapped after the walk fails closed instead of resolving; file.metadata()comes from the already-open handle, so there is no second path resolution between the type check and the read;nonblock(true)before theis_file()check is the right order for a FIFO substitution.
Four things I would like before this lands:
-
The
nonblockflag has no test. The symlink test exercises the no-follow half, but nothing exercises "a FIFO was swapped in and we must not block forever". A#[cfg(unix)]test thatmkfifos over a walked path and asserts an error (rather than a hang) would justify the flag; without it, a future cleanup could dropnonblockand reintroduce a trivial DoS. -
The escape claim is only tested inside the root.
public/assets -> ../.privateis still inside the catalog root. Since the doc comment claims "no file outside the root is served", add a case where the link target is a second tempdir outside the root; it is cheap and it is the claim that matters. -
expect("a walked file has a name")inread_regular_file. IfEntry::pathis ever produced by something other thanwalk(it is a plainStringon a struct), this panics inside a request path. ReturningInvalidInputcosts oneok_or_else, and rejecting../empty components at the same time removes a foot-gun without affecting walked paths. -
Hard links are outside the guarantee. A hard link created inside the root that points at an outside file will still be served, because it is a regular file. That is a normal limitation of path-based confinement, but the doc comment currently reads as absolute; worth one clause so the security claim stays precise.
Not blocking: walk reads entry.metadata() for a Kind::File size, which can follow a symlink installed after enumeration. It is harmless today because the read path refuses symlinks, but if size feeds the byte budget, a note (or symlink_metadata) would keep the two paths consistent.
Stacked on #1409. The base is
DaleSeo/skills-fs.Motivation
The catalog walks a skill directory and reads file contents later. If a regular file or one of its parent directories is replaced with a symlink between those steps, the read can publish a hidden or sibling file under the original skill URI. Both paths can be inside the configured catalog root.
Changes
cap-fs-extcompanion tocap-stdunder the optionalskills-fsfeature.Validation
58f424a.cargo test -p rmcp --features skills-fs,client --lib --test test_skill_catalogpassed all 298 library tests and 30 catalog integration tests on Linux. All 14test_skillsprotocol tests also passed.cargo clippy -p rmcp --no-default-features --features skills-fs --lib -- -D warnings,cargo +nightly fmt --all -- --check, and the repository spelling check passed.cargo check --target x86_64-pc-windows-gnupassed for theskills-fslibrary. The repository’s full CI workflow runs on PRs targetingmainorrelease.