返回 CodeWhale
tests.rs
根目录 / crates / tui / src / sandbox / seatbelt / tests.rs
1 use super::*;
2
3 /// #4085: an inherited read-write extension is necessary but never sufficient
4 /// for WorkspaceWrite. Every extension-backed write must also match one
5 /// approved root and retain that root's protected-subpath exclusions.
6 #[test]
7 fn file_provider_extensions_intersect_each_workspace_root_and_exclusions() {
8 assert!(
9 is_available(),
10 "UNRUN: macOS sandbox-exec is unavailable; generated policy was not parsed"
11 );
12
13 let fixture = tempfile::tempdir().expect("create policy fixture");
14 let workspace = fixture.path().join("workspace");
15 let additional_root = fixture.path().join("additional-root");
16 std::fs::create_dir_all(workspace.join(".codewhale")).expect("create protected workspace path");
17 std::fs::create_dir_all(additional_root.join(".deepseek"))
18 .expect("create protected additional-root path");
19
20 let policy = SandboxPolicy::WorkspaceWrite {
21 writable_roots: vec![additional_root],
22 network_access: false,
23 exclude_tmpdir: true,
24 exclude_slash_tmp: true,
25 };
26 let workspace_write = generate_policy(&policy, &workspace, &[]);
27 let read_only = generate_policy(&SandboxPolicy::ReadOnly, &workspace, &[]);
28
29 for policy in [&workspace_write, &read_only] {
30 assert!(policy.contains("(version 1)"));
31 assert!(policy.contains("(deny default)"));
32 assert!(policy.contains("(allow file-read*)"));
33 assert!(policy.contains(r#"(allow file-read* (extension "com.apple.app-sandbox.read"))"#));
34 assert!(
35 policy.contains(r#"(allow file-read* (extension "com.apple.app-sandbox.read-write"))"#)
36 );
37 }
38 assert!(!workspace_write.contains("network-outbound"));
39
40 for root_index in 0..=1 {
41 let expected = format!(
42 r#"(require-all (extension "com.apple.app-sandbox.read-write") (subpath (param "WRITABLE_ROOT_{root_index}")) (require-not (subpath (param "WRITABLE_ROOT_{root_index}_RO_0"))))"#
43 );
44 assert!(
45 workspace_write.contains(&expected),
46 "extension write must retain root {root_index} and its exclusion:\n{workspace_write}"
47 );
48 }
49 let read_write_extension = r#"(extension "com.apple.app-sandbox.read-write")"#;
50 assert_eq!(
51 workspace_write.matches(read_write_extension).count(),
52 3,
53 "one read rule plus exactly one root-scoped write rule per approved root"
54 );
55 assert!(workspace_write.contains("file-write*"));
56 assert!(!workspace_write.lines().any(|line| {
57 line.trim() == r#"(allow file-write* (extension "com.apple.app-sandbox.read-write"))"#
58 }));
59 assert!(!read_only.contains(r#"file-write* (extension "com.apple.app-sandbox.read-write")"#));
60 assert_eq!(read_only.matches(read_write_extension).count(), 1);
61 assert!(!read_only.contains("WRITABLE_ROOT"));
62
63 let args = create_seatbelt_args(vec!["/usr/bin/true".to_string()], &policy, &workspace, &[]);
64 let output = Command::new(SANDBOX_EXEC_PATH)
65 .args(args)
66 .current_dir(&workspace)
67 .output()
68 .expect("parse generated policy with sandbox-exec");
69 assert!(
70 output.status.success(),
71 "sandbox-exec rejected generated intersection policy: {}",
72 String::from_utf8_lossy(&output.stderr)
73 );
74 }
75
76 /// Hermetic command/policy-shape coverage for the six operations reported in
77 /// #4085. Each operation gets a fresh fixture so an early failure cannot hide
78 /// later results. This is not physical File Provider acceptance.
79 #[test]
80 fn file_provider_synthetic_operations_are_independent_under_seatbelt() {
81 assert!(
82 is_available(),
83 "UNRUN: macOS sandbox-exec is unavailable; no synthetic operation evidence collected"
84 );
85
86 #[derive(Clone, Copy, Debug)]
87 enum Operation {
88 Mkdir,
89 Write,
90 Read,
91 Grep,
92 DeleteFile,
93 DeleteDirectory,
94 }
95
96 let mut failures = Vec::new();
97 for operation in [
98 Operation::Mkdir,
99 Operation::Write,
100 Operation::Read,
101 Operation::Grep,
102 Operation::DeleteFile,
103 Operation::DeleteDirectory,
104 ] {
105 let fixture = tempfile::tempdir().expect("create independent operation fixture");
106 let workspace = fixture
107 .path()
108 .join("Library/CloudStorage/TestProvider/Workspace");
109 std::fs::create_dir_all(&workspace).expect("create synthetic CloudStorage workspace");
110 let target = workspace.join("target");
111 let source = workspace.join("source");
112
113 let command = match operation {
114 Operation::Mkdir => vec![
115 "/bin/mkdir".to_string(),
116 target.to_string_lossy().into_owned(),
117 ],
118 Operation::Write => {
119 std::fs::write(&source, b"file-provider\n").expect("seed copy source");
120 vec![
121 "/bin/cp".to_string(),
122 source.to_string_lossy().into_owned(),
123 target.to_string_lossy().into_owned(),
124 ]
125 }
126 Operation::Read => {
127 std::fs::write(&target, b"file-provider\n").expect("seed read target");
128 vec![
129 "/bin/cat".to_string(),
130 target.to_string_lossy().into_owned(),
131 ]
132 }
133 Operation::Grep => {
134 std::fs::write(&target, b"file-provider\n").expect("seed grep target");
135 vec![
136 "/usr/bin/grep".to_string(),
137 "-q".to_string(),
138 "file-provider".to_string(),
139 target.to_string_lossy().into_owned(),
140 ]
141 }
142 Operation::DeleteFile => {
143 std::fs::write(&target, b"file-provider\n").expect("seed deletion target");
144 vec!["/bin/rm".to_string(), target.to_string_lossy().into_owned()]
145 }
146 Operation::DeleteDirectory => {
147 std::fs::create_dir(&target).expect("seed directory deletion target");
148 vec![
149 "/bin/rmdir".to_string(),
150 target.to_string_lossy().into_owned(),
151 ]
152 }
153 };
154
155 let args = create_seatbelt_args(command, &SandboxPolicy::default(), &workspace, &[]);
156 let output = Command::new(SANDBOX_EXEC_PATH)
157 .args(args)
158 .current_dir(&workspace)
159 .output()
160 .expect("execute sandboxed operation");
161
162 let effect_matches = match operation {
163 Operation::Mkdir => target.is_dir(),
164 Operation::Write => {
165 matches!(std::fs::read(&target), Ok(bytes) if bytes == b"file-provider\n")
166 }
167 Operation::Read => output.stdout == b"file-provider\n",
168 Operation::Grep => true,
169 Operation::DeleteFile | Operation::DeleteDirectory => !target.exists(),
170 };
171 if !output.status.success() || !effect_matches {
172 failures.push(format!(
173 "{operation:?}: status={:?}, stderr={}",
174 output.status.code(),
175 String::from_utf8_lossy(&output.stderr)
176 ));
177 }
178 }
179
180 assert!(
181 failures.is_empty(),
182 "independent sandbox operations failed:\n{}",
183 failures.join("\n")
184 );
185 }
186
187 /// S1 (#5568): the opt-in read deny-list must actually stop reads that the
188 /// full-disk-read posture would otherwise allow — live, with sandbox-exec —
189 /// and an empty list must leave the generated profile byte-identical.
190 #[test]
191 fn denied_read_subpaths_block_reads_under_every_sandboxed_posture() {
192 assert!(
193 is_available(),
194 "UNRUN: macOS sandbox-exec is unavailable; no deny-list evidence collected"
195 );
196
197 let secret_dir = tempfile::tempdir().expect("secret dir");
198 let secret_file = secret_dir.path().join("id_ed25519");
199 std::fs::write(&secret_file, "PRIVATE KEY MATERIAL").expect("write secret");
200 let workspace = tempfile::tempdir().expect("workspace");
201 let read_cmd = vec![
202 "/bin/cat".to_string(),
203 secret_file.to_string_lossy().into_owned(),
204 ];
205
206 for policy in [SandboxPolicy::ReadOnly, SandboxPolicy::default()] {
207 // Without the deny-list the read succeeds (full-disk read posture).
208 let open_args = create_seatbelt_args(read_cmd.clone(), &policy, workspace.path(), &[]);
209 let open = Command::new(SANDBOX_EXEC_PATH)
210 .args(open_args)
211 .current_dir(workspace.path())
212 .output()
213 .expect("run un-denied read");
214 assert!(
215 open.status.success(),
216 "baseline read should pass under {policy:?}: {}",
217 String::from_utf8_lossy(&open.stderr)
218 );
219
220 // With the parent directory denied, the same read must fail. The
221 // rule set comes from the manager's setter, which canonicalizes:
222 // macOS tempdirs live behind the /var -> /private/var symlink, and
223 // Seatbelt matches the kernel-resolved path, so a literal-only rule
224 // silently never fires (caught live by this very test).
225 let mut manager = crate::sandbox::SandboxManager::new();
226 manager.set_denied_read_subpaths(vec![secret_dir.path().to_path_buf()]);
227 let denied_args = create_seatbelt_args(
228 read_cmd.clone(),
229 &policy,
230 workspace.path(),
231 manager.denied_read_subpaths_for_test(),
232 );
233 let denied = Command::new(SANDBOX_EXEC_PATH)
234 .args(denied_args)
235 .current_dir(workspace.path())
236 .output()
237 .expect("run denied read");
238 assert!(
239 !denied.status.success(),
240 "deny-listed read must fail under {policy:?}, stdout: {}",
241 String::from_utf8_lossy(&denied.stdout)
242 );
243 }
244 }
245
245 lines RUST