//! pathtest -- Linux's path syscalls: C strings, `struct stat`, *at() (stage 15.1). //! //! Since 15.1 every path argument is what C passes: a pointer to a //! NUL-terminated string (before, our own programs passed pointer + //! length, which no libc can). The kernel copies it in carefully, so: //! - a NULL, kernel-space, or unmapped pointer is EFAULT (not a crash), //! also when the string runs off the end of a mapped page; //! - more than PATH_MAX (4096) bytes without a NUL is ENAMETOOLONG; //! - "" is ENOENT. //! Then: stat/fstat/lstat/newfstatat fill in Linux's 144-byte `struct //! stat`, and the *at() calls take paths relative to a directory fd. //! Exit code = number of failed checks. #![no_std] #![no_main] #[macro_use] #[path = "ulib.rs"] mod ulib; extern crate alloc; use alloc::string::String; use alloc::vec; use ulib::*; /// A kernel address: user code may never pass one of these. const KERNEL_PTR: u64 = 0xffff_8000_0000_0000; fn sys(nr: u64, a: u64, b: u64, c: u64) -> i64 { syscall3(nr, a, b, c) } fn main() -> i64 { let mut c = Checks::new("pathtest"); let mut st = Stat::default(); let stp = &mut st as *mut Stat as u64; // --- bad pointers: EFAULT, from every path call --------------------------- for &(bad, what) in &[(0u64, "NULL"), (KERNEL_PTR, "a kernel address"), (0xf0_0000_0000, "an unmapped user address")] { let calls = [ ("open", sys(SYS_OPEN, bad, 0, 0)), ("openat", syscall5(SYS_OPENAT, AT_FDCWD as u64, bad, 0, 0, 0)), ("stat", sys(SYS_STAT, bad, stp, 0)), ("lstat", sys(SYS_LSTAT, bad, stp, 0)), ("newfstatat", syscall5(SYS_NEWFSTATAT, AT_FDCWD as u64, bad, stp, 0, 0)), ("access", sys(SYS_ACCESS, bad, 0, 0)), ("mkdir", sys(SYS_MKDIR, bad, 0o755, 0)), ("rmdir", sys(SYS_RMDIR, bad, 0, 0)), ("unlink", sys(SYS_UNLINK, bad, 0, 0)), ("rename", sys(SYS_RENAME, bad, bad, 0)), ("chdir", sys(SYS_CHDIR, bad, 0, 0)), ("readlink", sys(SYS_READLINK, bad, stp, 64)), ("execve", sys(SYS_EXECVE, bad, 0, 0)), ]; let failed: alloc::vec::Vec<&str> = calls.iter().filter(|(_, r)| *r != -EFAULT).map(|(n, _)| *n).collect(); if !failed.is_empty() { println!("pathtest: not EFAULT for {}: {:?}", what, failed); } c.check(failed.is_empty(), &alloc::format!("13 path calls with {} as the path: EFAULT", what)); } // A path that runs off the end of a mapped page into an unmapped one: // the kernel must notice at the page boundary. let two = mmap(0, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) as u64; munmap(two + 4096, 4096); let tail = two + 4096 - 8; unsafe { core::ptr::copy_nonoverlapping(b"/tmp/abc".as_ptr(), tail as *mut u8, 8) }; // no NUL c.check(sys(SYS_STAT, tail, stp, 0) == -EFAULT, "a path running into an unmapped page: EFAULT"); unsafe { *((two + 4095) as *mut u8) = 0 }; // now it ends just in time c.check(sys(SYS_STAT, tail, stp, 0) == -ENOENT, "the same path ending right at the page end: ENOENT"); munmap(two, 4096); // Good path, bad `struct stat` pointer. c.check(sys(SYS_STAT, c"/".as_ptr() as u64, KERNEL_PTR, 0) == -EFAULT, "stat into a kernel address: EFAULT"); c.check(sys(SYS_FSTAT, 0, 0, 0) == -EFAULT, "fstat into NULL: EFAULT"); // --- lengths ------------------------------------------------------------------ let mut long = vec![b'a'; 5000]; // no NUL in the first 4096 bytes long.push(0); c.check(sys(SYS_STAT, long.as_ptr() as u64, stp, 0) == -ENAMETOOLONG, "no NUL within PATH_MAX: ENAMETOOLONG"); // 4095 bytes + NUL is allowed (short components, so it's just missing). let mut max = String::new(); while max.len() < 4095 { max.push_str("/d"); } max.truncate(4095); c.check(stat(&max, &mut st) == -ENOENT, "a 4095-byte path is accepted (ENOENT)"); c.check(sys(SYS_STAT, c"".as_ptr() as u64, stp, 0) == -ENOENT, "an empty path: ENOENT"); // --- Linux's struct stat ---------------------------------------------------- c.check(core::mem::size_of::() == 144, "struct stat is 144 bytes"); c.check(stat("/mnt/disk0/bin", &mut st) == 0 && st.is_dir() && st.nlink == 2, "a directory: S_IFDIR, 2 links"); let r = stat("/mnt/disk0/bin/pathtest", &mut st); let ino = st.ino; c.check( r == 0 && st.is_file() && st.size > 0 && st.blksize == 4096 && st.blocks == (st.size + 511) / 512 && st.ino != 0, "a file: S_IFREG, size, blksize 4096, 512-byte blocks, an inode number", ); c.check(st.uid == 0 && st.gid == 0 && st.perm() != 0, "owned by root, with permission bits"); let fd = open("/mnt/disk0/bin/pathtest", O_RDONLY); let mut st2 = Stat::default(); c.check(fstat(fd, &mut st2) == 0 && st2.ino == ino && st2.size == st.size, "fstat on it: same inode and size"); let mut st3 = Stat::default(); c.check(fstatat(fd, "", &mut st3, AT_EMPTY_PATH) == 0 && st3.ino == ino, "newfstatat(fd, \"\", AT_EMPTY_PATH) = fstat"); c.check(fstatat(fd, "x", &mut st3, 0) == -ENOTDIR, "a file as dirfd: ENOTDIR"); close(fd); c.check(fstat(0, &mut st) == 0 && st.is_char_device() && isatty(0), "stdin: a character device and a tty"); let (r, w) = pipe().unwrap(); c.check(fstat(r, &mut st) == 0 && st.file_type() == S_IFIFO, "a pipe: S_IFIFO"); close(r); close(w); // Symlinks: stat follows, lstat doesn't (on the ext2 disk, if it's there). let link = (0..4).map(|i| alloc::format!("/mnt/disk{}/hello-link", i)).find(|p| lstat(p, &mut st) == 0); if let Some(link) = link { c.check(lstat(&link, &mut st) == 0 && st.is_symlink() && st.size == 9, "lstat on a symlink: S_IFLNK, size = target length"); c.check(stat(&link, &mut st) == 0 && st.is_file(), "stat on it: the file it points to"); let mut buf = [0u8; 64]; let n = readlink(&link, &mut buf); c.check(n == 9 && &buf[..9] == b"hello.txt", "readlink: the target, no NUL"); let dir = &link[..link.rfind('/').unwrap()]; let dfd = open(dir, O_RDONLY | O_DIRECTORY); let n = syscall5(SYS_READLINKAT, dfd as u64, c"hello-link".as_ptr() as u64, buf.as_mut_ptr() as u64, 64, 0); c.check(n == 9, "readlinkat(dirfd, \"hello-link\")"); c.check(fstatat(dfd, "hello-link", &mut st, AT_SYMLINK_NOFOLLOW) == 0 && st.is_symlink(), "newfstatat AT_SYMLINK_NOFOLLOW = lstat"); close(dfd); } else { println!("pathtest: no ext2 disk with hello-link: skipped symlink checks"); } c.check(readlink("/mnt/disk0/bin", &mut [0u8; 8]) == -EINVAL, "readlink on a non-link: EINVAL"); // --- open flags ---------------------------------------------------------------- c.check(open("/mnt/disk0/bin/pathtest", O_RDONLY | O_DIRECTORY) == -ENOTDIR, "O_DIRECTORY on a file: ENOTDIR"); c.check(open("/tmp", O_WRONLY) == -EISDIR, "a directory opened for writing: EISDIR"); let fd = open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL); c.check(fd >= 0 && open("/tmp/pt-excl", O_WRONLY | O_CREAT | O_EXCL) == -EEXIST, "O_CREAT|O_EXCL: creates, then EEXIST"); close(fd); c.check(access("/tmp/pt-excl") == 0 && access("/tmp/nope") == -ENOENT, "access: 0 / ENOENT"); // --- *at() relative to a directory fd ------------------------------------------ c.check(mkdir("/tmp/ptdir") == 0, "mkdir /tmp/ptdir"); let dfd = open("/tmp/ptdir", O_RDONLY | O_DIRECTORY); c.check(dfd >= 0, "open a directory with O_DIRECTORY"); let fd = openat(dfd, "inner.txt", O_WRONLY | O_CREAT); c.check(fd >= 0 && write(fd, b"12345") == 5, "openat(dirfd, \"inner.txt\", O_CREAT) + write"); close(fd); c.check(stat("/tmp/ptdir/inner.txt", &mut st) == 0 && st.size == 5, "...it's in that directory"); c.check(openat(AT_FDCWD, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "openat(AT_FDCWD, absolute)"); c.check(openat(99, "inner.txt", O_RDONLY) == -EBADF, "openat with a bad dirfd: EBADF"); c.check(openat(99, "/tmp/ptdir/inner.txt", O_RDONLY) >= 0, "...unless the path is absolute"); c.check(syscall5(SYS_MKDIRAT, dfd as u64, c"sub".as_ptr() as u64, 0o755, 0, 0) == 0, "mkdirat(dirfd, \"sub\")"); c.check(sys(SYS_FACCESSAT, dfd as u64, c"sub".as_ptr() as u64, 0) == 0, "faccessat(dirfd, \"sub\")"); c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, 0, 0, 0) == -EISDIR, "unlinkat on a directory without AT_REMOVEDIR: EISDIR"); c.check(syscall5(SYS_UNLINKAT, dfd as u64, c"sub".as_ptr() as u64, AT_REMOVEDIR, 0, 0) == 0, "unlinkat(AT_REMOVEDIR)"); let r = syscall5(SYS_RENAMEAT, dfd as u64, c"inner.txt".as_ptr() as u64, AT_FDCWD as u64, c"/tmp/renamed.txt".as_ptr() as u64, 0); c.check(r == 0 && access("/tmp/ptdir/inner.txt") == -ENOENT, "renameat(dirfd, \"inner.txt\", AT_FDCWD, \"/tmp/renamed.txt\")"); // fchdir: cd to the directory we have open. c.check(sys(SYS_FCHDIR, dfd as u64, 0, 0) == 0 && stat("../renamed.txt", &mut st) == 0 && st.size == 5, "fchdir(dirfd), then a relative path"); chdir("/"); close(dfd); // --- rename ----------------------------------------------------------------------- let fd = open("/tmp/pt-new", O_WRONLY | O_CREAT); write(fd, b"new contents"); close(fd); c.check(rename("/tmp/pt-new", "/tmp/renamed.txt") == 0, "rename a file over another"); c.check(stat("/tmp/renamed.txt", &mut st) == 0 && st.size == 12 && access("/tmp/pt-new") == -ENOENT, "...it replaced it"); c.check(rename("/tmp/renamed.txt", "/mnt/disk0/x.txt") == -EXDEV, "rename across file systems: EXDEV"); c.check(rename("/tmp/ptdir", "/tmp/ptdir2") == 0 && stat("/tmp/ptdir2", &mut st) == 0 && st.is_dir(), "rename an empty directory"); c.check(rename("/tmp/ptdir2", "/tmp/ptdir2/in") == -EINVAL, "rename a directory into itself: EINVAL"); c.check(rename("/tmp/nope", "/tmp/x") == -ENOENT, "rename a missing file: ENOENT"); unlink("/tmp/renamed.txt"); unlink("/tmp/pt-excl"); rmdir("/tmp/ptdir2"); // --- small things musl asks at startup ------------------------------------------ c.check(sys(SYS_GETUID, 0, 0, 0) == 0, "getuid: 0 (root)"); c.done() }