RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

foundry / import / kernel-user / pathtest.rs
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()
}