foundry
Foundry OS: the operating system built on the Rustbelt kernel
10276 bytes ยท raw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | //! 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::<Stat>() == 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()
}
|