|
1 | 1 | --- upstream/linux-sandbox/src/bwrap.rs |
2 | 2 | +++ upstream/linux-sandbox/src/bwrap.rs |
3 | | -@@ -474,6 +474,20 @@ |
| 3 | +@@ -485,6 +485,20 @@ |
| 4 | + .filter(|path| path.exists()), |
4 | 5 | ); |
5 | 6 | } |
6 | | - |
| 7 | ++ |
7 | 8 | + // Skip subpaths covered by another entry. `--ro-bind` is |
8 | 9 | + // recursive, so the extra bind is redundant, and a subpath |
9 | 10 | + // symlink like /etc/os-release can resolve into a path whose |
10 | 11 | + // prefix is not yet bound, making bwrap fail with ENOENT. |
11 | 12 | + let readable_roots: BTreeSet<PathBuf> = readable_roots |
12 | 13 | + .iter() |
13 | 14 | + .filter(|path| { |
14 | | -+ !readable_roots.iter().any(|parent| { |
15 | | -+ parent.as_path() != path.as_path() && path.starts_with(parent) |
16 | | -+ }) |
| 15 | ++ !readable_roots |
| 16 | ++ .iter() |
| 17 | ++ .any(|parent| parent.as_path() != path.as_path() && path.starts_with(parent)) |
17 | 18 | + }) |
18 | 19 | + .cloned() |
19 | 20 | + .collect(); |
20 | | -+ |
| 21 | + |
21 | 22 | // A restricted policy can still explicitly request `/`, which is |
22 | 23 | // the broad read baseline. Explicit unreadable carveouts are |
23 | | - // re-applied later. |
24 | | -@@ -1072,10 +1086,11 @@ |
| 24 | +@@ -1071,7 +1085,7 @@ |
| 25 | + if let Some(first_missing_component) = find_first_non_existent_component(subpath) |
| 26 | + && is_within_allowed_write_paths(&first_missing_component, allowed_write_paths) |
| 27 | + { |
| 28 | +- append_missing_read_only_subpath_args(bwrap_args, &first_missing_component)?; |
| 29 | ++ append_missing_read_only_subpath_args(bwrap_args, subpath, &first_missing_component)?; |
| 30 | + } |
| 31 | + return Ok(()); |
| 32 | + } |
| 33 | +@@ -1085,10 +1099,11 @@ |
25 | 34 | } |
26 | | - |
| 35 | + |
27 | 36 | fn append_empty_file_bind_data_args(bwrap_args: &mut BwrapArgs, path: &Path) -> Result<()> { |
28 | 37 | - if bwrap_args.preserved_files.is_empty() { |
29 | 38 | - bwrap_args.preserved_files.push(File::open("/dev/null")?); |
|
37 | 46 | bwrap_args.args.push("--ro-bind-data".to_string()); |
38 | 47 | bwrap_args.args.push(null_fd); |
39 | 48 | bwrap_args.args.push(path_to_string(path)); |
| 49 | +@@ -1104,8 +1119,12 @@ |
| 50 | + bwrap_args.args.push(path_to_string(path)); |
| 51 | + } |
| 52 | + |
| 53 | +-fn append_missing_read_only_subpath_args(bwrap_args: &mut BwrapArgs, path: &Path) -> Result<()> { |
| 54 | +- if path.file_name().is_some_and(is_protected_metadata_name) { |
| 55 | ++fn append_missing_read_only_subpath_args( |
| 56 | ++ bwrap_args: &mut BwrapArgs, |
| 57 | ++ path: &Path, |
| 58 | ++ first_missing_component: &Path, |
| 59 | ++) -> Result<()> { |
| 60 | ++ if path == first_missing_component && path.file_name().is_some_and(is_protected_metadata_name) { |
| 61 | + append_empty_directory_args(bwrap_args, path); |
| 62 | + bwrap_args |
| 63 | + .synthetic_mount_targets |
| 64 | +@@ -1113,6 +1132,7 @@ |
| 65 | + return Ok(()); |
| 66 | + } |
| 67 | + |
| 68 | ++ append_missing_mount_parent_dir_args(bwrap_args, path, first_missing_component); |
| 69 | + append_missing_empty_file_bind_data_args(bwrap_args, path) |
| 70 | + } |
| 71 | + |
| 72 | +@@ -1175,12 +1195,64 @@ |
| 73 | + if let Some(first_missing_component) = find_first_non_existent_component(unreadable_root) |
| 74 | + && is_within_allowed_write_paths(&first_missing_component, allowed_write_paths) |
| 75 | + { |
| 76 | +- append_missing_empty_file_bind_data_args(bwrap_args, &first_missing_component)?; |
| 77 | ++ append_missing_unreadable_root_args( |
| 78 | ++ bwrap_args, |
| 79 | ++ unreadable_root, |
| 80 | ++ &first_missing_component, |
| 81 | ++ )?; |
| 82 | + } |
| 83 | + return Ok(()); |
| 84 | + } |
| 85 | + |
| 86 | + append_existing_unreadable_path_args(bwrap_args, unreadable_root, allowed_write_paths) |
| 87 | ++} |
| 88 | ++ |
| 89 | ++fn append_missing_unreadable_root_args( |
| 90 | ++ bwrap_args: &mut BwrapArgs, |
| 91 | ++ unreadable_root: &Path, |
| 92 | ++ first_missing_component: &Path, |
| 93 | ++) -> Result<()> { |
| 94 | ++ append_missing_mount_parent_dir_args(bwrap_args, unreadable_root, first_missing_component); |
| 95 | ++ append_missing_empty_file_bind_data_args(bwrap_args, unreadable_root) |
| 96 | ++} |
| 97 | ++ |
| 98 | ++fn append_missing_mount_parent_dir_args( |
| 99 | ++ bwrap_args: &mut BwrapArgs, |
| 100 | ++ mount_target: &Path, |
| 101 | ++ first_missing_component: &Path, |
| 102 | ++) { |
| 103 | ++ let Some(parent) = mount_target.parent() else { |
| 104 | ++ return; |
| 105 | ++ }; |
| 106 | ++ if !parent.starts_with(first_missing_component) { |
| 107 | ++ return; |
| 108 | ++ } |
| 109 | ++ |
| 110 | ++ let mut dir = first_missing_component.to_path_buf(); |
| 111 | ++ append_missing_mount_parent_dir(bwrap_args, &dir); |
| 112 | ++ |
| 113 | ++ let Ok(relative_parent) = parent.strip_prefix(first_missing_component) else { |
| 114 | ++ return; |
| 115 | ++ }; |
| 116 | ++ for component in relative_parent.components() { |
| 117 | ++ use std::path::Component; |
| 118 | ++ match component { |
| 119 | ++ Component::Normal(part) => { |
| 120 | ++ dir.push(part); |
| 121 | ++ append_missing_mount_parent_dir(bwrap_args, &dir); |
| 122 | ++ } |
| 123 | ++ Component::CurDir => {} |
| 124 | ++ Component::ParentDir | Component::RootDir | Component::Prefix(_) => return, |
| 125 | ++ } |
| 126 | ++ } |
| 127 | ++} |
| 128 | ++ |
| 129 | ++fn append_missing_mount_parent_dir(bwrap_args: &mut BwrapArgs, path: &Path) { |
| 130 | ++ bwrap_args.args.push("--dir".to_string()); |
| 131 | ++ bwrap_args.args.push(path_to_string(path)); |
| 132 | ++ bwrap_args |
| 133 | ++ .synthetic_mount_targets |
| 134 | ++ .push(SyntheticMountTarget::missing_empty_directory(path)); |
| 135 | + } |
| 136 | + |
| 137 | + fn append_existing_unreadable_path_args( |
| 138 | +@@ -1749,6 +1821,78 @@ |
| 139 | + } |
| 140 | + |
| 141 | + #[test] |
| 142 | ++ fn missing_nested_read_only_subpath_masks_leaf_not_first_missing_ancestor() { |
| 143 | ++ let temp_dir = TempDir::new().expect("temp dir"); |
| 144 | ++ let home = temp_dir.path().join("home"); |
| 145 | ++ let first_missing = home.join(".local"); |
| 146 | ++ let missing_parent = first_missing.join("share"); |
| 147 | ++ let blocked = missing_parent.join("keyrings"); |
| 148 | ++ std::fs::create_dir_all(&home).expect("create home"); |
| 149 | ++ |
| 150 | ++ let home_root = AbsolutePathBuf::from_absolute_path(&home).expect("absolute home"); |
| 151 | ++ let blocked_root = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked"); |
| 152 | ++ let policy = FileSystemSandboxPolicy::restricted(vec![ |
| 153 | ++ FileSystemSandboxEntry { |
| 154 | ++ path: FileSystemPath::Path { path: home_root }, |
| 155 | ++ access: FileSystemAccessMode::Write, |
| 156 | ++ }, |
| 157 | ++ FileSystemSandboxEntry { |
| 158 | ++ path: FileSystemPath::Path { path: blocked_root }, |
| 159 | ++ access: FileSystemAccessMode::Read, |
| 160 | ++ }, |
| 161 | ++ ]); |
| 162 | ++ |
| 163 | ++ let args = |
| 164 | ++ create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) |
| 165 | ++ .expect("filesystem args"); |
| 166 | ++ |
| 167 | ++ assert_dir_created(&args.args, &first_missing); |
| 168 | ++ assert_dir_created(&args.args, &missing_parent); |
| 169 | ++ assert_empty_file_bound_without_perms(&args.args, &blocked); |
| 170 | ++ assert_no_empty_file_bind(&args.args, &first_missing); |
| 171 | ++ let synthetic_targets = synthetic_mount_target_paths(&args); |
| 172 | ++ assert!(synthetic_targets.contains(&first_missing)); |
| 173 | ++ assert!(synthetic_targets.contains(&missing_parent)); |
| 174 | ++ assert!(synthetic_targets.contains(&blocked)); |
| 175 | ++ } |
| 176 | ++ |
| 177 | ++ #[test] |
| 178 | ++ fn missing_nested_unreadable_root_masks_leaf_not_first_missing_ancestor() { |
| 179 | ++ let temp_dir = TempDir::new().expect("temp dir"); |
| 180 | ++ let home = temp_dir.path().join("home"); |
| 181 | ++ let first_missing = home.join(".local"); |
| 182 | ++ let missing_parent = first_missing.join("share"); |
| 183 | ++ let blocked = missing_parent.join("keyrings"); |
| 184 | ++ std::fs::create_dir_all(&home).expect("create home"); |
| 185 | ++ |
| 186 | ++ let home_root = AbsolutePathBuf::from_absolute_path(&home).expect("absolute home"); |
| 187 | ++ let blocked_root = AbsolutePathBuf::from_absolute_path(&blocked).expect("absolute blocked"); |
| 188 | ++ let policy = FileSystemSandboxPolicy::restricted(vec![ |
| 189 | ++ FileSystemSandboxEntry { |
| 190 | ++ path: FileSystemPath::Path { path: home_root }, |
| 191 | ++ access: FileSystemAccessMode::Write, |
| 192 | ++ }, |
| 193 | ++ FileSystemSandboxEntry { |
| 194 | ++ path: FileSystemPath::Path { path: blocked_root }, |
| 195 | ++ access: FileSystemAccessMode::None, |
| 196 | ++ }, |
| 197 | ++ ]); |
| 198 | ++ |
| 199 | ++ let args = |
| 200 | ++ create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) |
| 201 | ++ .expect("filesystem args"); |
| 202 | ++ |
| 203 | ++ assert_dir_created(&args.args, &first_missing); |
| 204 | ++ assert_dir_created(&args.args, &missing_parent); |
| 205 | ++ assert_empty_file_bound_without_perms(&args.args, &blocked); |
| 206 | ++ assert_no_empty_file_bind(&args.args, &first_missing); |
| 207 | ++ let synthetic_targets = synthetic_mount_target_paths(&args); |
| 208 | ++ assert!(synthetic_targets.contains(&first_missing)); |
| 209 | ++ assert!(synthetic_targets.contains(&missing_parent)); |
| 210 | ++ assert!(synthetic_targets.contains(&blocked)); |
| 211 | ++ } |
| 212 | ++ |
| 213 | ++ #[test] |
| 214 | + fn transient_empty_preserved_file_uses_empty_file_bind_data() { |
| 215 | + let temp_dir = TempDir::new().expect("temp dir"); |
| 216 | + let workspace = temp_dir.path().join("workspace"); |
| 217 | +@@ -1796,6 +1940,40 @@ |
| 218 | + } |
| 219 | + |
| 220 | + #[test] |
| 221 | ++ fn multiple_existing_empty_file_masks_use_distinct_preserved_fds() { |
| 222 | ++ let temp_dir = TempDir::new().expect("temp dir"); |
| 223 | ++ let azure = temp_dir.path().join(".azure"); |
| 224 | ++ let gcloud = temp_dir.path().join(".gcloud"); |
| 225 | ++ File::create(&azure).expect("create empty azure file"); |
| 226 | ++ File::create(&gcloud).expect("create empty gcloud file"); |
| 227 | ++ |
| 228 | ++ let azure_root = AbsolutePathBuf::from_absolute_path(&azure).expect("absolute azure"); |
| 229 | ++ let gcloud_root = AbsolutePathBuf::from_absolute_path(&gcloud).expect("absolute gcloud"); |
| 230 | ++ let policy = FileSystemSandboxPolicy::restricted(vec![ |
| 231 | ++ FileSystemSandboxEntry { |
| 232 | ++ path: FileSystemPath::Path { path: azure_root }, |
| 233 | ++ access: FileSystemAccessMode::None, |
| 234 | ++ }, |
| 235 | ++ FileSystemSandboxEntry { |
| 236 | ++ path: FileSystemPath::Path { path: gcloud_root }, |
| 237 | ++ access: FileSystemAccessMode::None, |
| 238 | ++ }, |
| 239 | ++ ]); |
| 240 | ++ |
| 241 | ++ let args = |
| 242 | ++ create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) |
| 243 | ++ .expect("filesystem args"); |
| 244 | ++ |
| 245 | ++ assert_file_masked(&args.args, &azure); |
| 246 | ++ assert_file_masked(&args.args, &gcloud); |
| 247 | ++ assert_eq!( |
| 248 | ++ args.preserved_files.len(), |
| 249 | ++ 2, |
| 250 | ++ "each ro-bind-data mount needs its own fd" |
| 251 | ++ ); |
| 252 | ++ } |
| 253 | ++ |
| 254 | ++ #[test] |
| 255 | + fn missing_child_git_under_parent_repo_uses_protected_create_target() { |
| 256 | + let temp_dir = TempDir::new().expect("temp dir"); |
| 257 | + let repo = temp_dir.path().join("repo"); |
| 258 | +@@ -2687,9 +2865,28 @@ |
| 259 | + && window[4] == path |
| 260 | + }), |
| 261 | + "missing path bind should not set explicit file perms for {path}: {args:#?}" |
| 262 | ++ ); |
| 263 | ++ } |
| 264 | ++ |
| 265 | ++ fn assert_no_empty_file_bind(args: &[String], path: &Path) { |
| 266 | ++ let path = path_to_string(path); |
| 267 | ++ assert!( |
| 268 | ++ !args |
| 269 | ++ .windows(3) |
| 270 | ++ .any(|window| { window[0] == "--ro-bind-data" && window[2] == path }), |
| 271 | ++ "did not expect empty file bind for {path}: {args:#?}" |
| 272 | + ); |
| 273 | + } |
| 274 | + |
| 275 | ++ fn assert_dir_created(args: &[String], path: &Path) { |
| 276 | ++ let path = path_to_string(path); |
| 277 | ++ assert!( |
| 278 | ++ args.windows(2) |
| 279 | ++ .any(|window| window == ["--dir", path.as_str()]), |
| 280 | ++ "expected synthetic directory creation for {path}: {args:#?}" |
| 281 | ++ ); |
| 282 | ++ } |
| 283 | ++ |
| 284 | + fn assert_empty_directory_mounted_read_only(args: &[String], path: &Path) { |
| 285 | + let path = path_to_string(path); |
| 286 | + assert!( |
0 commit comments