feat(filesystem): add bind-mount deny lists to hide host files/paths - #1448
Open
neurolabs wants to merge 46 commits into
Open
feat(filesystem): add bind-mount deny lists to hide host files/paths#1448neurolabs wants to merge 46 commits into
neurolabs wants to merge 46 commits into
Conversation
Add the ignore::gitignore-backed DenyList used to hide bind-mount paths host-side, plus the workspace ignore dependency. Empty deny lists match nothing; component-only patterns fast-path on the entry name while path patterns match the reconstructed mount-relative path. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Hide gitignore-style denied paths host-side: lookup and create/mkdir/mknod/ symlink/link/unlink/rmdir/rename reject denied names (ENOENT/EACCES), and readdir/readdirplus omit matching entries. Component-only patterns fast-path on the entry name; path patterns reconstruct the mount-relative path via the inode anchor chain. Add a PassthroughConfig::deny field, builder method, and unit tests. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Hide gitignore-style denied paths host-side: lookup returns ENOENT, create/ mkdir/mknod/symlink/link/unlink/rmdir/rename return EACCES on denied names, and readdir/readdirplus skip denied entries. Reconstructs the mount-relative path from the parent inode's stored host path for path-pattern matching. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Move the DenyList matcher from backends::shared to backends::deny so both the unix and windows passthrough backends can share it. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
The DenyList is used only by the unix and windows passthrough backends, so it belongs beside the other shared passthrough helper (quota.rs) rather than at the backends root. Keeps the Unix-gated shared module intact and shortens the cross-platform import paths to super::deny. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Plumb a host-side deny-list through the wire contract and Rust SDK so it reaches the runtime --mount deny= grammar. VolumeMount::Bind gains a deny Vec<String> (mirroring quota_mib) with custom Serialize/Deserialize/ Debug; MountBuilder::deny() sets it and build() validates it is bind-only; spawn.rs renders deny= tokens into the --mount arg path only, never the MSB_*_MOUNS env specs. Cloud and node-ts bind construction sites are updated to compile with the new field. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Expose the bind-mount deny-list on --mount-dir as a repeatable deny= option. CliMountOptions/CliMountOptionSupport carry deny; the key=value parser pushes each pattern (repeatable, value split on first = only); apply_common_mount_options forwards them via MountBuilder::deny. deny is enabled only for --mount-dir and rejected with a clear error for --mount-file/--mount-disk/--mount-named. inspect.rs binds the new field. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Add a host-side deny-list of gitignore-style patterns for bind mounts.
Matching entries are hidden from the guest (ENOENT) and writes to them
are forbidden (EACCES). Patterns are relative to the mount root and may
be component names (".env", "*.log") or paths ("sub/secret").
Expose Deny on MountOptions and MountConfig, plumb it through to the
MountSpec FFI struct and the native mount builder, and cover the wire
shape and propagation with tests.
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
join_path already returns an owned PathBuf, so the address-of operator on the temporary adds nothing; clippy flags it (needless_borrow) and it fails the branch's clippy -D warnings gate. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
A '.*' or '*' pattern matched the '.' and '..' entries, stripping them from
guest readdir and making lookup(".") return ENOENT, which broke every path
walk through the mount. deny_matches_name now short-circuits the structural
dot entries to not-denied on both the unix and windows backends; adds
regression tests probing readdir retention and lookup(".") under a '.*'
pattern.
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
The --mount option block is comma-joined and the spec is colon-split, so a deny pattern containing ',' or ':' would fail the whole spawn or, worse, silently attenuate other mount protections; a newline or NUL would corrupt gitignore parsing. Validate at every entry point -- the runtime mount-spec parser, the Rust SDK MountBuilder, and the Go native apply_volume boundary -- rejecting empty patterns and the wire separators. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Mount.Named and Mount.NamedWith carried MountOptions.Deny into the mount config, and the native layer now returns an explicit error instead of discarding the option, matching the Rust SDK and CLI behavior for the same mistake. The previous test that codified the silent drop is inverted. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
The cloud wire format has no deny field, so converting a config with a non-empty deny list would silently create a sandbox that exposes the files the caller asked to hide. Reject the mount at the cloud create boundary -- which covers both the Rust SDK and the CLI -- and document the lossy direction on the wire-type conversion. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Bind mounts with a deny list now render a [deny=...] suffix in msb inspect so users can see why entries are hidden from the guest. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Move the private is_ignored inherent impl into the Methods section and put the public join_path ahead of the private name_as_path helper. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
…atterns Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
* handle case insensitive filesystems
analogue to how git handles this, we probe the case sensitivity,
since APFS can be case sensitive or -insensitive
* switch a couple of places from fail-open to fail-closed (safety first)
* support dir-only patterns ("node_modules/"). Before, dir-only patterns
did not match/deny any check.
* document that renaming an ancestor (parent directory) allows circumvention
of checks.
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
* Rename filesystem_is_case_insensitive to mount_is_case_insensitive and validate with a lowercase sibling name instead of a flipped-case one; document per-mount (not per-directory) sensitivity detection. * Extract shared resolve_dirent_type for DT_UNKNOWN entries in both the Linux and macOS readdir paths. * Tighten deny-path comments and expand unix/windows deny tests. Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
…ir-only flags Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
* added test coverage * The unix builder now delegates to PassthroughFs::new(cfg), fixing a missing-field compile break on MacOS. * skip mount_is_case_insensitive check when patterns.is_empty() * windows/mod.rs:239: switch fail-open on path traversal to fail-close * unify non-UTF-8 name semantics on Windows (EINVAL instead of ENOENT/EACCES) * extend mount_is_case_insensitive() to derive case-sensitivity of ro mounts by platform and (on linux) fs type * trim deny patterns in both SDK render paths, analogous to CLI * extended documentation Signed-off-by: Ole Langbehn <ole.langbehn@inoio.de>
Prompt To Fix All With AI### Issue 1
crates/filesystem/lib/backends/passthroughfs/windows/ops.rs:231-233
**Missing identity blocks every rename**
When a bind mount on FAT32, exFAT, or another filesystem without stable file identities has any directory-only deny pattern, this branch returns `EBUSY` for every rename before checking whether the allowed source and destination require that protection. As a result, ordinary file renames between non-denied paths fail even though neither path matches the deny list.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (3): Last reviewed commit: "fix(filesystem): fix greptile "missing i..." | Re-trigger Greptile |
On filesystems without file identities (FAT32/exFAT/some network volumes), file_identity returns None, so the identity re-check at ops.rs:225 was a no-op. An external writer could swap the source file for a directory between the deny-check's type decision (source_is_dir = false) and the actual std::fs::rename, moving the directory to a dir-only-denied destination. Fix: when file identity is unavailable and the deny set has dir-only patterns (where the type decision is security-relevant), the rename now fails closed with LINUX_EBUSY instead of proceeding with an unverifiable, stale type. Renames still work normally on filesystems with identities, and on identity-less filesystems without dir-only patterns.
When a bind mount on FAT32, exFAT, or another filesystem without stable file identities has any directory-only deny pattern, this branch returns EBUSY for every rename before checking whether the allowed source and destination require that protection. As a result, ordinary file renames between non-denied paths fail even though neither path matches the deny list. **Resolution:** The rename's fail-closed EBUSY now short-circuits on has_dir_only_patterns() and fires only when the source or destination path actually collides with a dir-only deny pattern on an identity-less filesystem, so FAT/exFAT renames between non-denied paths proceed normally while dir-only-denied moves stay blocked.
|
On an identity-less filesystem such as FAT32 or exFAT, renaming a regular file named node_modules to an otherwise allowed path passes the actual-type deny check, but dir_denied repeats matching with is_dir=true and returns EBUSY. This violates directory-only semantics and prevents a valid regular-file rename. **Resolution:** narrow the fail-closed EBUSY guard on identity-less filesystems (FAT/exFAT) to fire only when a file-decided source is renamed to a destination colliding with a dir-only deny pattern, so same-named file renames and ordinary renames keep working while a swapped-in directory can still be blocked from landing at a denied name.
When `RENAME_EXCHANGE` or macOS `RENAME_SWAP` swaps a regular file at a directory-only-denied name with a directory at an allowed name, the destination directory is checked only against its original `newname`. The exchange then moves that directory onto the denied `oldname`, bypassing the bind-mount write policy. **Resolution:** For macOS and Linux, RENAME_EXCHANGE now also rejects when the destination is a directory that would land at a dir-only-denied source name.
|
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1326
Summary
Adds an optional deny list of gitignore-style patterns to bind mounts. Matching host paths are hidden from the guest (ENOENT) and writes are rejected (EACCES) via the Unix/Windows passthrough backends. Usable across the CLI and Rust/Go/Python/Node SDKs.
Description
DenyListmatcher incrates/filesystemnode_modules/)core.ignorecasefor rw mounts, derived from platform/filesystem type on ro mounts/.vol+F_GETPATHagainst the canonical mount root and fails closed when a path cannot be resolveddeny=mount option; runtime and SDKs all reject empty patterns and patterns containing,/:/newline/NUL.deny=on--mount-dironly (rejected with a clear error on--mount-file/--mount-disk/--mount-named);msb inspectrenders a[deny=...]suffixCloudCreateBodyconversion boundary (cloud creates can't enforce them)Notes
Confidence Score: 3/5
The PR is not yet safe to merge because exchange operations can still move a concurrently substituted directory onto a directory-only-denied source name.
The Linux and macOS exchange paths make their reverse-direction deny decision from a one-time destination type snapshot, then revalidate only the source before the atomic swap, leaving the previously reported host-side destination replacement bypass outstanding.
Files Needing Attention: crates/filesystem/lib/backends/passthroughfs/unix/remove_ops.rs
Reviews (12): Last reviewed commit: "Merge branch 'main' into bind-mount-deny..." | Re-trigger Greptile
Context used: