Repository navigation
feat(jail): move local grant discovery behind the bus contract #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
5a57c13
feat(jail): expose host-configured grant discovery over the bus
senamakel b214510
test(jail): support case-insensitive filesystems and current Clippy
senamakel 6eb91d5
fix(jail): enforce protected roots and complete grant contract admission
senamakel 5915d5e
fix(jail): preserve protected aliases and reserve procfs grants
senamakel 4adc410
chore: files changed crates/tinybox-jail/src/grants.rs,crates/tinybox…
senamakel 515b073
test(grants): cover recursive ancestor grants against protected prefixes
senamakel 92c92c7
fix(grants): reject protected prefixes that are descendants of a grant
senamakel 53d586f
test(grants): cover protected prefix aliases and cargo source promotion
senamakel b8f866c
test(grants): cover credential root alias ancestor protection
senamakel e2f8c79
fix(jail): block writable grants reaching shared cargo sources
senamakel e45cb6c
docs(tinybus-module): document alias and cache grant rules
senamakel 7abf36b
test(grants): cover credential floor with aliased parent directories
senamakel 7545b72
fix(jail): resolve protected roots through missing ancestors
senamakel 0677075
test(grants): cover writable extras that target protected cargo and t…
senamakel c3ce0b4
fix(jail): block writable grants reaching fixed host toolchain state
senamakel 3e1c67e
docs(tinybox-module): document toolchain and git config protections
senamakel abe63a0
test(jail): cover protected paths under directory extras
senamakel 05b9d2e
feat(jail): inspect recursive grants for protected entries
senamakel 9a7c6e9
test(grants): drop system path assertions from toolchain homes test
senamakel 320200d
test(grants): cover symlink bypass of subtree pair protection
senamakel 08aa55f
fix(jail): reject pair-prefix symlinks in protected path scan
senamakel 67bb209
test(jail): cover git include comments and quoted markers
senamakel a79534f
refactor(grants): split grant resolution into focused submodules
senamakel c910127
chore: files changed crates/tinybox-jail/src/grants/floor.rs,crates/t…
senamakel bdc3f44
test(grants): reformat malformed include value cases
senamakel d8d23be
test(grants): use raw string with double hashes in git config test
senamakel 8be2a7e
refactor(tinybox-jail): collapse nested if in git config include parsing
senamakel 9a8e296
test(grants): cover symlinked git configs under writable parents
senamakel e5acb1a
fix(git_config): protect each requested gitconfig link location
senamakel af64d94
fix(jail): treat empty xdg_config_home as unset for git config
senamakel e90167c
chore: files changed crates/tinybox-core/src/error/mod.rs,crates/tiny…
senamakel bcd846c
docs: document local grant discovery contract
senamakel d5dc940
test(grants): cover git include continuation and missing include paths
senamakel 104b067
fix(grants): handle git config line continuations and absolute xdg paths
senamakel fce19b8
test(grants): cover gitconfig candidates for empty xdg config home
senamakel 4313be1
test(git_config): cover includeIf sections with tab separators
senamakel 02b10d3
fix(jail): handle tab separators in git config section headers
senamakel afc2af6
test(grants): extract discovery assertions into a helper
senamakel 3d55348
Merge remote-tracking branch 'origin/main' into merge-pr-51
senamakel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| //! Host-configured filesystem grant discovery and protected-path vocabulary. | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| /// Configuration used to discover toolchain and explicit filesystem grants. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] | ||
| #[serde(default)] | ||
| pub struct LocalGrantConfig { | ||
| /// Include existing toolchain homes and git configuration. | ||
| pub toolchain_homes: bool, | ||
| /// Additional read-only paths, accepting a leading tilde. | ||
| pub extra_read_only: Vec<String>, | ||
| /// Additional writable paths, accepting a leading tilde. | ||
| pub extra_read_write: Vec<String>, | ||
| /// Explicitly permit read access to `/proc`. | ||
| pub allow_proc: bool, | ||
| } | ||
| impl Default for LocalGrantConfig { | ||
| fn default() -> Self { | ||
| Self { | ||
| toolchain_homes: true, | ||
| extra_read_only: Vec::new(), | ||
| extra_read_write: Vec::new(), | ||
| allow_proc: false, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Host-owned protected-path floor; matching ignores case and separator style. | ||
| #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] | ||
| #[serde(default)] | ||
| pub struct ProtectedPaths { | ||
| /// Forbidden path components at any depth (including credential filenames). | ||
| pub components: Vec<String>, | ||
| /// Forbidden consecutive path-component pairs. | ||
| pub component_pairs: Vec<[String; 2]>, | ||
| /// Forbidden absolute roots and their descendants. Trailing separators and | ||
| /// Windows extended-path namespaces are normalized before matching. | ||
| pub prefixes: Vec<String>, | ||
| /// Credential roots whose ancestors must also never be granted. | ||
| pub credential_roots: Vec<String>, | ||
| } | ||
|
|
||
| /// Request to resolve existing local grant paths, without launching a command. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub struct ResolveLocalGrantsRequest { | ||
| /// Operator home; absent for hosts with no operator-home access. | ||
| pub home: Option<String>, | ||
| /// Toolchain and additional-grant configuration. | ||
| pub config: LocalGrantConfig, | ||
| /// Host's unconditional protected-path floor. | ||
| pub protected: ProtectedPaths, | ||
| } | ||
|
|
||
| /// Canonical existing filesystem grants beyond a workload's writable root. | ||
| #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub struct LocalGrants { | ||
| /// Read and executable paths. | ||
| pub read_only: Vec<String>, | ||
| /// Read, executable, and writable paths. | ||
| pub read_write: Vec<String>, | ||
| } | ||
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Local grant discovery | ||
|
|
||
| resolve_local_jail_grants returns canonical existing JailGrants through the | ||
| workspace Result type. ResolveLocalGrants exposes the same behavior through | ||
| the module, with UTF-8 paths and an input/final 64-grant limit. | ||
|
|
||
| mod.rs orchestrates discovery and read-only precedence; types.rs holds results. | ||
| floor.rs matches components, adjacent pairs, protected prefixes and credential | ||
| roots, preserving requested and canonical aliases and ancestor boundaries. | ||
| inspection.rs scans directory descendants without following links. Errors, | ||
| over 1,000,000 entries or over 128 levels refuse a grant. Pair-prefix symlinks | ||
| are refused conservatively. Discovery is a snapshot, not a kernel filename | ||
| deny rule for entries created later; hosts must control tree mutation. | ||
|
|
||
| git_config.rs reads admitted Git configs only. Relative includes retain their | ||
| logical parent, and visited identity combines canonical file and resolved parent. | ||
| Each link location remains read-only even when targets repeat. Includes stop at | ||
| depth eight. Quotes, supported Git escapes and unquoted comments are parsed. | ||
| Empty XDG_CONFIG_HOME is treated as unset. User-name and installation-prefix | ||
| include expansion cannot be resolved portably from the supplied contract: | ||
| ~user/ and %(prefix)/ therefore reject the whole request explicitly, without | ||
| returning a partial grant set. | ||
|
|
||
| Automatic toolchains, Cargo executable/configuration state, shared dependency | ||
| caches and Git configuration are fixed read-only. Writable extras cannot | ||
| promote them or their ancestors/descendants. Manual grants retain writable | ||
| precedence when they do not reach fixed state. Only allow_proc grants /proc, | ||
| read-only. Mutable tools need separately approved disposable state. | ||
|
|
||
| Hosts supply the complete protected floor and keep authorization. This module | ||
| does not start commands or weaken unavailable confinement. See | ||
| [the contract specification](../../../../docs/specs/local-grants.md). |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| //! Protected root spellings and component-boundary matching. | ||
| use super::Builder; | ||
| use std::path::Path; | ||
|
|
||
| impl Builder<'_> { | ||
| pub(super) fn refuses_path(&self, path: &Path) -> bool { | ||
| let normalized = normalized_floor_path(&path.to_string_lossy()); | ||
| // Procfs is reserved for the dedicated read-only toggle, including | ||
| // ancestors whose recursive grant would expose it. | ||
| let exposes_proc = Path::new("/proc").exists() | ||
| && (same_or_descendant(&normalized, "/proc") | ||
| || same_or_descendant("/proc", &normalized)); | ||
| self.is_protected(path) || self.exposes_credentials(path) || exposes_proc | ||
| } | ||
|
|
||
| /// True when `path` is, contains, or sits inside a credential directory. | ||
| pub(super) fn exposes_credentials(&self, path: &Path) -> bool { | ||
| let path = normalized_floor_path(&path.to_string_lossy()); | ||
| self.protected.credential_roots.iter().any(|root| { | ||
| root_spellings(Path::new(root)) | ||
| .any(|cred| same_or_descendant(&path, &cred) || same_or_descendant(&cred, &path)) | ||
| }) | ||
| } | ||
|
|
||
| pub(super) fn has_protected_components(&self, path: &Path) -> bool { | ||
| let normalized = normalized_floor_path(&path.to_string_lossy()); | ||
| let components: Vec<_> = normalized | ||
| .split('/') | ||
| .filter(|part| !part.is_empty()) | ||
| .collect(); | ||
| components.iter().any(|part| { | ||
| self.protected | ||
| .components | ||
| .iter() | ||
| .any(|blocked| part.eq_ignore_ascii_case(blocked)) | ||
| }) || components.windows(2).any(|pair| { | ||
| self.protected.component_pairs.iter().any(|blocked| { | ||
| pair[0].eq_ignore_ascii_case(&blocked[0]) | ||
| && pair[1].eq_ignore_ascii_case(&blocked[1]) | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| pub(super) fn is_protected(&self, path: &Path) -> bool { | ||
| let normalized = normalized_floor_path(&path.to_string_lossy()); | ||
| self.has_protected_components(path) | ||
| || self.protected.prefixes.iter().any(|prefix| { | ||
| root_spellings(Path::new(prefix)).any(|prefix| { | ||
| same_or_descendant(&normalized, &prefix) | ||
| || same_or_descendant(&prefix, &normalized) | ||
| }) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // A configured protected root may itself be a symlink (for example /etc on | ||
| // macOS). Preserve its requested spelling and its existing resolved target. | ||
| pub(super) fn root_spellings(root: &Path) -> impl Iterator<Item = String> { | ||
| let location = root.parent().and_then(|parent| { | ||
| let mut ancestor = parent; | ||
| let mut missing = Vec::new(); | ||
| let mut canonical = loop { | ||
| if let Ok(canonical) = ancestor.canonicalize() { | ||
| break canonical; | ||
| } | ||
| missing.push(ancestor.file_name()?); | ||
| ancestor = ancestor.parent()?; | ||
| }; | ||
| for component in missing.into_iter().rev() { | ||
| canonical.push(component); | ||
| } | ||
| canonical.push(root.file_name()?); | ||
| Some(normalized_floor_path(&canonical.to_string_lossy())) | ||
| }); | ||
| std::iter::once(normalized_floor_path(&root.to_string_lossy())) | ||
| .chain(location) | ||
| .chain( | ||
| root.canonicalize() | ||
| .ok() | ||
| .map(|canonical| normalized_floor_path(&canonical.to_string_lossy())), | ||
| ) | ||
| } | ||
|
|
||
| // Windows canonical paths may carry an extended drive or UNC namespace; | ||
| // compare the underlying filesystem path, regardless of spelling or case. | ||
| pub(super) fn normalized_floor_path(path: &str) -> String { | ||
| let normalized = path.to_ascii_lowercase().replace('\\', "/"); | ||
| if let Some(rest) = normalized.strip_prefix("//?/unc/") { | ||
| format!("//{rest}") | ||
| } else { | ||
| normalized | ||
| .strip_prefix("//?/") | ||
| .unwrap_or(&normalized) | ||
| .to_owned() | ||
| } | ||
| } | ||
|
|
||
| pub(super) fn same_or_descendant(candidate: &str, parent: &str) -> bool { | ||
| let root = parent.trim_end_matches('/'); | ||
| if root.is_empty() { | ||
| return parent.starts_with('/') && candidate.starts_with('/'); | ||
| } | ||
| candidate == root | ||
| || candidate | ||
| .strip_prefix(root) | ||
| .is_some_and(|rest| rest.starts_with('/')) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.