winx_code_agent/
sandbox.rs1#[cfg(target_os = "linux")]
24use std::path::Path;
25
26#[cfg(target_os = "linux")]
27use tracing::info;
28use tracing::warn;
29
30#[cfg(target_os = "linux")]
31const DEFAULT_RO_PATHS: &[&str] =
32 &["/usr", "/bin", "/sbin", "/lib", "/lib64", "/lib32", "/etc", "/proc", "/sys", "/run", "/opt"];
33
34#[cfg(target_os = "linux")]
35const DEFAULT_RW_PATHS: &[&str] = &["/tmp", "/dev", "/var/tmp"];
36
37pub fn apply_if_requested() {
42 if !crate::config::env_flag("WINX_SANDBOX") {
43 return;
44 }
45
46 let workspace_root = match std::env::current_dir() {
47 Ok(dir) => dir,
48 Err(e) => {
49 warn!(
50 "WINX_SANDBOX: cannot determine the cwd ({e}); the workspace may not be writable \
51 under the sandbox - start winx from your project directory."
52 );
53 std::path::PathBuf::from(".")
54 }
55 };
56
57 #[cfg(not(target_os = "linux"))]
58 {
59 let _ = &workspace_root;
60 warn!("WINX_SANDBOX is set but Landlock is Linux-only; continuing UNSANDBOXED.");
61 }
62
63 #[cfg(target_os = "linux")]
64 match apply_linux(&workspace_root) {
65 Ok(status) => info!(
66 "WINX_SANDBOX active (Landlock {status:?}); writes confined to the workspace + /tmp, \
67 home not readable. Extend via WINX_SANDBOX_RO_PATHS / WINX_SANDBOX_RW_PATHS."
68 ),
69 Err(e) => warn!("WINX_SANDBOX: could not apply Landlock ({e}); continuing UNSANDBOXED."),
70 }
71}
72
73#[cfg(target_os = "linux")]
74fn apply_linux(workspace_root: &Path) -> anyhow::Result<landlock::RulesetStatus> {
75 use landlock::{Access, AccessFs, Ruleset, RulesetAttr, ABI};
76
77 let abi = ABI::V1;
80 let ro = AccessFs::from_read(abi);
81 let rw = AccessFs::from_all(abi);
82
83 let mut ruleset = Ruleset::default().handle_access(rw)?.create()?;
87
88 for path in DEFAULT_RO_PATHS {
89 ruleset = add_path(ruleset, Path::new(path), ro)?;
90 }
91 for path in env_paths("WINX_SANDBOX_RO_PATHS") {
92 ruleset = add_path(ruleset, &path, ro)?;
93 }
94 for path in DEFAULT_RW_PATHS {
95 ruleset = add_path(ruleset, Path::new(path), rw)?;
96 }
97 for path in env_paths("WINX_SANDBOX_RW_PATHS") {
98 ruleset = add_path(ruleset, &path, rw)?;
99 }
100 ruleset = add_path(ruleset, workspace_root, rw)?;
102
103 Ok(ruleset.restrict_self()?.ruleset)
104}
105
106#[cfg(target_os = "linux")]
109fn add_path(
110 ruleset: landlock::RulesetCreated,
111 path: &Path,
112 access: landlock::BitFlags<landlock::AccessFs>,
113) -> anyhow::Result<landlock::RulesetCreated> {
114 use landlock::{PathBeneath, PathFd, RulesetCreatedAttr};
115 match PathFd::new(path) {
116 Ok(fd) => Ok(ruleset.add_rule(PathBeneath::new(fd, access))?),
117 Err(_) => Ok(ruleset),
118 }
119}
120
121#[cfg(target_os = "linux")]
123fn env_paths(var: &str) -> Vec<std::path::PathBuf> {
124 parse_path_list(&std::env::var(var).unwrap_or_default())
125}
126
127#[cfg(target_os = "linux")]
130fn parse_path_list(value: &str) -> Vec<std::path::PathBuf> {
131 value.split(':').filter(|segment| !segment.is_empty()).map(std::path::PathBuf::from).collect()
132}
133
134#[cfg(all(test, target_os = "linux"))]
135mod tests {
136 use super::parse_path_list;
137 use std::path::PathBuf;
138
139 #[test]
140 fn parse_path_list_splits_and_drops_empties() {
141 assert!(parse_path_list("").is_empty());
142 assert_eq!(parse_path_list("/a:/b"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
143 assert_eq!(parse_path_list("/a::/b:"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
144 }
145}