|
4 | 4 |
|
5 | 5 | use std::env::consts::ARCH;
|
6 | 6 |
|
7 |
| -use anyhow::Result; |
| 7 | +use anyhow::{bail, ensure, Result}; |
8 | 8 | use cap_std::fs::Dir;
|
9 | 9 | use cap_std_ext::cap_std;
|
10 | 10 | use cap_std_ext::dirext::CapStdExtDirExt as _;
|
11 | 11 | use fn_error_context::context;
|
12 | 12 |
|
| 13 | +use crate::utils::openat2_with_retry; |
| 14 | + |
13 | 15 | /// check for the existence of the /var/run directory
|
14 | 16 | /// if it exists we need to check that it links to /run if not error
|
15 | 17 | /// if it does not exist error.
|
16 | 18 | #[context("Linting")]
|
17 | 19 | pub(crate) fn lint(root: &Dir) -> Result<()> {
|
18 |
| - let lints = [check_var_run, check_kernel, check_parse_kargs, check_usretc]; |
| 20 | + let lints = [ |
| 21 | + check_var_run, |
| 22 | + check_kernel, |
| 23 | + check_parse_kargs, |
| 24 | + check_usretc, |
| 25 | + check_utf8, |
| 26 | + ]; |
19 | 27 | for lint in lints {
|
20 | 28 | lint(&root)?;
|
21 | 29 | }
|
@@ -59,12 +67,72 @@ fn check_kernel(root: &Dir) -> Result<()> {
|
59 | 67 | Ok(())
|
60 | 68 | }
|
61 | 69 |
|
| 70 | +/// Open the target directory, but return Ok(None) if this would cross a mount point. |
| 71 | +fn open_dir_noxdev( |
| 72 | + parent: &Dir, |
| 73 | + path: impl AsRef<std::path::Path>, |
| 74 | +) -> std::io::Result<Option<Dir>> { |
| 75 | + use rustix::fs::{Mode, OFlags, ResolveFlags}; |
| 76 | + match openat2_with_retry( |
| 77 | + parent, |
| 78 | + path, |
| 79 | + OFlags::CLOEXEC | OFlags::DIRECTORY | OFlags::NOFOLLOW, |
| 80 | + Mode::empty(), |
| 81 | + ResolveFlags::NO_XDEV | ResolveFlags::BENEATH, |
| 82 | + ) { |
| 83 | + Ok(r) => Ok(Some(Dir::reopen_dir(&r)?)), |
| 84 | + Err(e) if e == rustix::io::Errno::XDEV => Ok(None), |
| 85 | + Err(e) => return Err(e.into()), |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +fn check_utf8(dir: &Dir) -> Result<()> { |
| 90 | + for entry in dir.entries()? { |
| 91 | + let entry = entry?; |
| 92 | + let name = entry.file_name(); |
| 93 | + |
| 94 | + let Some(strname) = &name.to_str() else { |
| 95 | + // will escape nicely like "abc\xFFdéf" |
| 96 | + bail!("/: Found non-utf8 filename {name:?}"); |
| 97 | + }; |
| 98 | + |
| 99 | + let ifmt = entry.file_type()?; |
| 100 | + if ifmt.is_symlink() { |
| 101 | + let target = dir.read_link_contents(&name)?; |
| 102 | + ensure!( |
| 103 | + target.to_str().is_some(), |
| 104 | + "/{strname}: Found non-utf8 symlink target" |
| 105 | + ); |
| 106 | + } else if ifmt.is_dir() { |
| 107 | + let Some(subdir) = open_dir_noxdev(dir, entry.file_name())? else { |
| 108 | + continue; |
| 109 | + }; |
| 110 | + if let Err(err) = check_utf8(&subdir) { |
| 111 | + // Try to do the path pasting only in the event of an error |
| 112 | + bail!("/{strname}{err:?}"); |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + Ok(()) |
| 117 | +} |
| 118 | + |
62 | 119 | #[cfg(test)]
|
63 | 120 | fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
|
64 | 121 | let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
|
65 | 122 | Ok(tempdir)
|
66 | 123 | }
|
67 | 124 |
|
| 125 | +#[test] |
| 126 | +fn test_open_noxdev() -> Result<()> { |
| 127 | + let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; |
| 128 | + // This hard requires the host setup to have /usr/bin on the same filesystem as / |
| 129 | + let usr = Dir::open_ambient_dir("/usr", cap_std::ambient_authority())?; |
| 130 | + assert!(open_dir_noxdev(&usr, "bin").unwrap().is_some()); |
| 131 | + // Requires a mounted /proc, but that also seems ane. |
| 132 | + assert!(open_dir_noxdev(&root, "proc").unwrap().is_none()); |
| 133 | + Ok(()) |
| 134 | +} |
| 135 | + |
68 | 136 | #[test]
|
69 | 137 | fn test_var_run() -> Result<()> {
|
70 | 138 | let root = &fixture()?;
|
@@ -117,3 +185,75 @@ fn test_usr_etc() -> Result<()> {
|
117 | 185 | check_usretc(root).unwrap();
|
118 | 186 | Ok(())
|
119 | 187 | }
|
| 188 | + |
| 189 | +#[test] |
| 190 | +fn test_non_utf8() { |
| 191 | + use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; |
| 192 | + |
| 193 | + let root = &fixture().unwrap(); |
| 194 | + |
| 195 | + // Try to create some adversarial symlink situations to ensure the walk doesn't crash |
| 196 | + root.create_dir("subdir").unwrap(); |
| 197 | + // Self-referential symlinks |
| 198 | + root.symlink("self", "self").unwrap(); |
| 199 | + // Infinitely looping dir symlinks |
| 200 | + root.symlink("..", "subdir/parent").unwrap(); |
| 201 | + // Broken symlinks |
| 202 | + root.symlink("does-not-exist", "broken").unwrap(); |
| 203 | + // Out-of-scope symlinks |
| 204 | + root.symlink("../../x", "escape").unwrap(); |
| 205 | + // Should be fine |
| 206 | + check_utf8(root).unwrap(); |
| 207 | + |
| 208 | + // But this will cause an issue |
| 209 | + let baddir = OsStr::from_bytes(b"subdir/2/bad\xffdir"); |
| 210 | + root.create_dir("subdir/2").unwrap(); |
| 211 | + root.create_dir(baddir).unwrap(); |
| 212 | + let Err(err) = check_utf8(root) else { |
| 213 | + unreachable!("Didn't fail"); |
| 214 | + }; |
| 215 | + assert_eq!( |
| 216 | + err.to_string(), |
| 217 | + r#"/subdir/2/: Found non-utf8 filename "bad\xFFdir""# |
| 218 | + ); |
| 219 | + root.remove_dir(baddir).unwrap(); // Get rid of the problem |
| 220 | + check_utf8(root).unwrap(); // Check it |
| 221 | + |
| 222 | + // Create a new problem in the form of a regular file |
| 223 | + let badfile = OsStr::from_bytes(b"regular\xff"); |
| 224 | + root.write(badfile, b"Hello, world!\n").unwrap(); |
| 225 | + let Err(err) = check_utf8(root) else { |
| 226 | + unreachable!("Didn't fail"); |
| 227 | + }; |
| 228 | + assert_eq!( |
| 229 | + err.to_string(), |
| 230 | + r#"/: Found non-utf8 filename "regular\xFF""# |
| 231 | + ); |
| 232 | + root.remove_file(badfile).unwrap(); // Get rid of the problem |
| 233 | + check_utf8(root).unwrap(); // Check it |
| 234 | + |
| 235 | + // And now test invalid symlink targets |
| 236 | + root.symlink(badfile, "subdir/good-name").unwrap(); |
| 237 | + let Err(err) = check_utf8(root) else { |
| 238 | + unreachable!("Didn't fail"); |
| 239 | + }; |
| 240 | + assert_eq!( |
| 241 | + err.to_string(), |
| 242 | + r#"/subdir/good-name: Found non-utf8 symlink target"# |
| 243 | + ); |
| 244 | + root.remove_file("subdir/good-name").unwrap(); // Get rid of the problem |
| 245 | + check_utf8(root).unwrap(); // Check it |
| 246 | + |
| 247 | + // Finally, test a self-referential symlink with an invalid name. |
| 248 | + // We should spot the invalid name before we check the target. |
| 249 | + root.symlink(badfile, badfile).unwrap(); |
| 250 | + let Err(err) = check_utf8(root) else { |
| 251 | + unreachable!("Didn't fail"); |
| 252 | + }; |
| 253 | + assert_eq!( |
| 254 | + err.to_string(), |
| 255 | + r#"/: Found non-utf8 filename "regular\xFF""# |
| 256 | + ); |
| 257 | + root.remove_file(badfile).unwrap(); // Get rid of the problem |
| 258 | + check_utf8(root).unwrap(); // Check it |
| 259 | +} |
0 commit comments