From 3fa0b3de2ff7b42c4d6f2f5e14d7c1289364e011 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:31:00 +0200 Subject: [PATCH 01/12] build: the C sysroot names libm, libpthread, libdl and librt A POSIX link names them beside the C library, and LLVM's configure adds -lm to every probe it links on a UNIX system (llvm/cmake/config-ix.cmake). The C sysroot held libtoyos_c.a alone, so each such probe failed: configuring src/llvm-project/llvm (849da7d62) for x86_64-unknown-toyos against #637's C sysroot de0de8ee7862147a stopped at CheckAtomic.cmake:59, exit 1, with 18 "unable to find library -lm" in CMakeConfigureLog.yaml. Every function those libraries name is libtoyos_c.a's, so each is an ar archive of no members, as musl ships them. build_c lays them out; the sysroot recipe moves, since what a sysroot holds changed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- src/libc.rs | 32 +++++++++++++++++++++++++++++++ src/sysroot.rs | 3 ++- userland/libc/include/glob.h | 26 ------------------------- userland/libc/include/semaphore.h | 13 ------------- 4 files changed, 34 insertions(+), 40 deletions(-) delete mode 100644 userland/libc/include/glob.h delete mode 100644 userland/libc/include/semaphore.h diff --git a/src/libc.rs b/src/libc.rs index 758721fd2d6..6de060544a7 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -116,9 +116,41 @@ pub fn build_c(root: &Path, toolchain: &Path, target_dir: &Path, arch: Arch) { let archive = target_dir.join(format!("{target}/release/libtoyos_libc.a")); fs::copy(&archive, lib.join("libtoyos_c.a")) .unwrap_or_else(|e| panic!("copy {} into {}: {e}", archive.display(), lib.display())); + empty_libraries(&lib); crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.join("include")); } +/// The libraries a POSIX link names beside the C library, `-lm` first, whose +/// functions are all `libtoyos_c.a`'s. +const EMPTY_LIBRARIES: [&str; 4] = ["m", "pthread", "dl", "rt"]; + +/// Put each of [`EMPTY_LIBRARIES`] in `lib` as an archive of no members, as +/// musl ships them: a link that names one finds it and takes nothing from it. +fn empty_libraries(lib: &Path) { + for name in EMPTY_LIBRARIES { + let path = lib.join(format!("lib{name}.a")); + fs::write(&path, b"!\n").unwrap_or_else(|e| panic!("write {}: {e}", path.display())); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use toyos_tmpdir::TempDir; + + /// **`-lm` finds a library in the C sysroot**, and so do POSIX's other + /// three: each an `ar` archive of no members. + #[test] + fn the_c_sysroot_names_the_posix_libraries() { + let lib = TempDir::new("libc-empty"); + empty_libraries(&lib); + for name in ["m", "pthread", "dl", "rt"] { + let path = lib.join(format!("lib{name}.a")); + assert_eq!(fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())), b"!\n"); + } + } +} + /// Extract .o files from rlibs and merge them into a single GNU-format ar archive. fn merge_rlibs(rlib_paths: &[std::path::PathBuf]) -> Vec { let mut members: Vec<(String, Vec)> = Vec::new(); diff --git a/src/sysroot.rs b/src/sysroot.rs index 684ae2e9cbd..1b4cb352925 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -64,7 +64,8 @@ const SOURCES: &str = "SOURCES"; /// std build's recipe below. Moving it moves every key. const RECIPE: &str = "bootstrap stage-0 local rebuild, profile compiler, no LLVM, \ libtoyos_c merged, libraries from the stamp, linked by rust-lld, \ - a C sysroot of libc's staticlib and headers per target; 5"; + a C sysroot of libc's staticlib, the empty libraries beside it, and headers \ + per target; 6"; /// Every sysroot on this host. pub fn sysroots_dir(rust_dir: &Path) -> PathBuf { diff --git a/userland/libc/include/glob.h b/userland/libc/include/glob.h deleted file mode 100644 index d9cdf154de5..00000000000 --- a/userland/libc/include/glob.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef _GLOB_H -#define _GLOB_H - -#include - -typedef struct { - size_t gl_pathc; - char **gl_pathv; - size_t gl_offs; -} glob_t; - -#define GLOB_ERR 0x01 -#define GLOB_MARK 0x02 -#define GLOB_NOSORT 0x04 -#define GLOB_NOCHECK 0x08 -#define GLOB_APPEND 0x10 -#define GLOB_NOESCAPE 0x20 - -#define GLOB_NOSPACE 1 -#define GLOB_ABORTED 2 -#define GLOB_NOMATCH 3 - -int glob(const char *pattern, int flags, int (*errfunc)(const char *, int), glob_t *pglob); -void globfree(glob_t *pglob); - -#endif diff --git a/userland/libc/include/semaphore.h b/userland/libc/include/semaphore.h deleted file mode 100644 index 751e527271e..00000000000 --- a/userland/libc/include/semaphore.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef _SEMAPHORE_H -#define _SEMAPHORE_H - -typedef struct { - int value; -} sem_t; - -int sem_init(sem_t *sem, int pshared, unsigned int value); -int sem_wait(sem_t *sem); -int sem_post(sem_t *sem); -int sem_destroy(sem_t *sem); - -#endif From 1a687a041c24eb2d9549b67f405fd34ff2c4f7ac Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:31:20 +0200 Subject: [PATCH 02/12] libc: the headers are held to the definitions, and the POSIX surface LLVM compiles against LLVM's libraries stop, for a ToyOS host, on headers and names this library does not have, and on functions it declares and never defines. This gives them the surface, each name doing what POSIX says or refusing with errno, and a test that holds every header to what libc defines. The gate (toyos-libc-copies/src/prototypes.rs) is the exit of issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md: every prototype in include/ against the no_mangle function libc defines in the C library's configuration, by each argument's and the result's ABI class, width and signedness; every definition declared by a header, save the entry and unwinding names it lists with their reasons; no two declarations of one name apart. Macros that define exports are expanded where invoked. Its first run found, and this fixes: - __assert_fail declared with four parameters, defined with three. - creat, glob, globfree, mprotect, sem_*, wait declared and never defined: creat is defined; mprotect refuses; glob.h and semaphore.h go; wait leaves sys/wait.h until the process-lifecycle track's stage 3 defines it. - opendir, readdir, closedir, fsync, ftruncate, fchmod, pread and pwrite defined and never declared: dirent.h is new; pread and pwrite are deleted (below); the rest are declared. - creat declared taking an int, where POSIX and the definition take mode_t. close_socket stays undeclared, excused with its defect: close never calls it. New headers: dirent.h, pwd.h, sysexits.h, POSIX.1-2024's endian.h, sys/resource.h, sys/statvfs.h, sys/un.h and sys/utsname.h. No BSD name is carried to spare LLVM a platform arm: no machine/endian.h, no statvfs f_flags or MNT_LOCAL. Real, each with a host test (toyos-libc-copies) and a corpus case: - strnlen; strsignal, in glibc's words for every signal signal.h numbers. - modf and logb, bit for bit the host C library's. - readlink, with POSIX's EINVAL for a path that is no link, which the kernel answers NotFound; symlink, with POSIX's EEXIST, asked before the kernel, which displaces what the name held. - readdir reads the kernel's answer as SYS_READDIR writes it (kind, name, NUL, eight bytes of size). It read the answer as NUL-separated names, so every name after the first began with the last one's size. d_type is DT_DIR or DT_UNKNOWN, the listing telling no more; a name past d_name is EOVERFLOW rather than cut; opendir sets errno. - dladdr: the image holding an address from SYS_QUERY_MODULES, named by its path, and its exported symbol chosen as glibc's dl-addr.c chooses. - posix_madvise: advice changes no access's meaning, so taking none is POSIX's own reading. - aligned_alloc beside std, for the alignments std's malloc gives; C11's EINVAL for a larger one, which its free cannot release. - sysconf answers _SC_ARG_MAX and _SC_GETPW_R_SIZE_MAX with -1 and errno untouched, and EINVAL for a name it does not know. Refused with ENOSYS (refused.rs), each in its POSIX form: alarm, execv, execve, setsid, getsid, gethostname, uname, link, fchown, chmod, fchmod, statvfs, fstatvfs, getrlimit, setrlimit, getpwnam_r, getpwuid_r, msync, mprotect, realpath. chmod and fchmod answered 0 and set nothing. fcntl answered 0 to every command, so LLVM's tryLockFile believed it held a lock nobody held: F_GETLK, F_SETLK and F_SETLKW refuse with ENOSYS. pread and pwrite were seek, read or write, seek back, racing any thread sharing the descriptor, and no call reads at an offset; they are deleted, declared by no header, and LLVM's configure finds none, as before. mmap ignored the file and its offset and mapped zeros: a file mapping is refused ENODEV, which MemoryBuffer answers by reading the file; executable memory, which the kernel does not map, ENOTSUP; no bytes, or a fixed place off a page, EINVAL; a kernel refusal ENOMEM where it set nothing. Left to the process-lifecycle track's stage 3, which owns them: wait, wait4, sigemptyset, sigfillset, sigaddset, posix_spawn. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- tests/testcases/tinycc/206_libc_refusals.c | 109 +++ .../testcases/tinycc/206_libc_refusals.expect | 33 + tests/testcases/tinycc/207_libc_names.c | 147 ++++ toyos-libc-copies/src/dladdr_symbols.rs | 156 ++++ toyos-libc-copies/src/fparts_differential.rs | 72 ++ toyos-libc-copies/src/lib.rs | 34 +- toyos-libc-copies/src/listing_reader.rs | 56 ++ toyos-libc-copies/src/memory_refusals.rs | 43 + toyos-libc-copies/src/prototypes.rs | 779 ++++++++++++++++++ toyos-libc-copies/src/text_differential.rs | 82 ++ userland/libc/include/dirent.h | 37 + userland/libc/include/dlfcn.h | 8 + userland/libc/include/endian.h | 28 + userland/libc/include/errno.h | 3 + userland/libc/include/fcntl.h | 17 +- userland/libc/include/limits.h | 1 + userland/libc/include/math.h | 2 + userland/libc/include/pwd.h | 26 + userland/libc/include/signal.h | 5 + userland/libc/include/stdlib.h | 1 + userland/libc/include/string.h | 2 + userland/libc/include/sys/mman.h | 12 + userland/libc/include/sys/resource.h | 59 ++ userland/libc/include/sys/socket.h | 7 +- userland/libc/include/sys/stat.h | 1 + userland/libc/include/sys/statvfs.h | 35 + userland/libc/include/sys/un.h | 11 + userland/libc/include/sys/utsname.h | 22 + userland/libc/include/sys/wait.h | 1 - userland/libc/include/sysexits.h | 23 + userland/libc/include/unistd.h | 15 + userland/libc/src/elfsym.rs | 149 ++++ userland/libc/src/errno.rs | 7 + userland/libc/src/fparts.rs | 60 ++ userland/libc/src/lib.rs | 6 + userland/libc/src/link.rs | 94 ++- userland/libc/src/listing.rs | 27 + userland/libc/src/math.rs | 12 + userland/libc/src/memory.rs | 12 + userland/libc/src/memreq.rs | 51 ++ userland/libc/src/misc.rs | 10 +- userland/libc/src/posix_io.rs | 147 ++-- userland/libc/src/refused.rs | 148 ++++ userland/libc/src/stdio.rs | 2 +- userland/libc/src/string.rs | 11 + userland/libc/src/text.rs | 42 + 46 files changed, 2542 insertions(+), 63 deletions(-) create mode 100644 tests/testcases/tinycc/206_libc_refusals.c create mode 100644 tests/testcases/tinycc/206_libc_refusals.expect create mode 100644 tests/testcases/tinycc/207_libc_names.c create mode 100644 toyos-libc-copies/src/dladdr_symbols.rs create mode 100644 toyos-libc-copies/src/fparts_differential.rs create mode 100644 toyos-libc-copies/src/listing_reader.rs create mode 100644 toyos-libc-copies/src/memory_refusals.rs create mode 100644 toyos-libc-copies/src/prototypes.rs create mode 100644 toyos-libc-copies/src/text_differential.rs create mode 100644 userland/libc/include/dirent.h create mode 100644 userland/libc/include/endian.h create mode 100644 userland/libc/include/pwd.h create mode 100644 userland/libc/include/sys/resource.h create mode 100644 userland/libc/include/sys/statvfs.h create mode 100644 userland/libc/include/sys/un.h create mode 100644 userland/libc/include/sys/utsname.h create mode 100644 userland/libc/include/sysexits.h create mode 100644 userland/libc/src/elfsym.rs create mode 100644 userland/libc/src/fparts.rs create mode 100644 userland/libc/src/listing.rs create mode 100644 userland/libc/src/memreq.rs create mode 100644 userland/libc/src/refused.rs create mode 100644 userland/libc/src/text.rs diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c new file mode 100644 index 00000000000..5cb26101f39 --- /dev/null +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -0,0 +1,109 @@ +/* What libc refuses: each call answers failure in its own POSIX form, errno + says why, and nothing is done. ENOSYS where ToyOS lacks the function; mmap's + own errno where it refuses a mapping. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FILE_PATH "/tmp/206_libc_refusals" + +static const char *errno_name(int e) { + switch (e) { + case 0: return "no errno"; + case ENOSYS: return "ENOSYS"; + case EINVAL: return "EINVAL"; + case ENODEV: return "ENODEV"; + case ENOTSUP: return "ENOTSUP"; + default: return "another errno"; + } +} + +/* One refusal: what the call answered, and what errno says. */ +static void said(const char *call, long answer) { + printf("%s: %ld, %s\n", call, answer, errno_name(errno)); + errno = 0; +} + +int main(void) { + char *argv[] = { "shell", NULL }; + char *envp[] = { NULL }; + char buf[256]; + struct utsname uts; + struct statvfs vfs; + struct rlimit limit = { 0, 0 }; + struct passwd pw, *found = &pw; + struct flock lock = { F_WRLCK, SEEK_SET, 0, 0, 0 }; + + int fd = open(FILE_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0 || write(fd, "x", 1) != 1) { + printf("could not make " FILE_PATH "\n"); + return 1; + } + char *page = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (page == MAP_FAILED) { + printf("could not map a page\n"); + return 1; + } + errno = 0; + + said("alarm", alarm(1)); + said("execv", execv("/system/bin/shell", argv)); + said("execve", execve("/system/bin/shell", argv, envp)); + said("setsid", setsid()); + said("getsid", getsid(0)); + said("gethostname", gethostname(buf, sizeof buf)); + said("uname", uname(&uts)); + said("link", link(FILE_PATH, FILE_PATH ".link")); + said("fchown", fchown(fd, 0, 0)); + said("chmod", chmod(FILE_PATH, 0600)); + said("fchmod", fchmod(fd, 0600)); + said("statvfs", statvfs("/tmp", &vfs)); + said("fstatvfs", fstatvfs(fd, &vfs)); + said("getrlimit", getrlimit(RLIMIT_STACK, &limit)); + said("setrlimit", setrlimit(RLIMIT_CORE, &limit)); + said("msync", msync(page, 4096, MS_SYNC)); + said("mprotect", mprotect(page, 4096, PROT_READ)); + said("realpath", realpath("/tmp", NULL) == NULL ? -1 : 0); + said("fcntl F_SETLK", fcntl(fd, F_SETLK, &lock)); + said("fcntl F_SETLKW", fcntl(fd, F_SETLKW, &lock)); + said("fcntl F_GETLK", fcntl(fd, F_GETLK, &lock)); + + /* The _r lookups answer their error, and null for the entry. */ + int answer = getpwnam_r("root", &pw, buf, sizeof buf, &found); + printf("getpwnam_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); + found = &pw; + answer = getpwuid_r(0, &pw, buf, sizeof buf, &found); + printf("getpwuid_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); + + /* mmap: no file, no executable page, no empty mapping. */ + said("mmap a file", mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0) == MAP_FAILED ? -1 : 0); + said("mmap a file shared", mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 4096) == MAP_FAILED ? -1 : 0); + said("mmap executable", mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); + said("mmap nothing", mmap(NULL, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); + + /* sysconf: -1 with errno untouched for a limit on what ToyOS lacks, and + EINVAL for a name it does not know. */ + said("sysconf _SC_ARG_MAX", sysconf(_SC_ARG_MAX)); + said("sysconf _SC_GETPW_R_SIZE_MAX", sysconf(_SC_GETPW_R_SIZE_MAX)); + said("sysconf 12345", sysconf(12345)); + + /* posix_madvise takes every advice and answers an error number. */ + printf("posix_madvise WILLNEED: %d\n", posix_madvise(page, 4096, POSIX_MADV_WILLNEED)); + printf("posix_madvise 99: %s\n", errno_name(posix_madvise(page, 4096, 99))); + + /* The file is as it was: its one byte, and no second name. */ + struct stat st; + printf("file: %ld bytes; second name: %s\n", fstat(fd, &st) == 0 ? (long)st.st_size : -1L, + access(FILE_PATH ".link", F_OK) == 0 ? "made" : "none"); + close(fd); + unlink(FILE_PATH); + return 0; +} diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect new file mode 100644 index 00000000000..3ad23e2d17d --- /dev/null +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -0,0 +1,33 @@ +alarm: 0, ENOSYS +execv: -1, ENOSYS +execve: -1, ENOSYS +setsid: -1, ENOSYS +getsid: -1, ENOSYS +gethostname: -1, ENOSYS +uname: -1, ENOSYS +link: -1, ENOSYS +fchown: -1, ENOSYS +chmod: -1, ENOSYS +fchmod: -1, ENOSYS +statvfs: -1, ENOSYS +fstatvfs: -1, ENOSYS +getrlimit: -1, ENOSYS +setrlimit: -1, ENOSYS +msync: -1, ENOSYS +mprotect: -1, ENOSYS +realpath: -1, ENOSYS +fcntl F_SETLK: -1, ENOSYS +fcntl F_SETLKW: -1, ENOSYS +fcntl F_GETLK: -1, ENOSYS +getpwnam_r: ENOSYS, entry null +getpwuid_r: ENOSYS, entry null +mmap a file: -1, ENODEV +mmap a file shared: -1, ENODEV +mmap executable: -1, ENOTSUP +mmap nothing: -1, EINVAL +sysconf _SC_ARG_MAX: -1, no errno +sysconf _SC_GETPW_R_SIZE_MAX: -1, no errno +sysconf 12345: -1, EINVAL +posix_madvise WILLNEED: 0 +posix_madvise 99: EINVAL +file: 1 bytes; second name: none diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c new file mode 100644 index 00000000000..d790fb1d66c --- /dev/null +++ b/tests/testcases/tinycc/207_libc_names.c @@ -0,0 +1,147 @@ +/* What libc does for the names LLVM builds against: creat, symlink and + readlink as POSIX has them; a directory listed with the name and kind of + each entry; dladdr naming the image and the exported symbol an address lies + in; and strnlen, strsignal, modf, logb and the conversions. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DIR_PATH "/tmp/207_libc_names" +#define TARGET DIR_PATH "/target" +#define LINK DIR_PATH "/link" +/* The image's test library, which exports tls_get_label. */ +#define LIB "/system/lib/libtls_lib.so" + +int main(void); + +static const char *errno_name(int e) { + switch (e) { + case EEXIST: return "EEXIST"; + case EINVAL: return "EINVAL"; + case ENOENT: return "ENOENT"; + default: return "another errno"; + } +} + +static int by_name(const void *a, const void *b) { + return strcmp(*(char *const *)a, *(char *const *)b); +} + +static void links(void) { + char buf[64]; + int fd = creat(TARGET, 0644); + if (fd < 0 || write(fd, "hello", 5) != 5 || close(fd) != 0) { + printf("creat %s failed\n", TARGET); + return; + } + printf("symlink: %d\n", symlink(TARGET, LINK)); + printf("symlink over the link: %d, %s\n", symlink(TARGET, LINK), errno_name(errno)); + printf("symlink over a file: %d, %s\n", symlink(LINK, TARGET), errno_name(errno)); + + ssize_t n = readlink(LINK, buf, sizeof buf); + printf("readlink: %zd, \"%.*s\"\n", n, n > 0 ? (int)n : 0, buf); + memset(buf, '#', sizeof buf); + n = readlink(LINK, buf, 4); + printf("readlink into 4: %zd, \"%.5s\"\n", n, buf); + printf("readlink of a file: %zd, %s\n", readlink(TARGET, buf, sizeof buf), errno_name(errno)); + printf("readlink of nothing: %zd, %s\n", readlink(DIR_PATH "/none", buf, sizeof buf), errno_name(errno)); + printf("readlink into 0: %zd, %s\n", readlink(LINK, buf, 0), errno_name(errno)); + + fd = open(LINK, O_RDONLY); + n = fd < 0 ? -1 : read(fd, buf, sizeof buf); + printf("read through the link: \"%.*s\"\n", n > 0 ? (int)n : 0, buf); + if (fd >= 0) + close(fd); +} + +static void listing(void) { + if (mkdir(DIR_PATH "/sub", 0755) != 0 || close(creat(DIR_PATH "/sub/inner", 0644)) != 0) { + printf("could not make %s/sub\n", DIR_PATH); + return; + } + DIR *dir = opendir(DIR_PATH); + if (!dir) { + printf("opendir %s: %s\n", DIR_PATH, errno_name(errno)); + return; + } + char *names[16]; + int count = 0; + struct dirent *entry; + while (count < 16 && (entry = readdir(dir)) != NULL) { + char line[300]; + snprintf(line, sizeof line, "%s %s", entry->d_name, + entry->d_type == DT_DIR ? "DT_DIR" : entry->d_type == DT_UNKNOWN ? "DT_UNKNOWN" : "another type"); + names[count++] = strdup(line); + } + closedir(dir); + qsort(names, count, sizeof names[0], by_name); + for (int i = 0; i < count; i++) + printf("entry: %s\n", names[i]); + errno = 0; + printf("opendir of nothing: %s, %s\n", opendir(DIR_PATH "/none") ? "a stream" : "null", errno_name(errno)); +} + +static int first_image(struct dl_phdr_info *info, size_t size, void *data) { + (void)size; + uintptr_t low = UINTPTR_MAX; + for (int i = 0; i < info->dlpi_phnum; i++) + if (info->dlpi_phdr[i].p_type == PT_LOAD && info->dlpi_phdr[i].p_vaddr < low) + low = info->dlpi_phdr[i].p_vaddr; + *(uintptr_t *)data = info->dlpi_addr + (low & ~(uintptr_t)0xfff); + return 1; +} + +static void dl(void) { + Dl_info info; + uintptr_t start = 0; + dl_iterate_phdr(first_image, &start); + int found = dladdr((const void *)main, &info); + printf("dladdr(main): %d; names this program: %s; starts where dl_iterate_phdr says: %s; symbol: %s\n", + found, found && strstr(info.dli_fname, "test_c_207_libc_names") ? "yes" : "no", + found && (uintptr_t)info.dli_fbase == start ? "yes" : "no", + found && info.dli_sname ? info.dli_sname : "none"); + + int local; + printf("dladdr(a stack address): %d\n", dladdr(&local, &info)); + + void *lib = dlopen(LIB, RTLD_NOW); + void *code = lib ? dlsym(lib, "tls_get_label") : NULL; + if (!code) { + printf("could not load %s\n", LIB); + return; + } + found = dladdr((const char *)code + 1, &info); + printf("dladdr(tls_get_label + 1): %d, %s, %s at the symbol's address: %s\n", found, + found ? info.dli_fname : "-", found && info.dli_sname ? info.dli_sname : "none", + found && info.dli_saddr == code ? "yes" : "no"); +} + +int main(void) { + if (mkdir(DIR_PATH, 0755) != 0) { + printf("could not make %s\n", DIR_PATH); + return 1; + } + links(); + listing(); + dl(); + + printf("strnlen: %zu %zu %zu\n", strnlen("hello", 3), strnlen("hi", 10), strnlen("", 4)); + printf("strsignal: %s; %s\n", strsignal(SIGSEGV), strsignal(99)); + double whole; + double part = modf(-3.25, &whole); + printf("modf(-3.25): %g and %g\n", whole, part); + printf("logb: %g %g %g\n", logb(0.1), logb(1024.0), logb(0.0)); + printf("endian: %s %#x %#x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", + (unsigned)htobe32(0x01020304), (unsigned)le16toh(0x0102)); + return 0; +} diff --git a/toyos-libc-copies/src/dladdr_symbols.rs b/toyos-libc-copies/src/dladdr_symbols.rs new file mode 100644 index 00000000000..56314faf8d2 --- /dev/null +++ b/toyos-libc-copies/src/dladdr_symbols.rs @@ -0,0 +1,156 @@ +//! `dladdr`'s search on an image laid out here, with each way a table says how +//! many symbols it has: `.gnu.hash`, `DT_HASH`, and neither. The answers are +//! glibc's `dladdr` rule (`elf/dl-addr.c`): an exported symbol, no TLS, +//! defined in a section, holding the address in `[value, value + size)` or at +//! `value` when its size is 0, the highest value of several. + +use crate::elfsym::Image; + +const SIZE: usize = 0x2000; +const PHDRS: usize = 0x40; +const DYNAMIC: usize = 0x100; +const SYMTAB: usize = 0x200; +const STRTAB: usize = 0x400; +const HASH: usize = 0x500; + +const GLOBAL: u8 = 1 << 4; +const WEAK: u8 = 2 << 4; +const FUNC: u8 = 2; +const OBJECT: u8 = 1; +const TLS: u8 = 6; +const HIDDEN: u8 = 2; +const ABS: u16 = 0xfff1; + +/// Name, `st_info`, `st_other`, `st_shndx`, value, size; index 0 is the null +/// symbol, and 1 the one local, which a `.gnu.hash` leaves unhashed. +const SYMBOLS: &[(&str, u8, u8, u16, u64, u64)] = &[ + ("", 0, 0, 0, 0, 0), + ("local", FUNC, 0, 1, 0x1000, 0x800), + ("func", GLOBAL | FUNC, 0, 1, 0x1100, 0x100), + ("big", GLOBAL | FUNC, 0, 1, 0x1000, 0x300), + ("weak", WEAK | FUNC, 0, 1, 0x1200, 0x80), + ("mark", GLOBAL, 0, 1, 0x1280, 0), + ("hidden", GLOBAL | FUNC, HIDDEN, 1, 0x1300, 0x40), + ("tls", GLOBAL | TLS, 0, 1, 0x1340, 0x40), + ("undef", GLOBAL | FUNC, 0, 0, 0, 0), + ("abs", GLOBAL, 0, ABS, 0x1400, 0x40), + ("last", GLOBAL | OBJECT, 0, 1, 0x1500, 0x10), +]; + +#[derive(Clone, Copy)] +enum Count { + GnuHash, + SysvHash, + Gap, +} + +/// An image at the start of an 8-aligned buffer, every table addressed from +/// it, `.dynamic` naming the symbols as `count` says. +fn image(count: Count) -> Vec { + let mut bytes = vec![0u8; SIZE]; + let mut put = |at: usize, value: &[u8]| bytes[at..at + value.len()].copy_from_slice(value); + // PT_LOAD over the whole image, then PT_DYNAMIC. + put(PHDRS, &1u32.to_le_bytes()); + put(PHDRS + 32, &(SIZE as u64).to_le_bytes()); + put(PHDRS + 40, &(SIZE as u64).to_le_bytes()); + put(PHDRS + 56, &2u32.to_le_bytes()); + put(PHDRS + 56 + 16, &(DYNAMIC as u64).to_le_bytes()); + let mut names = vec![0u8]; + for (i, (name, info, other, shndx, value, size)) in SYMBOLS.iter().enumerate() { + let at = SYMTAB + i * 24; + let offset = if name.is_empty() { 0 } else { names.len() as u32 }; + if !name.is_empty() { + names.extend_from_slice(name.as_bytes()); + names.push(0); + } + put(at, &offset.to_le_bytes()); + put(at + 4, &[*info, *other]); + put(at + 6, &shndx.to_le_bytes()); + put(at + 8, &value.to_le_bytes()); + put(at + 16, &size.to_le_bytes()); + } + put(STRTAB, &names); + let symbols = SYMBOLS.len() as u32; + let mut tags = vec![(6u64, SYMTAB as u64), (5, STRTAB as u64), (10, names.len() as u64)]; + match count { + Count::GnuHash => { + // One bucket, one bloom word, every symbol from 2 hashed in one + // chain whose last value has its low bit set. + let (nbuckets, symoffset, bloom_words) = (1u32, 2u32, 1u32); + put(HASH, &nbuckets.to_le_bytes()); + put(HASH + 4, &symoffset.to_le_bytes()); + put(HASH + 8, &bloom_words.to_le_bytes()); + put(HASH + 12, &6u32.to_le_bytes()); + put(HASH + 16, &u64::MAX.to_le_bytes()); + put(HASH + 24, &symoffset.to_le_bytes()); + for i in symoffset..symbols { + let last = u32::from(i == symbols - 1); + put(HASH + 28 + (i - symoffset) as usize * 4, &((i << 1) | last).to_le_bytes()); + } + tags.push((0x6fff_fef5, HASH as u64)); + } + Count::SysvHash => { + put(HASH, &1u32.to_le_bytes()); + put(HASH + 4, &symbols.to_le_bytes()); + tags.push((4, HASH as u64)); + } + Count::Gap => {} + } + for (i, (tag, value)) in tags.into_iter().chain([(0, 0)]).enumerate() { + put(DYNAMIC + i * 16, &tag.to_le_bytes()); + put(DYNAMIC + i * 16 + 8, &value.to_le_bytes()); + } + bytes.chunks(8).map(|c| u64::from_le_bytes(c.try_into().unwrap())).collect() +} + +fn loaded(words: &[u64]) -> Image { + let base = words.as_ptr() as u64; + Image { base, phdr: (base as usize + PHDRS) as *const u8, phnum: 2 } +} + +/// The symbol `dladdr` names for `offset` into the image, by name, with the +/// offset of its value. +fn named(image: Image, offset: u64) -> Option<(String, u64)> { + // SAFETY: the image is the buffer `image` laid out, whole. + let (name, at) = unsafe { image.symbol(image.base + offset) }?; + // SAFETY: a name in the image's string table, NUL-terminated there. + let name = unsafe { std::ffi::CStr::from_ptr(name.cast()) }; + Some((name.to_str().unwrap().to_string(), at - image.base)) +} + +#[test] +fn every_count_finds_glibcs_symbol() { + for count in [Count::GnuHash, Count::SysvHash, Count::Gap] { + let words = image(count); + let image = loaded(&words); + for (offset, want) in [ + (0x1000, Some(("big", 0x1000))), + (0x1150, Some(("func", 0x1100))), + (0x1210, Some(("weak", 0x1200))), + (0x1280, Some(("mark", 0x1280))), + (0x1281, Some(("big", 0x1000))), + (0x1310, None), + (0x1350, None), + (0x1410, None), + (0x1505, Some(("last", 0x1500))), + (0x1510, None), + (0x0800, None), + ] { + let want = want.map(|(n, v)| (n.to_string(), v)); + assert_eq!(named(image, offset), want, "offset {offset:#x}"); + } + } +} + +#[test] +fn an_image_holds_its_load_segment_and_starts_at_its_page() { + let words = image(Count::GnuHash); + let image = loaded(&words); + let base = image.base; + // SAFETY: the image's program headers, in the buffer. + unsafe { + assert!(image.contains(base) && image.contains(base + SIZE as u64 - 1)); + assert!(!image.contains(base + SIZE as u64) && !image.contains(base - 1)); + assert_eq!(image.start(), base); + } +} diff --git a/toyos-libc-copies/src/fparts_differential.rs b/toyos-libc-copies/src/fparts_differential.rs new file mode 100644 index 00000000000..992318b9984 --- /dev/null +++ b/toyos-libc-copies/src/fparts_differential.rs @@ -0,0 +1,72 @@ +//! `modf` and `logb` against the host C library's, bit for bit: every special +//! value, the edges of the subnormals and of the integers a double holds, and +//! a spread of bit patterns across every exponent. + +use crate::fparts; + +extern "C" { + #[link_name = "modf"] + fn host_modf(x: f64, int: *mut f64) -> f64; + #[link_name = "logb"] + fn host_logb(x: f64) -> f64; +} + +fn same(a: f64, b: f64) -> bool { + a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()) +} + +fn inputs() -> Vec { + let mut xs = vec![ + 0.0, + 1.0, + 0.5, + 1.5, + 2.5, + -2.5, + 123.456, + 1e300, + f64::MAX, + f64::MIN_POSITIVE, + f64::MIN_POSITIVE / 2.0, + f64::from_bits(1), + f64::from_bits(0x000f_ffff_ffff_ffff), + 4_503_599_627_370_495.5, + 4_503_599_627_370_496.0, + 9_007_199_254_740_993.0, + f64::INFINITY, + f64::NAN, + ]; + // Every exponent, three mantissas each. + let mut seed = 0x9e37_79b9_7f4a_7c15u64; + for exponent in 0..=0x7ffu64 { + for _ in 0..3 { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + xs.push(f64::from_bits((exponent << 52) | (seed & 0x000f_ffff_ffff_ffff))); + } + } + let negated: Vec = xs.iter().map(|x| -x).collect(); + xs.extend(negated); + xs +} + +#[test] +fn modf_is_the_host_libraries() { + for x in inputs() { + let (int, fraction) = fparts::modf(x); + let mut host_int = 0.0; + // SAFETY: `host_int` is a writable double. + let host_fraction = unsafe { host_modf(x, &mut host_int) }; + assert!(same(int, host_int) && same(fraction, host_fraction), "modf({x:e} = {:#x}): ({int:e}, {fraction:e}), the host ({host_int:e}, {host_fraction:e})", x.to_bits()); + } +} + +#[test] +fn logb_is_the_host_libraries() { + for x in inputs() { + // SAFETY: a pure function of its argument. + let host = unsafe { host_logb(x) }; + assert!(same(fparts::logb(x), host), "logb({x:e} = {:#x}): {}, the host {host}", x.to_bits(), fparts::logb(x)); + } +} diff --git a/toyos-libc-copies/src/lib.rs b/toyos-libc-copies/src/lib.rs index 371c8d880e3..987d54d5328 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -2,11 +2,43 @@ //! it has, over every length to 300 and every source and destination offset to //! 20, against `copy_within` and `fill`, with the two buffers overlapping both //! ways; and its square roots against `f64::sqrt` and `f32::sqrt`. Each host -//! architecture checks its own module. +//! architecture checks its own module. libc's modules that read and set nothing +//! but what they are handed: `strnlen` and `modf` and `logb` against the host C +//! library's, `strsignal`'s texts, the readdir answer's reader, `dladdr`'s +//! symbol search on an image laid out here, and what the memory calls refuse. +//! And libc's headers against its definitions (`prototypes`). #[cfg(test)] #[path = "../../userland/libc/src/arch/mod.rs"] mod arch; +#[cfg(test)] +#[path = "../../userland/libc/src/elfsym.rs"] +mod elfsym; +#[cfg(test)] +#[path = "../../userland/libc/src/fparts.rs"] +mod fparts; +#[cfg(test)] +#[path = "../../userland/libc/src/listing.rs"] +mod listing; +#[cfg(test)] +#[path = "../../userland/libc/src/memreq.rs"] +mod memreq; +#[cfg(test)] +#[path = "../../userland/libc/src/text.rs"] +mod text; + +#[cfg(test)] +mod dladdr_symbols; +#[cfg(test)] +mod fparts_differential; +#[cfg(test)] +mod listing_reader; +#[cfg(test)] +mod memory_refusals; +#[cfg(test)] +mod prototypes; +#[cfg(test)] +mod text_differential; #[cfg(test)] mod tests { diff --git a/toyos-libc-copies/src/listing_reader.rs b/toyos-libc-copies/src/listing_reader.rs new file mode 100644 index 00000000000..92121f7423c --- /dev/null +++ b/toyos-libc-copies/src/listing_reader.rs @@ -0,0 +1,56 @@ +//! The readdir answer's reader against answers encoded as the kernel's +//! `sys_readdir` encodes them (`kernel/src/syscall/fs.rs`): a kind byte, the +//! name, a NUL and eight bytes of size, for each entry. + +use crate::listing; + +/// An answer for `entries`, each a name, whether it is a directory, and a size. +fn answer(entries: &[(&str, bool, u64)]) -> Vec { + let mut out = Vec::new(); + for (name, is_dir, size) in entries { + out.push(if *is_dir { 2 } else { 1 }); + out.extend_from_slice(name.as_bytes()); + out.push(0); + out.extend_from_slice(&size.to_le_bytes()); + } + out +} + +fn read(bytes: &[u8]) -> Vec<(String, bool)> { + let mut pos = 0; + let mut out = Vec::new(); + while let Some(entry) = listing::next(bytes, &mut pos) { + out.push((String::from_utf8(entry.name.to_vec()).unwrap(), entry.is_dir)); + } + assert_eq!(pos, bytes.len(), "the reader stopped short of the answer's end"); + out +} + +#[test] +fn every_entry_is_read_back_whatever_its_size_holds() { + // Sizes whose bytes are NULs, kind bytes and letters: a reader that does + // not step over all eight takes them for names. + let entries = [ + ("a", false, 0), + ("dir", true, 0x0201_0000_0000_0000), + ("b.txt", false, 0x6162_6300_0102_0304), + ("", false, u64::MAX), + ("last", true, 17), + ]; + let got = read(&answer(&entries)); + let want: Vec<(String, bool)> = entries.iter().map(|(n, d, _)| ((*n).to_string(), *d)).collect(); + assert_eq!(got, want); + assert!(read(&[]).is_empty()); +} + +#[test] +#[should_panic(expected = "of kind 3")] +fn an_unknown_kind_is_a_broken_kernel() { + read(&[3, b'x', 0, 0, 0, 0, 0, 0, 0, 0, 0]); +} + +#[test] +#[should_panic(expected = "no size")] +fn a_short_entry_is_a_broken_kernel() { + read(&[1, b'x', 0, 0, 0]); +} diff --git a/toyos-libc-copies/src/memory_refusals.rs b/toyos-libc-copies/src/memory_refusals.rs new file mode 100644 index 00000000000..396457417ee --- /dev/null +++ b/toyos-libc-copies/src/memory_refusals.rs @@ -0,0 +1,43 @@ +//! What libc's memory calls refuse before the kernel or the allocator is +//! asked: `mmap` a file, executable memory, no bytes, or a fixed place off a +//! page; `posix_madvise` anything but its five; `aligned_alloc` beside std any +//! alignment std's `malloc` does not give. + +use crate::memreq::{self, MapRefusal, MAP_ANONYMOUS, MAP_FIXED, PROT_EXEC, PROT_READ, PROT_WRITE}; + +const MAP_SHARED: i32 = 0x01; +const MAP_PRIVATE: i32 = 0x02; + +#[test] +fn mmap_refuses_what_the_kernel_cannot_map_and_nothing_else() { + let rw = PROT_READ | PROT_WRITE; + let anonymous = MAP_PRIVATE | MAP_ANONYMOUS; + for (addr, len, prot, flags, want) in [ + (0, 4096, rw, anonymous, None), + (0, 1, PROT_READ, MAP_SHARED | MAP_ANONYMOUS, None), + (0, 4096, 0, anonymous, None), + (0x4000_0000, 4096, rw, anonymous | MAP_FIXED, None), + // Off a page, which only a fixed place has to be on. + (0x4000_0001, 4096, rw, anonymous, None), + (0x4000_0001, 4096, rw, anonymous | MAP_FIXED, Some(MapRefusal::Invalid)), + (0, 0, rw, anonymous, Some(MapRefusal::Invalid)), + (0, 4096, PROT_READ, MAP_PRIVATE, Some(MapRefusal::File)), + (0, 4096, rw, MAP_SHARED, Some(MapRefusal::File)), + (0, 4096, PROT_READ | PROT_EXEC, anonymous, Some(MapRefusal::Exec)), + ] { + assert_eq!(memreq::mmap_refusal(addr, len, prot, flags), want, "mmap({addr:#x}, {len}, {prot:#x}, {flags:#x})"); + } +} + +#[test] +fn posix_madvise_takes_its_five_and_only_them() { + let taken: Vec = (-2..8).filter(|&a| memreq::is_advice(a)).collect(); + assert_eq!(taken, [0, 1, 2, 3, 4]); +} + +#[test] +fn aligned_alloc_beside_std_gives_what_malloc_aligns_to() { + let given: Vec = (0..=64).filter(|&a| memreq::std_aligns(a)).collect(); + assert_eq!(given, [1, 2, 4, 8, 16]); + assert!(!memreq::std_aligns(1 << 12)); +} diff --git a/toyos-libc-copies/src/prototypes.rs b/toyos-libc-copies/src/prototypes.rs new file mode 100644 index 00000000000..5864fb8ecbd --- /dev/null +++ b/toyos-libc-copies/src/prototypes.rs @@ -0,0 +1,779 @@ +//! libc's C headers held to its Rust definitions: every function a header +//! declares is one libc defines, with the definition's signature; every +//! function libc defines is declared by a header, save [`UNDECLARED`]; and no +//! two declarations of one name disagree. What is compared is what a call +//! passes, as the two C ABIs ToyOS has decide it: each argument's and the +//! result's class, and an integer's width and signedness. +//! +//! Both sides are read as text. A header is C with its preprocessor lines +//! dropped, `__cplusplus`'s arms with them; a definition is an item marked +//! `no_mangle`. A declaration or a definition this reader cannot take apart is +//! a red naming it, never a skip. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +/// What libc defines and no header declares, each with why. +const UNDECLARED: &[(&str, &str)] = &[ + ("_start", "the entry the loader starts a C program at"), + ("rust_eh_personality", "the personality `core`'s unwind tables name"), + ("_Unwind_Resume", "what the precompiled `alloc`'s landing pads name"), + ("close_socket", "`close`'s arm for a socket, which `close` never calls: issues/build/libc-close-of-a-socket-ends-the-process.md"), +]; + +fn libc() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland/libc") +} + +/// How a value crosses a call. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Class { + Void, + /// Bytes, and signedness where the type fixes it: `char` and `wchar_t` + /// differ between the two architectures. + Int(u8, Option), + F32, + F64, + /// x87's 80 bits on x86-64, binary128 on AArch64: no Rust type is either, + /// so only an assembly definition, whose signature is unchecked, has one. + LongDouble, + Ptr, + VaList, + /// A struct or union by value. + Aggregate, +} + +impl Class { + fn agrees(self, other: Class) -> bool { + match (self, other) { + (Class::Int(a, sa), Class::Int(b, sb)) => a == b && (sa.is_none() || sb.is_none() || sa == sb), + (a, b) => a == b, + } + } +} + +#[derive(Clone, Debug)] +struct Signature { + ret: Class, + params: Vec, + variadic: bool, +} + +impl Signature { + /// Whether a call written against `self` reaches `def` intact. A C `...` + /// is met by a Rust `...`, or by trailing integers and pointers read from + /// the registers the variadic arguments arrive in. + fn reaches(&self, def: &Signature) -> bool { + if !self.ret.agrees(def.ret) || def.params.len() < self.params.len() { + return false; + } + let fixed = self.params.iter().zip(&def.params).all(|(c, r)| c.agrees(*r)); + let rest = &def.params[self.params.len()..]; + let tail = if self.variadic { + def.variadic || rest.iter().all(|c| matches!(c, Class::Int(..) | Class::Ptr)) + } else { + rest.is_empty() && !def.variadic + }; + fixed && tail + } + + fn agrees(&self, other: &Signature) -> bool { + self.ret.agrees(other.ret) + && self.variadic == other.variadic + && self.params.len() == other.params.len() + && self.params.iter().zip(&other.params).all(|(a, b)| a.agrees(*b)) + } +} + +// ---- the headers ---- + +/// `text` without comments, `\` continuations or preprocessor lines, and +/// without the arms `__cplusplus` selects; every other conditional's arms are +/// both kept. +fn preprocess(text: &str) -> String { + let mut plain = String::new(); + let mut rest = text; + while let Some(at) = rest.find("/*").into_iter().chain(rest.find("//")).min() { + plain.push_str(&rest[..at]); + rest = if rest[at..].starts_with("/*") { + &rest[at + rest[at..].find("*/").expect("an unterminated comment") + 2..] + } else { + &rest[at + rest[at..].find('\n').unwrap_or(rest.len() - at)..] + }; + } + plain.push_str(rest); + let joined = plain.replace("\\\n", " "); + // Per open conditional: whether it is `__cplusplus`'s, and whether its + // current arm is kept. + let mut frames: Vec<(bool, bool)> = Vec::new(); + let mut out = String::new(); + for line in joined.lines() { + let directive = line.trim_start(); + if let Some(d) = directive.strip_prefix('#') { + let words: Vec<&str> = d.split_whitespace().collect(); + match words.first().copied() { + Some("if" | "ifdef" | "ifndef") => { + let cplusplus = words.get(1) == Some(&"__cplusplus"); + frames.push((cplusplus, !(cplusplus && words[0] == "ifdef"))); + } + Some("else") => { + let top = frames.last_mut().expect("#else outside a conditional"); + if top.0 { + top.1 = !top.1; + } + } + Some("elif") => {} + Some("endif") => { + frames.pop().expect("#endif outside a conditional"); + } + _ => {} + } + continue; + } + if frames.iter().all(|f| f.1) { + out.push_str(line); + out.push('\n'); + } + } + out +} + +fn tokens(text: &str) -> Vec { + let mut out = Vec::new(); + let chars: Vec = text.chars().collect(); + let mut i = 0; + while i < chars.len() { + let c = chars[i]; + if c.is_whitespace() { + i += 1; + } else if c.is_alphanumeric() || c == '_' { + let start = i; + while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') { + i += 1; + } + out.push(chars[start..i].iter().collect()); + } else if c == '"' { + let start = i; + i += 1; + while chars[i] != '"' { + i += 1; + } + i += 1; + out.push(chars[start..i].iter().collect()); + } else if chars[i..].starts_with(&['.', '.', '.']) { + out.push("...".to_string()); + i += 3; + } else { + out.push(c.to_string()); + i += 1; + } + } + out +} + +/// `tokens` with every `__attribute__((...))` and `_Noreturn` taken out. +fn without_attributes(tokens: &[String]) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < tokens.len() { + if tokens[i] == "__attribute__" { + let mut depth = 0; + i += 1; + loop { + match tokens[i].as_str() { + "(" => depth += 1, + ")" => depth -= 1, + _ => {} + } + i += 1; + if depth == 0 { + break; + } + } + } else if tokens[i] == "_Noreturn" { + i += 1; + } else { + out.push(tokens[i].clone()); + i += 1; + } + } + out +} + +/// Top-level declarations: the tokens before each `;` outside every bracket, +/// a struct's `{...}` body kept whole, each marked whether it is an inline +/// function's definition, which ends at its body. +fn declarations(tokens: &[String]) -> Vec<(Vec, bool)> { + let mut out = Vec::new(); + let mut current = Vec::new(); + let mut depth = 0i32; + for t in tokens { + match t.as_str() { + "{" | "(" | "[" => depth += 1, + "}" | ")" | "]" => depth -= 1, + _ => {} + } + current.push(t.clone()); + if depth == 0 && t == ";" { + current.pop(); + out.push((std::mem::take(&mut current), false)); + } else if depth == 0 && t == "}" { + let open = current.iter().position(|x| x == "{").expect("a `}` with no `{`"); + if open > 0 && current[open - 1] == ")" { + out.push((std::mem::take(&mut current), true)); + } + } + } + assert!(current.is_empty(), "a header ends inside a declaration: {current:?}"); + out +} + +const QUALIFIERS: &[&str] = &["const", "volatile", "restrict", "__restrict", "__restrict__", "register", "extern", "static", "inline", "__inline", "__inline__"]; + +/// The class of the C type `words` spell, qualifiers and pointer and array +/// declarators already gone, against `typedefs`. +fn c_class(words: &[&str], typedefs: &BTreeMap) -> Option { + if words.is_empty() { + return None; + } + if let [kind, _] = words { + match *kind { + "struct" | "union" => return Some(Class::Aggregate), + "enum" => return Some(Class::Int(4, None)), + _ => {} + } + } + if let [one] = words { + if let Some(c) = typedefs.get(*one) { + return Some(*c); + } + } + let unsigned = words.contains(&"unsigned"); + let signed = words.contains(&"signed"); + let base: Vec<&str> = words.iter().copied().filter(|w| *w != "unsigned" && *w != "signed").collect(); + let sign = |fixed: bool| Some(!fixed); + Some(match base.as_slice() { + ["void"] => Class::Void, + ["char"] if unsigned => Class::Int(1, Some(false)), + ["char"] if signed => Class::Int(1, Some(true)), + ["char"] => Class::Int(1, None), + ["_Bool"] | ["bool"] => Class::Int(1, Some(false)), + ["short"] | ["short", "int"] => Class::Int(2, sign(unsigned)), + [] | ["int"] => Class::Int(4, sign(unsigned)), + ["long"] | ["long", "int"] | ["long", "long"] | ["long", "long", "int"] => Class::Int(8, sign(unsigned)), + ["float"] => Class::F32, + ["double"] => Class::F64, + ["long", "double"] => Class::LongDouble, + _ => return None, + }) +} + +/// The class of one parameter or result: its type, whether or not a name +/// follows it. +fn c_param(tokens: &[String], typedefs: &BTreeMap) -> Option { + if tokens.iter().any(|t| t == "*" || t == "[" || t == "(") { + return Some(Class::Ptr); + } + let words: Vec<&str> = tokens.iter().map(String::as_str).filter(|w| !QUALIFIERS.contains(w)).collect(); + c_class(&words, typedefs).or_else(|| c_class(&words[..words.len().checked_sub(1)?], typedefs)) +} + +/// The classes of the C builtins and the headers every header reaches for: +/// clang's own `stddef.h`, `stdint.h`'s widths and `stdarg.h`. +fn builtin_typedefs() -> BTreeMap { + let mut t = BTreeMap::new(); + for (name, class) in [ + ("size_t", Class::Int(8, Some(false))), + ("ssize_t", Class::Int(8, Some(true))), + ("ptrdiff_t", Class::Int(8, Some(true))), + ("intptr_t", Class::Int(8, Some(true))), + ("uintptr_t", Class::Int(8, Some(false))), + ("wchar_t", Class::Int(4, None)), + ("__WINT_TYPE__", Class::Int(4, None)), + ("va_list", Class::VaList), + ("__builtin_va_list", Class::VaList), + ] { + t.insert(name.to_string(), class); + } + // clang's own type macros, which `stdint.h` may spell its types with: on + // both targets a least or fast type is the exact one of its width. + for bits in [8u8, 16, 32, 64] { + for (signed, s, u) in [(true, "INT", "int"), (false, "UINT", "uint")] { + let class = Class::Int(bits / 8, Some(signed)); + t.insert(format!("{u}{bits}_t"), class); + for kind in ["", "_LEAST", "_FAST"] { + t.insert(format!("__{s}{kind}{bits}_TYPE__"), class); + } + } + } + for (name, signed) in [("__INTMAX_TYPE__", true), ("__UINTMAX_TYPE__", false), ("__INTPTR_TYPE__", true), ("__UINTPTR_TYPE__", false)] { + t.insert(name.to_string(), Class::Int(8, Some(signed))); + } + t +} + +#[derive(Debug)] +struct Declaration { + header: String, + signature: Signature, +} + +/// Every function prototype under `include`, by name, and the names the +/// headers define inline. +fn headers(include: &Path) -> (BTreeMap>, BTreeSet, Vec) { + let mut files = Vec::new(); + walk(include, "h", &mut files); + let mut typedefs = builtin_typedefs(); + let mut all = Vec::new(); + for file in &files { + let rel = file.strip_prefix(include).unwrap().display().to_string(); + let text = fs::read_to_string(file).unwrap_or_else(|e| panic!("{}: {e}", file.display())); + for (decl, inline) in declarations(&without_attributes(&tokens(&preprocess(&text)))) { + if decl.first().map(String::as_str) == Some("typedef") { + typedef(&decl[1..], &mut typedefs); + } else if !decl.is_empty() { + all.push((rel.clone(), decl, inline)); + } + } + } + let mut prototypes: BTreeMap> = BTreeMap::new(); + let mut inline_names = BTreeSet::new(); + let mut unread = Vec::new(); + for (header, decl, inline) in all { + // A function's name is the identifier before the first `(` outside a + // bracket; a declaration with none declares no function. + // A struct, union or enum body is no prototype. + if !inline && decl.iter().any(|t| t == "{") { + continue; + } + let Some(open) = decl.iter().position(|t| t == "(") else { continue }; + let name = &decl[open - 1]; + if name == "(" || !name.chars().all(|c| c.is_alphanumeric() || c == '_') { + // `(*name)`: a pointer variable, which is no function. + continue; + } + if inline { + inline_names.insert(name.clone()); + continue; + } + let close = matching(&decl, open); + let parsed = (|| { + if close != decl.len() - 1 { + return None; + } + let ret = c_param(&decl[..open - 1], &typedefs)?; + let mut params = Vec::new(); + let mut variadic = false; + for param in split_commas(&decl[open + 1..close]) { + match param.as_slice() { + [dots] if dots == "..." => variadic = true, + [void] if void == "void" => {} + [] => {} + p => params.push(c_param(p, &typedefs)?), + } + } + Some(Signature { ret, params, variadic }) + })(); + match parsed { + Some(signature) => prototypes.entry(name.clone()).or_default().push(Declaration { header, signature }), + None => unread.push(format!("{header}: `{}` is a declaration this reader cannot take apart", decl.join(" "))), + } + } + (prototypes, inline_names, unread) +} + +/// Record the typedef `decl` declares (after the keyword). +fn typedef(decl: &[String], typedefs: &mut BTreeMap) { + let words: Vec<&String> = decl.iter().collect(); + // `(*name)(...)`: a function pointer. + if let Some(star) = words.windows(2).position(|w| w[0] == "(" && w[1] == "*") { + typedefs.insert(words[star + 2].clone(), Class::Ptr); + return; + } + // `name[n]`: an array, which a parameter receives as a pointer. + if let Some(open) = words.iter().position(|w| *w == "[") { + typedefs.insert(words[open - 1].clone(), Class::Ptr); + return; + } + let name = words.last().expect("an empty typedef").to_string(); + let ty: Vec<&str> = decl[..decl.len() - 1].iter().map(String::as_str).filter(|w| !QUALIFIERS.contains(w)).collect(); + let class = match ty.first().copied() { + _ if ty.contains(&"*") => Class::Ptr, + Some("enum") => Class::Int(4, None), + Some("struct" | "union") => Class::Aggregate, + _ => c_class(&ty, typedefs).unwrap_or_else(|| panic!("typedef {name}: {ty:?} is no type this reader knows")), + }; + typedefs.insert(name, class); +} + +fn matching(tokens: &[String], open: usize) -> usize { + let mut depth = 0; + for (i, t) in tokens.iter().enumerate().skip(open) { + match t.as_str() { + "(" => depth += 1, + ")" => { + depth -= 1; + if depth == 0 { + return i; + } + } + _ => {} + } + } + panic!("an unclosed parenthesis in {tokens:?}") +} + +fn split_commas(tokens: &[String]) -> Vec> { + let mut out = vec![Vec::new()]; + let mut depth = 0; + for t in tokens { + match t.as_str() { + "(" | "[" | "<" => depth += 1, + ")" | "]" | ">" => depth -= 1, + "," if depth == 0 => { + out.push(Vec::new()); + continue; + } + _ => {} + } + out.last_mut().unwrap().push(t.clone()); + } + out +} + +fn walk(dir: &Path, ext: &str, out: &mut Vec) { + let mut entries: Vec = fs::read_dir(dir).unwrap_or_else(|e| panic!("{}: {e}", dir.display())).map(|e| e.unwrap().path()).collect(); + entries.sort(); + for path in entries { + if path.is_dir() { + walk(&path, ext, out); + } else if path.extension().is_some_and(|e| e == ext) { + out.push(path); + } + } +} + +// ---- the definitions ---- + +#[derive(Debug)] +struct Definition { + file: String, + /// `None` for an assembly body, whose Rust signature says nothing. + signature: Option, +} + +/// Every function libc defines under `no_mangle` in the C library's +/// configuration (`std-runtime` off), by name, and every `no_mangle` item this +/// reader cannot take apart. +fn definitions(src: &Path) -> (BTreeMap, Vec) { + let mut files = Vec::new(); + walk(src, "rs", &mut files); + let mut unread = Vec::new(); + let texts: Vec<(String, String)> = files + .iter() + .map(|f| { + let file = f.strip_prefix(src).unwrap().display().to_string(); + let text = expand_exporting_macros(&fs::read_to_string(f).unwrap()).unwrap_or_else(|why| { + unread.push(format!("{file}: {why}")); + String::new() + }); + (file, text) + }) + .collect(); + let aliases = rust_aliases(&texts); + let mut defs = BTreeMap::new(); + for (file, text) in &texts { + let lines: Vec<&str> = text.lines().collect(); + for (i, line) in lines.iter().enumerate() { + let t = line.trim(); + if t != "#[no_mangle]" && t != "#[unsafe(no_mangle)]" { + continue; + } + // The attributes around it, and the item after them. + let mut first = i; + while first > 0 && lines[first - 1].trim().starts_with("#[") { + first -= 1; + } + let mut item = i + 1; + while lines[item].trim().starts_with("#[") { + item += 1; + } + let attributes: Vec<&str> = lines[first..item].iter().map(|l| l.trim()).collect(); + if attributes.contains(&"#[cfg(feature = \"std-runtime\")]") { + continue; + } + let naked = attributes.contains(&"#[unsafe(naked)]"); + let mut head = String::new(); + for l in &lines[item..] { + head.push_str(l.trim()); + head.push(' '); + if l.contains('{') || l.trim_end().ends_with(';') { + break; + } + } + if !head.contains(" fn ") && !head.starts_with("fn ") { + // A `static` is data, which no prototype declares. + if !head.contains("static ") { + unread.push(format!("{file}:{}: `{}` is a no_mangle item this reader cannot take apart", item + 1, head.trim())); + } + continue; + } + match rust_signature(&head, &aliases) { + Some((name, signature)) => { + let signature = if naked { None } else { Some(signature) }; + defs.insert(name, Definition { file: file.clone(), signature }); + } + None => unread.push(format!("{file}:{}: `{}` is a definition this reader cannot take apart", item + 1, head.trim())), + } + } + } + (defs, unread) +} + +/// `text` with each `macro_rules!` whose body defines a `no_mangle` item +/// expanded at its invocations, and every macro's definition taken out. An +/// exporting macro is one rule, `($( pattern )*) => {$( body )*}`, whose +/// pattern is literals and `$name:fragment`s that each take one token. +fn expand_exporting_macros(text: &str) -> Result { + let mut text = text.to_string(); + while let Some(at) = text.find("macro_rules!") { + let open = at + text[at..].find('{').ok_or("a macro_rules! with no body")?; + let close = closing(&text, open)?; + let name = text[at + "macro_rules!".len()..open].trim().to_string(); + let block = text[open + 1..close].to_string(); + text.replace_range(at..=close, ""); + if !block.contains("no_mangle") { + continue; + } + let repeated = |inner: &str| inner.trim().strip_prefix("$(").and_then(|p| p.strip_suffix(")*")).map(str::to_string); + let matcher = block.find('(').ok_or_else(|| format!("macro {name} has no matcher"))?; + let matcher_end = closing(&block, matcher)?; + let pattern = repeated(&block[matcher + 1..matcher_end]).ok_or_else(|| format!("macro {name}'s matcher is no `$(...)*`"))?; + let body = matcher_end + block[matcher_end..].find('{').ok_or_else(|| format!("macro {name} has no transcriber"))?; + let body = repeated(&block[body + 1..closing(&block, body)?]).ok_or_else(|| format!("macro {name}'s transcriber is no `$(...)*`"))?; + let pattern = macro_tokens(&pattern); + let call = format!("{name}!"); + while let Some(c) = text.find(&call) { + let bracket = c + call.len() + text[c + call.len()..].find(['{', '(']).ok_or_else(|| format!("{call} with no arguments"))?; + let end = closing(&text, bracket)?; + let expansion = expand(&pattern, ¯o_tokens(&text[bracket + 1..end]), &body).map_err(|why| format!("{call}: {why}"))?; + let semicolon = text[end + 1..].trim_start().starts_with(';'); + let after = if semicolon { end + 1 + text[end + 1..].find(';').unwrap() + 1 } else { end + 1 }; + text.replace_range(c..after, &expansion); + } + } + Ok(text) +} + +/// The index of the bracket closing the one at `open`. +fn closing(text: &str, open: usize) -> Result { + let mut depth = 0; + for (i, c) in text.char_indices().skip_while(|(i, _)| *i < open) { + match c { + '{' | '(' | '[' => depth += 1, + '}' | ')' | ']' => { + depth -= 1; + if depth == 0 { + return Ok(i); + } + } + _ => {} + } + } + Err(format!("an unclosed bracket at byte {open}")) +} + +/// A macro's pattern or arguments as tokens: a path or `$name:fragment` is +/// one, and so is `=>`. +fn macro_tokens(text: &str) -> Vec { + let word = |c: char| c.is_alphanumeric() || matches!(c, '_' | '$' | ':'); + let mut out = Vec::new(); + let mut rest = text.trim_start(); + while let Some(c) = rest.chars().next() { + let len = if word(c) { + rest.find(|c: char| !word(c)).unwrap_or(rest.len()) + } else if rest.starts_with("=>") { + 2 + } else { + c.len_utf8() + }; + out.push(rest[..len].to_string()); + rest = rest[len..].trim_start(); + } + out +} + +/// `body` once per entry of `args` that `pattern` matches, each fragment's +/// `$name` replaced by the token it took. +fn expand(pattern: &[String], args: &[String], body: &str) -> Result { + let mut out = String::new(); + let mut i = 0; + while i < args.len() { + let mut bound: Vec<(&str, &str)> = Vec::new(); + for p in pattern { + let arg = args.get(i).ok_or("the arguments end inside an entry")?; + match p.strip_prefix('$').and_then(|v| v.split_once(':')) { + Some((name, _fragment)) => bound.push((name, arg)), + None if p == arg => {} + None => return Err(format!("`{arg}` where the pattern has `{p}`")), + } + i += 1; + } + // `$name` is a prefix of `$name_l`: the longer is replaced first. + bound.sort_by_key(|(name, _)| std::cmp::Reverse(name.len())); + let mut expanded = body.to_string(); + for (name, value) in bound { + expanded = expanded.replace(&format!("${name}"), value); + } + out.push_str(&expanded); + out.push('\n'); + } + Ok(out) +} + +/// Every `type Name = Type;` in libc, by name; a name two files alias to +/// types of different sign (the architectures' `WChar`) is either sign. +fn rust_aliases(texts: &[(String, String)]) -> BTreeMap { + let mut spelled: BTreeMap> = BTreeMap::new(); + for (_, text) in texts { + for line in text.lines() { + let t = line.trim(); + let t = t.strip_prefix("pub(crate) ").or_else(|| t.strip_prefix("pub ")).unwrap_or(t); + if let Some(rest) = t.strip_prefix("type ") { + if let Some((name, ty)) = rest.split_once('=') { + spelled.entry(name.trim().to_string()).or_default().push(ty.trim().trim_end_matches(';').to_string()); + } + } + } + } + let mut out = BTreeMap::new(); + for (name, types) in &spelled { + let classes: Vec = types.iter().filter_map(|t| rust_class(t, &BTreeMap::new())).collect(); + let class = match classes.as_slice() { + [Class::Int(b, _), ..] if classes.iter().all(|c| matches!(c, Class::Int(x, _) if x == b)) && classes.windows(2).any(|w| w[0] != w[1]) => Class::Int(*b, None), + [c, ..] => *c, + [] => continue, + }; + out.insert(name.clone(), class); + } + out +} + +fn rust_class(ty: &str, aliases: &BTreeMap) -> Option { + let ty = ty.trim(); + if ty.starts_with('*') || ty.starts_with('&') || ty.contains("fn(") || ty.starts_with("Option<") { + return Some(Class::Ptr); + } + let last = ty.rsplit("::").next().unwrap(); + let base = last.split('<').next().unwrap(); + Some(match base { + "!" | "()" => Class::Void, + "i8" | "c_schar" => Class::Int(1, Some(true)), + "u8" | "c_uchar" | "bool" => Class::Int(1, Some(false)), + "c_char" => Class::Int(1, None), + "i16" | "c_short" => Class::Int(2, Some(true)), + "u16" | "c_ushort" => Class::Int(2, Some(false)), + "i32" | "c_int" => Class::Int(4, Some(true)), + "u32" | "c_uint" => Class::Int(4, Some(false)), + "i64" | "isize" | "c_long" | "c_longlong" => Class::Int(8, Some(true)), + "u64" | "usize" | "c_ulong" | "c_ulonglong" => Class::Int(8, Some(false)), + "f32" => Class::F32, + "f64" => Class::F64, + "VaList" => Class::VaList, + other => match aliases.get(other) { + Some(c) => *c, + None if other.chars().next().is_some_and(char::is_uppercase) || other.contains('_') => Class::Aggregate, + None => return None, + }, + }) +} + +/// `pub unsafe extern "C" fn name(params) -> ret`, as one line. +fn rust_signature(head: &str, aliases: &BTreeMap) -> Option<(String, Signature)> { + // An arrow's `>` is no bracket: `~~` keeps every index where it was. + let head = head.replace("->", "~~"); + let after = &head[head.find("fn ")? + 3..]; + let open = after.find('(')?; + let name = after[..open].trim().to_string(); + let mut depth = 0; + let mut close = None; + for (i, c) in after.char_indices().skip(open) { + match c { + '(' | '<' | '[' => depth += 1, + ')' | '>' | ']' => { + depth -= 1; + if depth == 0 { + close = Some(i); + break; + } + } + _ => {} + } + } + let close = close?; + let mut params = Vec::new(); + let mut variadic = false; + let tokens: Vec = after[open + 1..close].chars().map(|c| c.to_string()).collect(); + for param in split_commas(&tokens) { + let text = param.concat().replace("~~", "->"); + let text = text.trim(); + if text.is_empty() { + continue; + } + let ty = text.split_once(':').map_or(text, |(_, ty)| ty).trim(); + if ty == "..." { + variadic = true; + continue; + } + params.push(rust_class(ty, aliases)?); + } + let rest = after[close + 1..].trim(); + let ret = match rest.strip_prefix("~~") { + Some(r) => rust_class(r.split(['{', ';']).next()?.trim().trim_end_matches("where").trim(), aliases)?, + None => Class::Void, + }; + Some((name, Signature { ret, params, variadic })) +} + +#[test] +fn every_prototype_is_a_definition_and_every_definition_is_declared() { + let root = libc(); + let (prototypes, inline, mut wrong) = headers(&root.join("include")); + let (defs, unread) = definitions(&root.join("src")); + wrong.extend(unread); + + for (name, decls) in &prototypes { + for pair in decls.windows(2) { + if !pair[0].signature.agrees(&pair[1].signature) { + wrong.push(format!("{name}: {} and {} declare it differently: {:?} and {:?}", pair[0].header, pair[1].header, pair[0].signature, pair[1].signature)); + } + } + let decl = &decls[0]; + match defs.get(name) { + None => wrong.push(format!("{name}: {} declares it, and libc defines no such function", decl.header)), + Some(Definition { signature: Some(def), file }) if !decl.signature.reaches(def) => wrong.push(format!( + "{name}: {} declares {:?}, and {file} defines {:?}", + decl.header, decl.signature, def + )), + Some(_) => {} + } + } + for (name, def) in &defs { + let excused = UNDECLARED.iter().any(|(n, _)| n == name); + let declared = prototypes.contains_key(name) || inline.contains(name); + if !declared && !excused { + wrong.push(format!("{name}: {} defines it, and no header declares it", def.file)); + } + if declared && excused { + wrong.push(format!("{name}: declared, and still excused in UNDECLARED")); + } + } + for (name, _) in UNDECLARED { + if !defs.contains_key(*name) { + wrong.push(format!("{name}: excused in UNDECLARED, and libc no longer defines it")); + } + } + assert!(prototypes.len() > 100 && defs.len() > 100, "{} prototypes, {} definitions read", prototypes.len(), defs.len()); + assert!(wrong.is_empty(), "libc's headers and definitions part in {} places:\n {}", wrong.len(), wrong.join("\n ")); +} diff --git a/toyos-libc-copies/src/text_differential.rs b/toyos-libc-copies/src/text_differential.rs new file mode 100644 index 00000000000..8f9c0e1ad06 --- /dev/null +++ b/toyos-libc-copies/src/text_differential.rs @@ -0,0 +1,82 @@ +//! `strnlen` against the host C library's, over every bound around every +//! string in a buffer that ends in no NUL; and `strsignal`'s texts: one of its +//! own for each signal `include/signal.h` numbers, in glibc's words. + +use std::collections::BTreeSet; + +use crate::text; + +extern "C" { + #[link_name = "strnlen"] + fn host_strnlen(s: *const u8, max: usize) -> usize; +} + +#[test] +fn strnlen_is_the_host_libraries() { + let buf = b"abc\0\0defgh\0x"; + for start in 0..buf.len() { + for max in 0..=buf.len() - start { + let s = buf[start..].as_ptr(); + // SAFETY: `max` bytes from `s` are inside `buf`, and neither reads + // past `s + max` nor past a NUL. + let (ours, host) = unsafe { (text::strnlen(s, max), host_strnlen(s, max)) }; + assert_eq!(ours, host, "strnlen at {start}, bounded by {max}"); + } + } +} + +/// glibc's `strsignal` for each number Linux gives a signal `signal.h` names. +const GLIBC: &[(&str, &str)] = &[ + ("SIGHUP", "Hangup"), + ("SIGINT", "Interrupt"), + ("SIGQUIT", "Quit"), + ("SIGILL", "Illegal instruction"), + ("SIGTRAP", "Trace/breakpoint trap"), + ("SIGABRT", "Aborted"), + ("SIGBUS", "Bus error"), + ("SIGFPE", "Floating point exception"), + ("SIGKILL", "Killed"), + ("SIGUSR1", "User defined signal 1"), + ("SIGSEGV", "Segmentation fault"), + ("SIGUSR2", "User defined signal 2"), + ("SIGPIPE", "Broken pipe"), + ("SIGALRM", "Alarm clock"), + ("SIGTERM", "Terminated"), + ("SIGCHLD", "Child exited"), + ("SIGCONT", "Continued"), + ("SIGSTOP", "Stopped (signal)"), +]; + +fn text_of(sig: i32) -> String { + let bytes = text::signal_text(sig); + let (last, text) = bytes.split_last().expect("an empty text"); + assert_eq!(*last, 0, "strsignal({sig}) is not NUL-terminated"); + assert!(!text.contains(&0), "strsignal({sig}) has a NUL inside"); + String::from_utf8(text.to_vec()).unwrap() +} + +#[test] +fn every_signal_signal_h_numbers_has_glibcs_text() { + let header = include_str!("../../userland/libc/include/signal.h"); + // `#define SIGHUP 1`, and not `SIG_DFL` or `SIG_BLOCK`. + let numbered: Vec<(&str, i32)> = header + .lines() + .filter_map(|line| line.strip_prefix("#define ")) + .filter_map(|rest| { + let mut words = rest.split_whitespace(); + let name = words.next().filter(|n| n.starts_with("SIG") && !n.contains('_'))?; + Some((name, words.next()?.parse().ok()?)) + }) + .collect(); + assert_eq!(numbered.len(), GLIBC.len(), "signal.h numbers {numbered:?}"); + let mut texts = BTreeSet::new(); + for (name, sig) in numbered { + let (_, words) = GLIBC.iter().find(|(n, _)| *n == name).unwrap_or_else(|| panic!("{name}: no glibc text to hold strsignal to")); + assert_eq!(text_of(sig), *words, "strsignal({name} = {sig})"); + texts.insert(text_of(sig)); + } + assert_eq!(texts.len(), GLIBC.len(), "two signals share a text"); + for unnamed in [0, 16, 20, 64, -1, i32::MAX] { + assert_eq!(text_of(unnamed), "Unknown signal", "strsignal({unnamed})"); + } +} diff --git a/userland/libc/include/dirent.h b/userland/libc/include/dirent.h new file mode 100644 index 00000000000..794953b9ef1 --- /dev/null +++ b/userland/libc/include/dirent.h @@ -0,0 +1,37 @@ +#ifndef _DIRENT_H +#define _DIRENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct __dirstream DIR; + +struct dirent { + ino_t d_ino; + unsigned char d_type; + char d_name[256]; +}; + +/* What d_type can say. ToyOS's listing tells a directory from everything + else, so readdir answers DT_DIR or DT_UNKNOWN. */ +#define DT_UNKNOWN 0 +#define DT_FIFO 1 +#define DT_CHR 2 +#define DT_DIR 4 +#define DT_BLK 6 +#define DT_REG 8 +#define DT_LNK 10 +#define DT_SOCK 12 + +DIR *opendir(const char *path); +struct dirent *readdir(DIR *dir); +int closedir(DIR *dir); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/userland/libc/include/dlfcn.h b/userland/libc/include/dlfcn.h index 900f341e679..0480a0fe6ee 100644 --- a/userland/libc/include/dlfcn.h +++ b/userland/libc/include/dlfcn.h @@ -7,8 +7,16 @@ #define RTLD_LOCAL 0x000 #define RTLD_DEFAULT ((void *)0) +typedef struct { + const char *dli_fname; + void *dli_fbase; + const char *dli_sname; + void *dli_saddr; +} Dl_info; + void *dlopen(const char *filename, int flags); void *dlsym(void *handle, const char *symbol); +int dladdr(const void *addr, Dl_info *info); int dlclose(void *handle); char *dlerror(void); diff --git a/userland/libc/include/endian.h b/userland/libc/include/endian.h new file mode 100644 index 00000000000..b7b5d4d242e --- /dev/null +++ b/userland/libc/include/endian.h @@ -0,0 +1,28 @@ +#ifndef _ENDIAN_H +#define _ENDIAN_H + +#include + +#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ +#error "every architecture ToyOS runs on is little-endian" +#endif + +#define LITTLE_ENDIAN 1234 +#define BIG_ENDIAN 4321 +#define BYTE_ORDER LITTLE_ENDIAN + +#define htobe16(x) __builtin_bswap16((uint16_t)(x)) +#define htobe32(x) __builtin_bswap32((uint32_t)(x)) +#define htobe64(x) __builtin_bswap64((uint64_t)(x)) +#define be16toh(x) __builtin_bswap16((uint16_t)(x)) +#define be32toh(x) __builtin_bswap32((uint32_t)(x)) +#define be64toh(x) __builtin_bswap64((uint64_t)(x)) + +#define htole16(x) ((uint16_t)(x)) +#define htole32(x) ((uint32_t)(x)) +#define htole64(x) ((uint64_t)(x)) +#define le16toh(x) ((uint16_t)(x)) +#define le32toh(x) ((uint32_t)(x)) +#define le64toh(x) ((uint64_t)(x)) + +#endif diff --git a/userland/libc/include/errno.h b/userland/libc/include/errno.h index 7fc819d6df5..11ab578cf96 100644 --- a/userland/libc/include/errno.h +++ b/userland/libc/include/errno.h @@ -37,6 +37,7 @@ int *__errno_location(void); #define ERANGE 34 #define ENOSYS 38 #define ELOOP 40 +#define EOVERFLOW 75 #define ENAMETOOLONG 36 #define ENOTEMPTY 39 #define EWOULDBLOCK EAGAIN @@ -46,6 +47,8 @@ int *__errno_location(void); #define EMSGSIZE 90 #define EPROTOTYPE 91 #define ENOPROTOOPT 92 +#define EOPNOTSUPP 95 +#define ENOTSUP EOPNOTSUPP #define EAFNOSUPPORT 97 #define EADDRINUSE 98 #define EADDRNOTAVAIL 99 diff --git a/userland/libc/include/fcntl.h b/userland/libc/include/fcntl.h index 95c0b4eab97..6ddf8f363d6 100644 --- a/userland/libc/include/fcntl.h +++ b/userland/libc/include/fcntl.h @@ -18,10 +18,25 @@ #define F_SETFD 2 #define F_GETFL 3 #define F_SETFL 4 +#define F_GETLK 5 +#define F_SETLK 6 +#define F_SETLKW 7 #define FD_CLOEXEC 1 +#define F_RDLCK 0 +#define F_WRLCK 1 +#define F_UNLCK 2 + +struct flock { + short l_type; + short l_whence; + off_t l_start; + off_t l_len; + pid_t l_pid; +}; + int open(const char *path, int flags, ...); int fcntl(int fd, int cmd, ...); -int creat(const char *path, int mode); +int creat(const char *path, mode_t mode); #endif diff --git a/userland/libc/include/limits.h b/userland/libc/include/limits.h index a939aee8e7d..6ee0424cfd3 100644 --- a/userland/libc/include/limits.h +++ b/userland/libc/include/limits.h @@ -21,6 +21,7 @@ #define ULLONG_MAX ULONG_MAX #define PATH_MAX 4096 +#define _POSIX_ARG_MAX 4096 #define NAME_MAX 255 #endif diff --git a/userland/libc/include/math.h b/userland/libc/include/math.h index 88b3555ff0b..e5660774fa4 100644 --- a/userland/libc/include/math.h +++ b/userland/libc/include/math.h @@ -27,6 +27,8 @@ double cosh(double x); double tanh(double x); double round(double x); double trunc(double x); +double modf(double x, double *iptr); +double logb(double x); float floorf(float x); float ceilf(float x); float sqrtf(float x); diff --git a/userland/libc/include/pwd.h b/userland/libc/include/pwd.h new file mode 100644 index 00000000000..bf9001abac0 --- /dev/null +++ b/userland/libc/include/pwd.h @@ -0,0 +1,26 @@ +#ifndef _PWD_H +#define _PWD_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct passwd { + char *pw_name; + uid_t pw_uid; + gid_t pw_gid; + char *pw_dir; + char *pw_shell; +}; + +int getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); +int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/userland/libc/include/signal.h b/userland/libc/include/signal.h index 9b523773129..a7da2508a84 100644 --- a/userland/libc/include/signal.h +++ b/userland/libc/include/signal.h @@ -10,7 +10,9 @@ #define SIGBUS 7 #define SIGFPE 8 #define SIGKILL 9 +#define SIGUSR1 10 #define SIGSEGV 11 +#define SIGUSR2 12 #define SIGPIPE 13 #define SIGALRM 14 #define SIGTERM 15 @@ -42,6 +44,9 @@ typedef struct { #define SA_SIGINFO 4 #define SA_RESTART 0x10000000 +#define SA_ONSTACK 0x08000000 +#define SA_NODEFER 0x40000000 +#define SA_RESETHAND 0x80000000 struct sigaction { union { diff --git a/userland/libc/include/stdlib.h b/userland/libc/include/stdlib.h index 4154e12549d..b0b7d7f13f4 100644 --- a/userland/libc/include/stdlib.h +++ b/userland/libc/include/stdlib.h @@ -31,6 +31,7 @@ double strtod(const char *s, char **endptr); char *getenv(const char *name); int setenv(const char *name, const char *value, int overwrite); int unsetenv(const char *name); +char *realpath(const char *path, char *resolved); void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); diff --git a/userland/libc/include/string.h b/userland/libc/include/string.h index 4c9e10be5c0..1549f1f3edf 100644 --- a/userland/libc/include/string.h +++ b/userland/libc/include/string.h @@ -10,6 +10,8 @@ int memcmp(const void *s1, const void *s2, size_t n); void *memchr(const void *s, int c, size_t n); size_t strlen(const char *s); +size_t strnlen(const char *s, size_t maxlen); +char *strsignal(int sig); char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); char *strcat(char *dest, const char *src); diff --git a/userland/libc/include/sys/mman.h b/userland/libc/include/sys/mman.h index 7c5d0f10d2d..fafe2db085c 100644 --- a/userland/libc/include/sys/mman.h +++ b/userland/libc/include/sys/mman.h @@ -16,8 +16,20 @@ #define MAP_FAILED ((void *)-1) +#define MS_ASYNC 1 +#define MS_INVALIDATE 2 +#define MS_SYNC 4 + +#define POSIX_MADV_NORMAL 0 +#define POSIX_MADV_RANDOM 1 +#define POSIX_MADV_SEQUENTIAL 2 +#define POSIX_MADV_WILLNEED 3 +#define POSIX_MADV_DONTNEED 4 + void *mmap(void *addr, size_t length, int prot, int flags, int fd, long offset); int munmap(void *addr, size_t length); +int msync(void *addr, size_t len, int flags); +int posix_madvise(void *addr, size_t len, int advice); int mprotect(void *addr, size_t len, int prot); #endif diff --git a/userland/libc/include/sys/resource.h b/userland/libc/include/sys/resource.h new file mode 100644 index 00000000000..4a768ef40a6 --- /dev/null +++ b/userland/libc/include/sys/resource.h @@ -0,0 +1,59 @@ +#ifndef _SYS_RESOURCE_H +#define _SYS_RESOURCE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned long rlim_t; + +#define RLIM_INFINITY (~(rlim_t)0) +#define RLIM_SAVED_MAX RLIM_INFINITY +#define RLIM_SAVED_CUR RLIM_INFINITY + +#define RLIMIT_CPU 0 +#define RLIMIT_FSIZE 1 +#define RLIMIT_DATA 2 +#define RLIMIT_STACK 3 +#define RLIMIT_CORE 4 +#define RLIMIT_RSS 5 +#define RLIMIT_NOFILE 7 +#define RLIMIT_AS 9 + +#define RUSAGE_SELF 0 +#define RUSAGE_CHILDREN (-1) + +struct rlimit { + rlim_t rlim_cur; + rlim_t rlim_max; +}; + +struct rusage { + struct timeval ru_utime; + struct timeval ru_stime; + long ru_maxrss; + long ru_ixrss; + long ru_idrss; + long ru_isrss; + long ru_minflt; + long ru_majflt; + long ru_nswap; + long ru_inblock; + long ru_oublock; + long ru_msgsnd; + long ru_msgrcv; + long ru_nsignals; + long ru_nvcsw; + long ru_nivcsw; +}; + +int getrlimit(int resource, struct rlimit *rlp); +int setrlimit(int resource, const struct rlimit *rlp); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/userland/libc/include/sys/socket.h b/userland/libc/include/sys/socket.h index f1921150dea..1086333c61d 100644 --- a/userland/libc/include/sys/socket.h +++ b/userland/libc/include/sys/socket.h @@ -7,6 +7,7 @@ typedef unsigned int socklen_t; #define AF_UNSPEC 0 +#define AF_UNIX 1 #define AF_INET 2 #define SOCK_STREAM 1 @@ -28,13 +29,15 @@ typedef unsigned int socklen_t; #define MSG_NOSIGNAL 0x4000 +typedef unsigned short sa_family_t; + struct sockaddr { - unsigned short sa_family; + sa_family_t sa_family; char sa_data[14]; }; struct sockaddr_storage { - unsigned short ss_family; + sa_family_t ss_family; char _pad[126]; }; diff --git a/userland/libc/include/sys/stat.h b/userland/libc/include/sys/stat.h index bf24651b0b5..fb74c80fa47 100644 --- a/userland/libc/include/sys/stat.h +++ b/userland/libc/include/sys/stat.h @@ -61,6 +61,7 @@ int fstat(int fd, struct stat *buf); int lstat(const char *path, struct stat *buf); int mkdir(const char *path, mode_t mode); int chmod(const char *path, mode_t mode); +int fchmod(int fd, mode_t mode); mode_t umask(mode_t mask); #endif diff --git a/userland/libc/include/sys/statvfs.h b/userland/libc/include/sys/statvfs.h new file mode 100644 index 00000000000..b00652ab4f9 --- /dev/null +++ b/userland/libc/include/sys/statvfs.h @@ -0,0 +1,35 @@ +#ifndef _SYS_STATVFS_H +#define _SYS_STATVFS_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned long fsblkcnt_t; +typedef unsigned long fsfilcnt_t; + +struct statvfs { + unsigned long f_bsize; + unsigned long f_frsize; + fsblkcnt_t f_blocks; + fsblkcnt_t f_bfree; + fsblkcnt_t f_bavail; + fsfilcnt_t f_files; + fsfilcnt_t f_ffree; + fsfilcnt_t f_favail; + unsigned long f_fsid; + unsigned long f_flag; + unsigned long f_namemax; +}; + +#define ST_RDONLY 1 +#define ST_NOSUID 2 + +int statvfs(const char *path, struct statvfs *buf); +int fstatvfs(int fd, struct statvfs *buf); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/userland/libc/include/sys/un.h b/userland/libc/include/sys/un.h new file mode 100644 index 00000000000..cbb69cdd1f1 --- /dev/null +++ b/userland/libc/include/sys/un.h @@ -0,0 +1,11 @@ +#ifndef _SYS_UN_H +#define _SYS_UN_H + +#include + +struct sockaddr_un { + sa_family_t sun_family; + char sun_path[108]; +}; + +#endif diff --git a/userland/libc/include/sys/utsname.h b/userland/libc/include/sys/utsname.h new file mode 100644 index 00000000000..9281de190c4 --- /dev/null +++ b/userland/libc/include/sys/utsname.h @@ -0,0 +1,22 @@ +#ifndef _SYS_UTSNAME_H +#define _SYS_UTSNAME_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct utsname { + char sysname[65]; + char nodename[65]; + char release[65]; + char version[65]; + char machine[65]; +}; + +int uname(struct utsname *buf); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/userland/libc/include/sys/wait.h b/userland/libc/include/sys/wait.h index 0a9ea00c67d..b4dbcdf2eff 100644 --- a/userland/libc/include/sys/wait.h +++ b/userland/libc/include/sys/wait.h @@ -11,7 +11,6 @@ #define WIFEXITED(s) (WTERMSIG(s) == 0) #define WIFSIGNALED(s) (WTERMSIG(s) != 0) -pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); #endif diff --git a/userland/libc/include/sysexits.h b/userland/libc/include/sysexits.h new file mode 100644 index 00000000000..108848f95ee --- /dev/null +++ b/userland/libc/include/sysexits.h @@ -0,0 +1,23 @@ +#ifndef _SYSEXITS_H +#define _SYSEXITS_H + +#define EX_OK 0 +#define EX__BASE 64 +#define EX_USAGE 64 +#define EX_DATAERR 65 +#define EX_NOINPUT 66 +#define EX_NOUSER 67 +#define EX_NOHOST 68 +#define EX_UNAVAILABLE 69 +#define EX_SOFTWARE 70 +#define EX_OSERR 71 +#define EX_OSFILE 72 +#define EX_CANTCREAT 73 +#define EX_IOERR 74 +#define EX_TEMPFAIL 75 +#define EX_PROTOCOL 76 +#define EX_NOPERM 77 +#define EX_CONFIG 78 +#define EX__MAX 78 + +#endif diff --git a/userland/libc/include/unistd.h b/userland/libc/include/unistd.h index 4b5c401a7ff..fd9e31b38dd 100644 --- a/userland/libc/include/unistd.h +++ b/userland/libc/include/unistd.h @@ -23,12 +23,24 @@ unsigned int sleep(unsigned int seconds); int usleep(unsigned int usec); int isatty(int fd); int execvp(const char *file, char *const argv[]); +int execv(const char *path, char *const argv[]); +int execve(const char *path, char *const argv[], char *const envp[]); int fork(void); int pipe(int pipefd[2]); void _exit(int status); +int fsync(int fd); +int ftruncate(int fd, off_t length); +ssize_t readlink(const char *path, char *buf, size_t size); +int symlink(const char *target, const char *linkpath); +int link(const char *existing, const char *newpath); +int fchown(int fd, uid_t owner, gid_t group); +unsigned int alarm(unsigned int seconds); +int gethostname(char *name, size_t len); pid_t getpid(void); pid_t getppid(void); +pid_t getsid(pid_t pid); +pid_t setsid(void); uid_t getuid(void); uid_t geteuid(void); gid_t getgid(void); @@ -37,7 +49,10 @@ int kill(pid_t pid, int sig); long sysconf(int name); +#define _SC_ARG_MAX 0 #define _SC_PAGESIZE 30 +#define _SC_PAGE_SIZE _SC_PAGESIZE +#define _SC_GETPW_R_SIZE_MAX 70 #define _SC_CLK_TCK 2 #define _SC_NPROCESSORS_ONLN 84 diff --git a/userland/libc/src/elfsym.rs b/userland/libc/src/elfsym.rs new file mode 100644 index 00000000000..c140215a31f --- /dev/null +++ b/userland/libc/src/elfsym.rs @@ -0,0 +1,149 @@ +//! What `dladdr` reads of a loaded image: whether an address lies in it, where +//! its first mapped byte is, and which of its dynamic symbols holds the +//! address, chosen as glibc's `dladdr` chooses. It reads nothing but the image +//! it is handed, so the host tests it on an image it lays out +//! (`toyos-libc-copies`). +//! +//! The loader relocates nothing in `PT_DYNAMIC`, so every address the dynamic +//! table holds is relative to the image's base. + +const PT_LOAD: u32 = 1; +const PT_DYNAMIC: u32 = 2; +const PHDR_SIZE: usize = 56; + +const DT_NULL: u64 = 0; +const DT_HASH: u64 = 4; +const DT_STRTAB: u64 = 5; +const DT_SYMTAB: u64 = 6; +const DT_STRSZ: u64 = 10; +const DT_GNU_HASH: u64 = 0x6fff_fef5; + +const SYM_SIZE: usize = 24; +const SHN_UNDEF: u16 = 0; +const SHN_ABS: u16 = 0xfff1; +const STB_GLOBAL: u8 = 1; +const STB_WEAK: u8 = 2; +const STT_TLS: u8 = 6; +const STV_INTERNAL: u8 = 1; +const STV_HIDDEN: u8 = 2; + +/// A loaded image, as `dl_iterate_phdr` describes it. +#[derive(Clone, Copy)] +pub(crate) struct Image { + pub(crate) base: u64, + pub(crate) phdr: *const u8, + pub(crate) phnum: usize, +} + +/// The fields of a program header this module reads. +struct Segment { + kind: u32, + vaddr: u64, + memsz: u64, +} + +impl Image { + /// # Safety + /// `phdr` points at `phnum` readable program headers. + unsafe fn segments(self) -> impl Iterator { + (0..self.phnum).map(move |i| { + // SAFETY: the caller's. + let p = unsafe { self.phdr.add(i * PHDR_SIZE) }; + unsafe { Segment { kind: read(p, 0), vaddr: read(p, 16), memsz: read(p, 40) } } + }) + } + + /// Whether a `PT_LOAD` segment of the image holds `addr`. + /// + /// # Safety + /// As [`Image::segments`]. + pub(crate) unsafe fn contains(self, addr: u64) -> bool { + unsafe { self.segments() }.any(|s| s.kind == PT_LOAD && addr.wrapping_sub(self.base + s.vaddr) < s.memsz) + } + + /// The image's first mapped byte: the page its lowest `PT_LOAD` starts in. + /// + /// # Safety + /// As [`Image::segments`]. + pub(crate) unsafe fn start(self) -> u64 { + let low = unsafe { self.segments() }.filter(|s| s.kind == PT_LOAD).map(|s| s.vaddr).min().unwrap_or(0); + self.base + (low & !0xfff) + } + + /// The name and address of the dynamic symbol of the image that holds + /// `addr`, as glibc chooses one: exported (global or weak, neither hidden + /// nor internal), no thread-local, defined in a section, with `addr` in + /// `[value, value + size)` or at `value` for a size of 0; of several, the + /// highest value. + /// + /// # Safety + /// As [`Image::segments`], and every table the image's `PT_DYNAMIC` names + /// is mapped. + pub(crate) unsafe fn symbol(self, addr: u64) -> Option<(*const u8, u64)> { + let dynamic = unsafe { self.segments() }.find(|s| s.kind == PT_DYNAMIC)?; + let at = |vaddr: u64| (self.base + vaddr) as *const u8; + let (mut symtab, mut strtab, mut strsz, mut hash, mut gnu_hash) = (None, None, 0, None, None); + let mut entry = at(dynamic.vaddr); + loop { + let (tag, value): (u64, u64) = unsafe { (read(entry, 0), read(entry, 8)) }; + match tag { + DT_NULL => break, + DT_SYMTAB => symtab = Some(value), + DT_STRTAB => strtab = Some(value), + DT_STRSZ => strsz = value, + DT_HASH => hash = Some(value), + DT_GNU_HASH => gnu_hash = Some(value), + _ => {} + } + entry = unsafe { entry.add(16) }; + } + let (symtab, strtab) = (symtab?, strtab?); + let count = match (gnu_hash, hash) { + (Some(table), _) => unsafe { gnu_hash_count(at(table)) }, + (None, Some(table)) => unsafe { read::(at(table), 4) as usize }, + // Adjacent in every layout a linker makes, as the loader also reads them. + (None, None) => strtab.saturating_sub(symtab) as usize / SYM_SIZE, + }; + let mut best: Option<(u64, u32)> = None; + for i in 0..count { + let sym = unsafe { at(symtab).add(i * SYM_SIZE) }; + let (name, info, other, shndx, value, size): (u32, u8, u8, u16, u64, u64) = + unsafe { (read(sym, 0), read(sym, 4), read(sym, 5), read(sym, 6), read(sym, 8), read(sym, 16)) }; + let (bind, visibility) = (info >> 4, other & 3); + let exported = (bind == STB_GLOBAL || bind == STB_WEAK) && visibility != STV_HIDDEN && visibility != STV_INTERNAL; + if !exported || info & 0xf == STT_TLS || shndx == SHN_UNDEF || shndx == SHN_ABS || u64::from(name) >= strsz { + continue; + } + let start = self.base + value; + let holds = addr >= start && (addr - start < size || (size == 0 && addr == start)); + if holds && best.is_none_or(|(v, _)| v < value) { + best = Some((value, name)); + } + } + best.map(|(value, name)| (unsafe { at(strtab).add(name as usize) }, self.base + value)) + } +} + +/// How many symbols the `.gnu.hash` table at `table` covers: through the end +/// of the chain the highest bucket starts, or up to its first hashed symbol +/// when every bucket is empty. +unsafe fn gnu_hash_count(table: *const u8) -> usize { + let (nbuckets, symoffset, bloom_words): (u32, u32, u32) = unsafe { (read(table, 0), read(table, 4), read(table, 8)) }; + let (nbuckets, symoffset) = (nbuckets as usize, symoffset as usize); + let buckets = unsafe { table.add(16 + bloom_words as usize * 8) }; + let chains = unsafe { buckets.add(nbuckets * 4) }; + let Some(mut i) = (0..nbuckets).map(|b| unsafe { read::(buckets, b * 4) as usize }).filter(|&s| s != 0).max() else { + return symoffset; + }; + // A chain's last hash has its low bit set. + while unsafe { read::(chains, (i - symoffset) * 4) } & 1 == 0 { + i += 1; + } + i + 1 +} + +/// # Safety +/// `at + offset` holds a `T`, however aligned. +unsafe fn read(at: *const u8, offset: usize) -> T { + unsafe { at.add(offset).cast::().read_unaligned() } +} diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index 04a9ada4f77..21805e4f9f2 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -16,3 +16,10 @@ pub extern "C" fn __errno_location() -> *mut i32 { pub(crate) fn set(code: i32) { ERRNO.set(code); } + +pub(crate) const ENOMEM: i32 = 12; +pub(crate) const ENODEV: i32 = 19; +pub(crate) const EINVAL: i32 = 22; +pub(crate) const ENOSYS: i32 = 38; +pub(crate) const EOVERFLOW: i32 = 75; +pub(crate) const EOPNOTSUPP: i32 = 95; diff --git a/userland/libc/src/fparts.rs b/userland/libc/src/fparts.rs new file mode 100644 index 00000000000..007682f3ba7 --- /dev/null +++ b/userland/libc/src/fparts.rs @@ -0,0 +1,60 @@ +//! A double's parts: `modf`'s integral and fractional parts and `logb`'s +//! exponent, read off its bits. It reads and sets nothing but what it is +//! handed, so the host holds it to its own C library's (`toyos-libc-copies`). + +const MANTISSA_BITS: u32 = 52; +const EXPONENT_MASK: u64 = 0x7ff; +const BIAS: i32 = 1023; + +/// `x`'s unbiased exponent field, `-1023` for zero and subnormals and `1024` +/// for infinities and NaNs. +fn exponent(x: f64) -> i32 { + ((x.to_bits() >> MANTISSA_BITS) & EXPONENT_MASK) as i32 - BIAS +} + +/// `modf`: `x`'s integral part and its fractional part, each with `x`'s sign; +/// an infinity's fraction is a zero and a NaN's parts are both the NaN. +pub(crate) fn modf(x: f64) -> (f64, f64) { + let e = exponent(x); + if x.is_nan() { + return (x, x); + } + let zero = f64::from_bits(x.to_bits() & (1 << 63)); + if e >= MANTISSA_BITS as i32 { + // No fraction bit left, infinities with them. + return (x, zero); + } + if e < 0 { + return (zero, x); + } + let fraction_bits = (1u64 << (MANTISSA_BITS as i32 - e)) - 1; + if x.to_bits() & fraction_bits == 0 { + return (x, zero); + } + let int = f64::from_bits(x.to_bits() & !fraction_bits); + // Exact: both have `x`'s sign and `int` shares every bit of `x` above + // the fraction's. + (int, x - int) +} + +/// `logb`: the exponent of `x` as a double, the one a normalized `x` would +/// have for a subnormal; `-inf` for a zero, `+inf` for an infinity, the NaN +/// for a NaN. +pub(crate) fn logb(x: f64) -> f64 { + if x.is_nan() { + return x; + } + if x.is_infinite() { + return f64::INFINITY; + } + if x == 0.0 { + return f64::NEG_INFINITY; + } + let e = exponent(x); + if e > -BIAS { + return f64::from(e); + } + // Subnormal: `mantissa * 2^-1074`, whose top set bit is the exponent. + let mantissa = x.to_bits() & ((1u64 << MANTISSA_BITS) - 1); + f64::from(63 - mantissa.leading_zeros() as i32 - 1074) +} diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 1052461cf53..b4901bd217a 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -5,17 +5,23 @@ extern crate alloc; mod arch; mod ctype; +mod elfsym; mod errno; +mod fparts; mod link; +mod listing; mod math; mod memory; +mod memreq; mod misc; mod posix_io; mod printf; mod pthread; +mod refused; mod socket; mod stdio; mod string; +mod text; mod time; // C runtime: the entry `arch::_start` calls, panic handler, and global allocator. diff --git a/userland/libc/src/link.rs b/userland/libc/src/link.rs index 7faffb24762..ef4e9a67255 100644 --- a/userland/libc/src/link.rs +++ b/userland/libc/src/link.rs @@ -1,15 +1,20 @@ -//! `dl_iterate_phdr`, from the kernel's `SYS_QUERY_MODULES` answer. +//! `dl_iterate_phdr` and `dladdr`, from the kernel's `SYS_QUERY_MODULES` +//! answer. //! -//! The contract is glibc's `dl_iterate_phdr(3)`, which the LSB adopts: the -//! executable first, named by the empty string, then every library in load -//! order; the walk stops at the first callback that returns non-zero and -//! returns that value, else 0. The walk is over one answer, so a module a -//! callback or another thread loads meanwhile is not visited. +//! `dl_iterate_phdr`'s contract is glibc's `dl_iterate_phdr(3)`, which the +//! LSB adopts: the executable first, named by the empty string, then every +//! library in load order; the walk stops at the first callback that returns +//! non-zero and returns that value, else 0. The walk is over one answer, so a +//! module a callback or another thread loads meanwhile is not visited. +use alloc::boxed::Box; use alloc::vec::Vec; +use core::sync::atomic::{AtomicPtr, Ordering}; use toyos_abi::syscall; +use crate::elfsym::Image; + /// `struct dl_phdr_info`, as `link.h` declares it. #[repr(C)] pub struct DlPhdrInfo { @@ -46,6 +51,83 @@ pub unsafe extern "C" fn dl_iterate_phdr(callback: Callback, data: *mut u8) -> i 0 } +/// `Dl_info`, as `dlfcn.h` declares it. +#[repr(C)] +pub struct DlInfo { + dli_fname: *const u8, + dli_fbase: *mut u8, + dli_sname: *const u8, + dli_saddr: *mut u8, +} + +/// The image holding `addr`, named by the path it was loaded from, and the +/// dynamic symbol of it that holds `addr` (`elfsym`), or null names where it +/// exports none; 0 where no image holds `addr`. +#[no_mangle] +pub unsafe extern "C" fn dladdr(addr: *const u8, info: *mut DlInfo) -> i32 { + let answer = answer(); + for (module, path) in syscall::modules(&answer) { + let image = Image { + base: module.base, + phdr: module.phdr as *const u8, + phnum: usize::try_from(module.phnum).expect("SYS_QUERY_MODULES: a phnum past usize"), + }; + // SAFETY: the kernel's description of an image this process has loaded. + if !unsafe { image.contains(addr.addr() as u64) } { + continue; + } + let symbol = unsafe { image.symbol(addr.addr() as u64) }; + unsafe { + *info = DlInfo { + dli_fname: named(module.base, path), + dli_fbase: core::ptr::with_exposed_provenance_mut(image.start() as usize), + dli_sname: symbol.map_or(core::ptr::null(), |(name, _)| name), + dli_saddr: symbol.map_or(core::ptr::null_mut(), |(_, at)| core::ptr::with_exposed_provenance_mut(at as usize)), + }; + } + return 1; + } + 0 +} + +/// A path `dladdr` has answered with, NUL-terminated, beside the base of the +/// image it named: kept for the life of the process, since `dli_fname` must +/// outlive every call, and each image's added once. +struct Named { + base: u64, + path: Vec, + next: *mut Named, +} + +static NAMED: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + +/// `path` of the image at `base`, as a C string that outlives the process's +/// every call. Two threads naming a new image at once may each add it. +fn named(base: u64, path: &[u8]) -> *const u8 { + let mut at = NAMED.load(Ordering::Acquire); + while !at.is_null() { + // SAFETY: a node once published is never freed or changed. + let node = unsafe { &*at }; + if node.base == base && node.path[..node.path.len() - 1] == *path { + return node.path.as_ptr(); + } + at = node.next; + } + let mut owned = Vec::with_capacity(path.len() + 1); + owned.extend_from_slice(path); + owned.push(0); + let node = Box::into_raw(Box::new(Named { base, path: owned, next: core::ptr::null_mut() })); + loop { + let head = NAMED.load(Ordering::Acquire); + // SAFETY: `node` is this thread's until the exchange publishes it. + unsafe { (*node).next = head }; + if NAMED.compare_exchange(head, node, Ordering::AcqRel, Ordering::Acquire).is_ok() { + // SAFETY: published, and never freed. + return unsafe { (*node).path.as_ptr() }; + } + } +} + /// One whole `SYS_QUERY_MODULES` answer: asked again while a `dlopen` /// elsewhere grows it between the size and the read. fn answer() -> Vec { diff --git a/userland/libc/src/listing.rs b/userland/libc/src/listing.rs new file mode 100644 index 00000000000..1fcabe549aa --- /dev/null +++ b/userland/libc/src/listing.rs @@ -0,0 +1,27 @@ +//! The kernel's answer to `SYS_READDIR`, one entry at a time: a kind byte (1 +//! for a file, 2 for a directory), the name, a NUL, and the size as eight +//! bytes, little-endian. It reads nothing but what it is handed, so the host +//! tests it (`toyos-libc-copies`). + +/// One entry of a listing. +pub(crate) struct Entry<'a> { + pub(crate) is_dir: bool, + pub(crate) name: &'a [u8], +} + +/// The entry at `*pos` in `answer`, with `*pos` moved past it; `None` once +/// `*pos` is at the end. An answer of any other shape is a kernel that broke +/// its ABI, and panics. +pub(crate) fn next<'a>(answer: &'a [u8], pos: &mut usize) -> Option> { + let rest = answer.get(*pos..).filter(|r| !r.is_empty())?; + let is_dir = match rest[0] { + 1 => false, + 2 => true, + kind => panic!("SYS_READDIR answered an entry of kind {kind}"), + }; + let len = rest[1..].iter().position(|&b| b == 0).expect("SYS_READDIR answered a name with no NUL"); + let end = 1 + len + 1 + 8; + assert!(rest.len() >= end, "SYS_READDIR answered an entry with no size"); + *pos += end; + Some(Entry { is_dir, name: &rest[1..1 + len] }) +} diff --git a/userland/libc/src/math.rs b/userland/libc/src/math.rs index 2d97c989566..1af00e1c843 100644 --- a/userland/libc/src/math.rs +++ b/userland/libc/src/math.rs @@ -241,6 +241,18 @@ pub extern "C" fn fabs(x: f64) -> f64 { f64::from_bits(x.to_bits() & !(1u64 << 6 #[no_mangle] pub extern "C" fn fabsf(x: f32) -> f32 { f32::from_bits(x.to_bits() & !(1u32 << 31)) } +#[no_mangle] +pub unsafe extern "C" fn modf(x: f64, iptr: *mut f64) -> f64 { + let (int, fraction) = crate::fparts::modf(x); + unsafe { *iptr = int }; + fraction +} + +#[no_mangle] +pub extern "C" fn logb(x: f64) -> f64 { + crate::fparts::logb(x) +} + // --- Utility functions --- #[no_mangle] diff --git a/userland/libc/src/memory.rs b/userland/libc/src/memory.rs index f3e0b101f43..7c006db81ea 100644 --- a/userland/libc/src/memory.rs +++ b/userland/libc/src/memory.rs @@ -11,6 +11,18 @@ extern "C" { pub fn free(p: *mut u8); } +/// `aligned_alloc` beside std: a block std's `malloc` gives, for an alignment +/// it gives, and C11's `EINVAL` for any other (`memreq::std_aligns`). +#[cfg(feature = "std-runtime")] +#[no_mangle] +pub unsafe extern "C" fn aligned_alloc(align: usize, size: usize) -> *mut u8 { + if !crate::memreq::std_aligns(align) { + crate::errno::set(crate::errno::EINVAL); + return ptr::null_mut(); + } + unsafe { malloc(size) } +} + #[cfg(not(feature = "std-runtime"))] mod backend { use core::alloc::Layout; diff --git a/userland/libc/src/memreq.rs b/userland/libc/src/memreq.rs new file mode 100644 index 00000000000..0498df1ca45 --- /dev/null +++ b/userland/libc/src/memreq.rs @@ -0,0 +1,51 @@ +//! What libc's memory calls refuse before the kernel or the allocator is +//! asked. It reads nothing but what it is handed, so the host tests it +//! (`toyos-libc-copies`). + +pub(crate) const PROT_READ: i32 = 0x1; +pub(crate) const PROT_WRITE: i32 = 0x2; +pub(crate) const PROT_EXEC: i32 = 0x4; +pub(crate) const MAP_FIXED: i32 = 0x10; +pub(crate) const MAP_ANONYMOUS: i32 = 0x20; + +/// The page `sysconf(_SC_PAGESIZE)` answers. +const PAGE: usize = 4096; + +/// Why `mmap` cannot give what it was asked. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum MapRefusal { + /// No bytes, or a fixed place off a page boundary: `EINVAL`. + Invalid, + /// A file's bytes, which the kernel maps for no process: `ENODEV`. + File, + /// Executable memory, which the kernel does not map: `ENOTSUP`. + Exec, +} + +/// Why `mmap` of `len` bytes at `addr` with `prot` and `flags` is refused, if +/// it is. An anonymous `MAP_SHARED` is not: with no `fork`, no other process +/// can map it, so a private mapping is all that sharing it could mean. +pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Option { + if len == 0 || (flags & MAP_FIXED != 0 && addr % PAGE != 0) { + return Some(MapRefusal::Invalid); + } + if flags & MAP_ANONYMOUS == 0 { + return Some(MapRefusal::File); + } + if prot & PROT_EXEC != 0 { + return Some(MapRefusal::Exec); + } + None +} + +/// Whether `advice` is one of `posix_madvise`'s five. +pub(crate) fn is_advice(advice: i32) -> bool { + (0..=4).contains(&advice) +} + +/// Whether `aligned_alloc` beside std can align to `align`: std's `malloc` +/// aligns every block to 16, and its `free` knows no other alignment. +#[cfg(feature = "std-runtime")] +pub(crate) fn std_aligns(align: usize) -> bool { + align.is_power_of_two() && align <= 16 +} diff --git a/userland/libc/src/misc.rs b/userland/libc/src/misc.rs index 4443d6cd7c8..0d439b91445 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -215,7 +215,9 @@ pub unsafe extern "C" fn kill(_pid: i32, _sig: i32) -> i32 { // sysconf +const _SC_ARG_MAX: i32 = 0; const _SC_PAGESIZE: i32 = 30; +const _SC_GETPW_R_SIZE_MAX: i32 = 70; const _SC_NPROCESSORS_ONLN: i32 = 84; const _SC_CLK_TCK: i32 = 2; @@ -225,7 +227,13 @@ pub unsafe extern "C" fn sysconf(name: i32) -> i64 { _SC_PAGESIZE => 4096, _SC_NPROCESSORS_ONLN => syscall::cpu_count() as i64, _SC_CLK_TCK => 100, - _ => -1, + // POSIX's -1 with `errno` untouched: the function each bounds, `exec` + // and the `getpw*_r` lookups, is one ToyOS does not have. + _SC_ARG_MAX | _SC_GETPW_R_SIZE_MAX => -1, + _ => { + crate::errno::set(crate::errno::EINVAL); + -1 + } } } diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index d593d57696f..83f3656dcef 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -99,6 +99,11 @@ pub unsafe extern "C" fn open(path: *const u8, flags: i32, _mode: u32) -> i32 { } } +#[no_mangle] +pub unsafe extern "C" fn creat(path: *const u8, mode: u32) -> i32 { + unsafe { open(path, O_WRONLY | O_CREAT | O_TRUNC, mode) } +} + #[no_mangle] pub unsafe extern "C" fn close(raw_fd: i32) -> i32 { syscall::close(fd(raw_fd)); @@ -316,6 +321,44 @@ pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { stat_impl(path, buf) } +/// `SYS_SYMLINK` displaces whatever `link` names, and POSIX refuses a name +/// that exists: asked first, so only a name another process makes in between +/// is still displaced. +#[no_mangle] +pub unsafe extern "C" fn symlink(target: *const u8, link: *const u8) -> i32 { + let is_link = syscall::readlink(c_str_to_bytes(link), &mut [0u8; 1]).is_ok(); + if is_link || unsafe { stat_impl(link, ptr::null_mut()) } == 0 { + crate::errno::set(EEXIST); + return -1; + } + match syscall::symlink(c_str_to_bytes(target), c_str_to_bytes(link)) { + Ok(()) => 0, + Err(e) => set_errno(e), + } +} + +/// `SYS_READLINK` answers `NotFound` for a path that is no link as for one +/// that names nothing, and POSIX tells them apart: `EINVAL` for the first. +#[no_mangle] +pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> isize { + if size == 0 { + crate::errno::set(EINVAL); + return -1; + } + let target = unsafe { core::slice::from_raw_parts_mut(buf, size) }; + match syscall::readlink(c_str_to_bytes(path), target) { + Ok(n) => n as isize, + Err(syscall::SyscallError::NotFound) => { + // `stat_impl` sets `errno` for a path that names nothing. + if unsafe { stat_impl(path, ptr::null_mut()) } == 0 { + crate::errno::set(EINVAL); + } + -1 + } + Err(e) => set_errno(e) as isize, + } +} + unsafe fn stat_impl(path: *const u8, buf: *mut Stat) -> i32 { let path_bytes = c_str_to_bytes(path); // Try opening read-only @@ -349,12 +392,6 @@ pub unsafe extern "C" fn isatty(raw_fd: i32) -> i32 { } } -#[no_mangle] -pub unsafe extern "C" fn chmod(_path: *const u8, _mode: u32) -> i32 { 0 } - -#[no_mangle] -pub unsafe extern "C" fn fchmod(_fd: i32, _mode: u32) -> i32 { 0 } - static mut UMASK_VAL: u32 = 0o022; #[no_mangle] @@ -364,31 +401,21 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { old } -#[no_mangle] -pub unsafe extern "C" fn fcntl(_fd: i32, _cmd: i32, _arg: i64) -> i32 { - // Stub — return success for common operations - 0 -} +const F_GETLK: i32 = 5; +const F_SETLK: i32 = 6; +const F_SETLKW: i32 = 7; -// pread/pwrite: emulate with seek + read/write + seek back #[no_mangle] -pub unsafe extern "C" fn pread(raw_fd: i32, buf: *mut u8, count: usize, offset: i64) -> isize { - let old = lseek(raw_fd, 0, SEEK_CUR); - if old < 0 { return -1; } - if lseek(raw_fd, offset, SEEK_SET) < 0 { return -1; } - let n = read(raw_fd, buf, count); - lseek(raw_fd, old, SEEK_SET); - n -} - -#[no_mangle] -pub unsafe extern "C" fn pwrite(raw_fd: i32, buf: *const u8, count: usize, offset: i64) -> isize { - let old = lseek(raw_fd, 0, SEEK_CUR); - if old < 0 { return -1; } - if lseek(raw_fd, offset, SEEK_SET) < 0 { return -1; } - let n = write(raw_fd, buf, count); - lseek(raw_fd, old, SEEK_SET); - n +pub unsafe extern "C" fn fcntl(_fd: i32, cmd: i32, _arg: i64) -> i32 { + match cmd { + // No file keeps a record lock, so none can be taken or tested. + F_GETLK | F_SETLK | F_SETLKW => { + crate::errno::set(crate::errno::ENOSYS); + -1 + } + // issues/build/libc-fcntl-answers-0-to-commands-it-does-not-do.md + _ => 0, + } } // Directory operations @@ -407,7 +434,9 @@ pub struct dirent { pub d_name: [u8; 256], } -const DT_REG: u8 = 8; +/// `d_type`s: the listing tells a directory from everything else, and no more. +const DT_UNKNOWN: u8 = 0; +const DT_DIR: u8 = 4; #[no_mangle] pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { @@ -422,7 +451,7 @@ pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { // the reported size; the kernel bounds the listing, so it cannot run away. let mut n = match syscall::readdir(path_bytes, core::slice::from_raw_parts_mut(buf, buf_size)) { Ok(n) => n, - Err(_) => { super::memory::free(buf); return ptr::null_mut(); } + Err(e) => { super::memory::free(buf); set_errno(e); return ptr::null_mut(); } }; if n > buf_size { super::memory::free(buf); @@ -431,7 +460,8 @@ pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { if buf.is_null() { return ptr::null_mut(); } n = match syscall::readdir(path_bytes, core::slice::from_raw_parts_mut(buf, buf_size)) { Ok(n) if n <= buf_size => n, - _ => { super::memory::free(buf); return ptr::null_mut(); } + Ok(_) => { super::memory::free(buf); crate::errno::set(EAGAIN); return ptr::null_mut(); } + Err(e) => { super::memory::free(buf); set_errno(e); return ptr::null_mut(); } }; } @@ -448,24 +478,21 @@ pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { if dir.is_null() { return ptr::null_mut(); } let d = &mut *dir; - if d.pos >= d.len { return ptr::null_mut(); } - - // Entries are null-separated in the buffer let start = d.pos; - while d.pos < d.len && *d.buf.add(d.pos) != 0 { - d.pos += 1; - } - let name_len = d.pos - start; - if d.pos < d.len { d.pos += 1; } // skip null + let listing = core::slice::from_raw_parts(d.buf, d.len); + let Some(entry) = crate::listing::next(listing, &mut d.pos) else { return ptr::null_mut() }; // Use a static buffer for the dirent (not thread-safe, matching POSIX convention) static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; 256] }; let ent = &raw mut DIRENT_BUF; + if entry.name.len() >= (*ent).d_name.len() { + crate::errno::set(crate::errno::EOVERFLOW); + return ptr::null_mut(); + } (*ent).d_ino = (start + 1) as u64; - (*ent).d_type = DT_REG; // We don't have type info in readdir buffer, default to file - let copy_len = name_len.min(255); - ptr::copy_nonoverlapping(d.buf.add(start), (*ent).d_name.as_mut_ptr(), copy_len); - (*ent).d_name[copy_len] = 0; + (*ent).d_type = if entry.is_dir { DT_DIR } else { DT_UNKNOWN }; + ptr::copy_nonoverlapping(entry.name.as_ptr(), (*ent).d_name.as_mut_ptr(), entry.name.len()); + (*ent).d_name[entry.name.len()] = 0; ent } @@ -505,24 +532,44 @@ const NANOS_PER_SEC: u64 = 1_000_000_000; pub unsafe extern "C" fn mmap( addr: *mut u8, len: usize, prot: i32, flags: i32, _fd: i32, _offset: i64, ) -> *mut u8 { + use crate::memreq::{self, MapRefusal}; use toyos_abi::syscall::{MmapProt, MmapFlags}; + const MAP_FAILED: *mut u8 = usize::MAX as *mut u8; + if let Some(refusal) = memreq::mmap_refusal(addr.addr(), len, prot, flags) { + crate::errno::set(match refusal { + MapRefusal::Invalid => EINVAL, + MapRefusal::File => crate::errno::ENODEV, + // POSIX's ENOTSUP, which is EOPNOTSUPP here. + MapRefusal::Exec => crate::errno::EOPNOTSUPP, + }); + return MAP_FAILED; + } + let mut mp = MmapProt::NONE; - if prot & 1 != 0 { mp = mp | MmapProt::READ; } - if prot & 2 != 0 { mp = mp | MmapProt::WRITE; } + if prot & memreq::PROT_READ != 0 { mp = mp | MmapProt::READ; } + if prot & memreq::PROT_WRITE != 0 { mp = mp | MmapProt::WRITE; } - let mut mf = MmapFlags::PRIVATE; - if flags & 0x20 != 0 { mf = mf | MmapFlags::ANONYMOUS; } - if flags & 0x10 != 0 { mf = mf | MmapFlags::FIXED; } + let mut mf = MmapFlags::PRIVATE | MmapFlags::ANONYMOUS; + if flags & memreq::MAP_FIXED != 0 { mf = mf | MmapFlags::FIXED; } let ptr = unsafe { syscall::mmap(addr, len, mp, mf) }; if ptr.is_null() { - usize::MAX as *mut u8 // MAP_FAILED + // The kernel's refusal does not say which it is: ENOMEM is POSIX's + // for a place or size the address space does not allow. + crate::errno::set(crate::errno::ENOMEM); + MAP_FAILED } else { ptr } } +/// Advice changes no access's meaning, so taking none of it is POSIX's. +#[no_mangle] +pub unsafe extern "C" fn posix_madvise(_addr: *mut u8, _len: usize, advice: i32) -> i32 { + if crate::memreq::is_advice(advice) { 0 } else { EINVAL } +} + #[no_mangle] pub unsafe extern "C" fn munmap(addr: *mut u8, len: usize) -> i32 { // SAFETY: caller is responsible for addr/len matching a previous mmap diff --git a/userland/libc/src/refused.rs b/userland/libc/src/refused.rs new file mode 100644 index 00000000000..f52ee8a62d2 --- /dev/null +++ b/userland/libc/src/refused.rs @@ -0,0 +1,148 @@ +//! What this C library refuses. Each function sets `errno` to `ENOSYS`, which +//! POSIX spells "functionality not supported", answers failure in its own +//! POSIX form, and does nothing else. What each waits on is +//! `issues/build/libc-refuses-what-toyos-cannot-yet-answer.md`'s. + +use core::ptr; + +use crate::errno::{self, ENOSYS}; + +fn refuse() -> i32 { + errno::set(ENOSYS); + -1 +} + +/// No signal is ever raised, `SIGALRM` among them. POSIX reserves no value of +/// `alarm`'s for failure: the 0 says no alarm was pending, which is so. +#[no_mangle] +pub extern "C" fn alarm(_seconds: u32) -> u32 { + errno::set(ENOSYS); + 0 +} + +/// A process never replaces its image: a program starts as a new process. +#[no_mangle] +pub unsafe extern "C" fn execv(_path: *const u8, _argv: *const *const u8) -> i32 { + refuse() +} + +/// As [`execv`]. +#[no_mangle] +pub unsafe extern "C" fn execve(_path: *const u8, _argv: *const *const u8, _envp: *const *const u8) -> i32 { + refuse() +} + +/// ToyOS has no POSIX session. +#[no_mangle] +pub extern "C" fn setsid() -> i32 { + refuse() +} + +/// As [`setsid`]. +#[no_mangle] +pub extern "C" fn getsid(_pid: i32) -> i32 { + refuse() +} + +/// No host name is published to a process. +#[no_mangle] +pub unsafe extern "C" fn gethostname(_name: *mut u8, _len: usize) -> i32 { + refuse() +} + +/// No release, version or node name is published to a process. +#[no_mangle] +pub unsafe extern "C" fn uname(_buf: *mut u8) -> i32 { + refuse() +} + +/// No filesystem ToyOS mounts keeps a second name for a file. +#[no_mangle] +pub unsafe extern "C" fn link(_existing: *const u8, _new: *const u8) -> i32 { + refuse() +} + +/// A file has no owner. +#[no_mangle] +pub extern "C" fn fchown(_fd: i32, _owner: u32, _group: u32) -> i32 { + refuse() +} + +/// A file has no mode bits to set. +#[no_mangle] +pub unsafe extern "C" fn chmod(_path: *const u8, _mode: u32) -> i32 { + refuse() +} + +/// As [`chmod`]. +#[no_mangle] +pub extern "C" fn fchmod(_fd: i32, _mode: u32) -> i32 { + refuse() +} + +/// No call answers a filesystem's size or free space. +#[no_mangle] +pub unsafe extern "C" fn statvfs(_path: *const u8, _buf: *mut u8) -> i32 { + refuse() +} + +/// As [`statvfs`]. +#[no_mangle] +pub unsafe extern "C" fn fstatvfs(_fd: i32, _buf: *mut u8) -> i32 { + refuse() +} + +/// No call answers a process's limits. +#[no_mangle] +pub unsafe extern "C" fn getrlimit(_resource: i32, _rlp: *mut u8) -> i32 { + refuse() +} + +/// As [`getrlimit`]. +#[no_mangle] +pub unsafe extern "C" fn setrlimit(_resource: i32, _rlp: *const u8) -> i32 { + refuse() +} + +/// ToyOS keeps no user database: a user is the part of the tree a session was +/// handed. POSIX's `_r` lookups answer their error, `result` null. +#[no_mangle] +pub unsafe extern "C" fn getpwnam_r( + _name: *const u8, + _pwd: *mut u8, + _buf: *mut u8, + _buflen: usize, + result: *mut *mut u8, +) -> i32 { + unsafe { *result = ptr::null_mut() }; + ENOSYS +} + +/// As [`getpwnam_r`]. +#[no_mangle] +pub unsafe extern "C" fn getpwuid_r(_uid: u32, _pwd: *mut u8, _buf: *mut u8, _buflen: usize, result: *mut *mut u8) -> i32 { + unsafe { *result = ptr::null_mut() }; + ENOSYS +} + +/// No call answers which pages of a range are mapped, and no mapping is of a +/// file (`mmap`). +#[no_mangle] +pub unsafe extern "C" fn msync(_addr: *mut u8, _len: usize, _flags: i32) -> i32 { + refuse() +} + +/// A mapping's protection is fixed when it is made. +#[no_mangle] +pub unsafe extern "C" fn mprotect(_addr: *mut u8, _len: usize, _prot: i32) -> i32 { + refuse() +} + +/// The kernel resolves a path by its own rules, not POSIX's (`..` read off +/// the text, only the last name's link followed, a relative link read +/// against its mount), and no call answers where one leads. +#[no_mangle] +pub unsafe extern "C" fn realpath(_path: *const u8, _resolved: *mut u8) -> *mut u8 { + errno::set(ENOSYS); + ptr::null_mut() +} diff --git a/userland/libc/src/stdio.rs b/userland/libc/src/stdio.rs index f2c692e7f6e..b26874f1dc2 100644 --- a/userland/libc/src/stdio.rs +++ b/userland/libc/src/stdio.rs @@ -610,7 +610,7 @@ pub unsafe extern "C" fn mkdir(path: *const u8, _mode: u32) -> i32 { } #[no_mangle] -pub unsafe extern "C" fn __assert_fail(expr: *const u8, file: *const u8, _line: i32) { +pub unsafe extern "C" fn __assert_fail(expr: *const u8, file: *const u8, _line: i32, _func: *const u8) { fputs(b"assertion failed: \0".as_ptr(), unsafe { stderr }); fputs(expr, unsafe { stderr }); fputs(b" at \0".as_ptr(), unsafe { stderr }); diff --git a/userland/libc/src/string.rs b/userland/libc/src/string.rs index 4d204d64327..8b024d33446 100644 --- a/userland/libc/src/string.rs +++ b/userland/libc/src/string.rs @@ -12,6 +12,17 @@ pub unsafe extern "C" fn strlen(s: *const u8) -> usize { len } +#[no_mangle] +pub unsafe extern "C" fn strnlen(s: *const u8, max: usize) -> usize { + unsafe { crate::text::strnlen(s, max) } +} + +/// The text is static, so no call overwrites another's. +#[no_mangle] +pub extern "C" fn strsignal(sig: i32) -> *const u8 { + crate::text::signal_text(sig).as_ptr() +} + #[no_mangle] pub unsafe extern "C" fn strcpy(dst: *mut u8, src: *const u8) -> *mut u8 { let mut i = 0; diff --git a/userland/libc/src/text.rs b/userland/libc/src/text.rs new file mode 100644 index 00000000000..c7a7d593b8a --- /dev/null +++ b/userland/libc/src/text.rs @@ -0,0 +1,42 @@ +//! C strings and texts: `strnlen`'s scan and `strsignal`'s descriptions. It +//! reads and sets nothing but what it is handed, so the host tests it +//! (`toyos-libc-copies`). + +/// The length of the string at `s`, never reading `s + max` or past it. +/// +/// # Safety +/// `s` is readable up to its first NUL or for `max` bytes, whichever is first. +pub(crate) unsafe fn strnlen(s: *const u8, max: usize) -> usize { + let mut n = 0; + while n < max && unsafe { *s.add(n) } != 0 { + n += 1; + } + n +} + +/// `strsignal`'s text for `sig`, NUL-terminated: glibc's words for each +/// signal `include/signal.h` numbers, and one text for every other number, +/// which POSIX leaves to the implementation. +pub(crate) fn signal_text(sig: i32) -> &'static [u8] { + match sig { + 1 => b"Hangup\0", + 2 => b"Interrupt\0", + 3 => b"Quit\0", + 4 => b"Illegal instruction\0", + 5 => b"Trace/breakpoint trap\0", + 6 => b"Aborted\0", + 7 => b"Bus error\0", + 8 => b"Floating point exception\0", + 9 => b"Killed\0", + 10 => b"User defined signal 1\0", + 11 => b"Segmentation fault\0", + 12 => b"User defined signal 2\0", + 13 => b"Broken pipe\0", + 14 => b"Alarm clock\0", + 15 => b"Terminated\0", + 17 => b"Child exited\0", + 18 => b"Continued\0", + 19 => b"Stopped (signal)\0", + _ => b"Unknown signal\0", + } +} From 8db5348402efd0f635203aafa0f6ab559c61dedc Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:44:21 +0200 Subject: [PATCH 03/12] libc: the signal mask kept and read back, fcntl's commands answered, alarm left to the signals Each libc case reads its effect back, as #644's review asks, and alarm and fcntl follow the issues that review had #644 file. - pthread_sigmask keeps the calling thread's mask in a thread-local, changed as POSIX says for SIG_BLOCK, SIG_UNBLOCK and SIG_SETMASK, SIGKILL and SIGSTOP dropped without an error, any other how refused EINVAL, and answered in the old set; a thread pthread_create starts takes its creator's. sigprocmask, which answered 0 and kept and filled nothing, is that mask with POSIX's -1 and errno. No signal is ever raised, so a blocked one is never held back. The arithmetic is sigmask.rs, host-tested; 208_signal_mask reads every change back through the next call and through a new thread. - alarm is not defined: POSIX gives it no failure to answer, and the SIGALRM it arms reaches a handler only through the signals stage 3 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md imitates. An ENOSYS alarm armed nothing and said so where no caller looks. - fcntl: a record lock is refused EINVAL, POSIX's answer for a file that supports no locking, not ENOSYS; F_DUPFD, F_DUPFD_CLOEXEC, F_GETFL, F_SETFL, F_GETOWN and F_SETOWN, which answered 0 and did nothing, ENOSYS; a command POSIX does not define EINVAL. F_GETFD and F_SETFD still answer 0: close-on-exec is stage 3's descriptor table. - 206_libc_refusals reads back that nothing refused was done: the file's mode, its one byte and no second name, the page still writable, the limit as it was. - 207_libc_names lists a directory of its own: a listing of /tmp shows no link (issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md). Its hex values spell 0x out, printf ignoring # (filed). - 114_bound_signal now stops at semaphore.h, which is gone. Issues: the refusals and what each waits on; close of a socket ending the process; stat's one serial number for every file; /tmp's links; printf's #. The header-drift issue closes: its exit is prototypes.rs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...libc-close-of-a-socket-ends-the-process.md | 20 ++++++ ...-by-hand-and-drift-from-its-definitions.md | 34 --------- ...-printf-ignores-the-alternate-form-flag.md | 15 ++++ ...bc-refuses-what-toyos-cannot-yet-answer.md | 45 ++++++++++++ ...nswers-one-serial-number-for-every-file.md | 19 +++++ ...mp-displaces-its-name-and-lists-nowhere.md | 19 +++++ tests/testcases/tinycc/206_libc_refusals.c | 25 +++++-- .../testcases/tinycc/206_libc_refusals.expect | 15 ++-- tests/testcases/tinycc/207_libc_names.c | 36 ++++++---- tests/testcases/tinycc/207_libc_names.expect | 21 ++++++ tests/testcases/tinycc/208_signal_mask.c | 72 +++++++++++++++++++ tests/testcases/tinycc/208_signal_mask.expect | 10 +++ tests/toyos.rs | 4 +- toyos-libc-copies/src/lib.rs | 12 +++- toyos-libc-copies/src/signal_masks.rs | 27 +++++++ userland/libc/include/fcntl.h | 3 + userland/libc/include/signal.h | 1 + userland/libc/include/unistd.h | 1 - userland/libc/src/lib.rs | 1 + userland/libc/src/misc.rs | 16 +++-- userland/libc/src/posix_io.rs | 30 +++++--- userland/libc/src/pthread.rs | 26 +++++++ userland/libc/src/refused.rs | 8 --- userland/libc/src/sigmask.rs | 27 +++++++ 24 files changed, 400 insertions(+), 87 deletions(-) create mode 100644 issues/build/libc-close-of-a-socket-ends-the-process.md delete mode 100644 issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md create mode 100644 issues/build/libc-printf-ignores-the-alternate-form-flag.md create mode 100644 issues/build/libc-refuses-what-toyos-cannot-yet-answer.md create mode 100644 issues/build/libc-stat-answers-one-serial-number-for-every-file.md create mode 100644 issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md create mode 100644 tests/testcases/tinycc/207_libc_names.expect create mode 100644 tests/testcases/tinycc/208_signal_mask.c create mode 100644 tests/testcases/tinycc/208_signal_mask.expect create mode 100644 toyos-libc-copies/src/signal_masks.rs create mode 100644 userland/libc/src/sigmask.rs diff --git a/issues/build/libc-close-of-a-socket-ends-the-process.md b/issues/build/libc-close-of-a-socket-ends-the-process.md new file mode 100644 index 00000000000..5e901fce70e --- /dev/null +++ b/issues/build/libc-close-of-a-socket-ends-the-process.md @@ -0,0 +1,20 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc's close of a socket ends the process + +Read from the code, not run. A socket's descriptor is `1024` plus its index in +libc's socket table (`userland/libc/src/socket.rs`, `SOCKET_FD_BASE`), and no +handle. `close` (`userland/libc/src/posix_io.rs`) passes every descriptor to +`SYS_CLOSE`, and the kernel answers a handle its caller does not hold by +ending the caller (`kernel/src/syscall/handles.rs`, `sys_close`, through +`HandleError::refuse`). `close_socket`, which closes the socket's pipes and +tells netd, is exported under `no_mangle` and called by nothing; the header +gate excuses it by this file (`toyos-libc-copies/src/prototypes.rs`). + +**Exit**: `close` of a socket's descriptor releases the socket and its pipes +and answers 0, `close_socket` is no export, and a guest C case closes a +connected socket and goes on. diff --git a/issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md b/issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md deleted file mode 100644 index 6e6a255611a..00000000000 --- a/issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# libc's C headers are written by hand, and nothing holds them to its definitions - -`userland/libc/include/` is headers typed beside the Rust that defines what -they declare, and no build or test compares the two. clang, compiling -doomgeneric for the first time, found two places they had already parted: - -- `atof` was defined (`userland/libc/src/stdio.rs`) and declared nowhere, so - `m_config.c`'s `(float) atof(value)` called it through C89's implicit `int` - declaration — the value read out of `rax` for a function that returns in - `xmm0`. toyos-cc compiled it that way without a word; clang refuses it. -- `usleep` was declared twice, `unsigned int` in `time.h` and `unsigned long` in - `unistd.h`, against a definition taking a `u32`. toyos-cc took both. - -Both are fixed in the headers. The class is not: a signature changed in -`userland/libc/src` changes no header, and the C sysroot -ships whatever the headers say. The corpus shows the -surface is also incomplete — `pthread.h` and -`signal.h` stop short of `PTHREAD_PROCESS_SHARED` and `SIGUSR1`. - -**Generating them with cbindgen was priced and not taken**: it is a new -dependency, and the headers are mostly what cbindgen cannot emit — macros, -constants, `FILE`, `va_list` plumbing — with the function prototypes the part -that drifts. - -**Exit**: every function prototype in `userland/libc/include/` is checked -against the symbol and signature libc defines, by a test that reds on a -prototype with no definition, a definition a header was meant to declare and -does not, or two declarations of one name that disagree. diff --git a/issues/build/libc-printf-ignores-the-alternate-form-flag.md b/issues/build/libc-printf-ignores-the-alternate-form-flag.md new file mode 100644 index 00000000000..3de881552e1 --- /dev/null +++ b/issues/build/libc-printf-ignores-the-alternate-form-flag.md @@ -0,0 +1,15 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc's printf ignores the alternate-form flag + +`printf`'s format reader (`userland/libc/src/printf.rs`) steps over `#` and +keeps nothing of it, so `%#x` prints `200` where C has `0x200`, `%#o` no +leading 0, and `%#g` drops the trailing zeros C keeps. The corpus cases that +print a hex value spell the `0x` out for it. + +**Exit**: `#` takes C's alternate form for every conversion that has one, and +a corpus case prints each. diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md new file mode 100644 index 00000000000..5f9c93159db --- /dev/null +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -0,0 +1,45 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc refuses what ToyOS cannot yet answer + +These answer failure in their POSIX form and do nothing +(`userland/libc/src/refused.rs`, `posix_io.rs`, `memory.rs`), each asserted by +`tests/testcases/tinycc/206_libc_refusals.c`. A program that calls one and +carries on without it runs; one that needs it does not. Each waits on what it +names: + +- `link`, `ENOSYS`: no call gives a file a second name. +- `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set. +- `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or + free space. +- `getrlimit` and `setrlimit`, `ENOSYS`: no call answers a process's limits. + LLVM's `getDefaultStackSize` (`llvm/lib/Support/ProgramStack.cpp`) ignores + the failure and reads the `rlimit` it did not get. +- `gethostname` and `uname`, `ENOSYS`: no host name, release or version is + published to a process. +- `realpath`, `ENOSYS`: the kernel resolves a path by rules of its own (`..` + read off the text, only the last name's link followed, a relative link read + against its mount), and no call answers where a path leads. +- `msync` and `mprotect`, `ENOSYS`: no call answers which pages of a range are + mapped, and a mapping's protection is fixed when it is made. +- `mmap` of a file, `ENODEV`, and of executable memory, `ENOTSUP`: the kernel + maps neither. +- `fcntl`: a record lock, `EINVAL`, POSIX's answer for a file that supports no + locking; `F_DUPFD`, `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and + `F_SETOWN`, `ENOSYS`. `F_GETFD` and `F_SETFD` answer 0 and keep nothing: + close-on-exec is the descriptor table of stage 3 of + `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`. +- `aligned_alloc` beside std, above 16, `EINVAL`: std's `malloc` aligns to 16 + and its `free` knows no other alignment. + +Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no +call replacing a process's image; `setsid` and `getsid`, no POSIX session; +`fchown`, no owner; `getpwnam_r` and `getpwuid_r`, no user database. + +**Exit**: each above either does what POSIX says, asserted by a guest C case +that reads its effect back, or is ruled out by the owner and moved to the list +above. diff --git a/issues/build/libc-stat-answers-one-serial-number-for-every-file.md b/issues/build/libc-stat-answers-one-serial-number-for-every-file.md new file mode 100644 index 00000000000..472f378383e --- /dev/null +++ b/issues/build/libc-stat-answers-one-serial-number-for-every-file.md @@ -0,0 +1,19 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc's stat answers one serial number for every file + +`stat`, `fstat` and `lstat` (`userland/libc/src/posix_io.rs`) zero the whole +`struct stat` and fill in the size, the mtime and the mode, so `st_dev` and +`st_ino` are 0 for every file. POSIX names a file by that pair, so every file +is one file to a program that asks: LLVM's `sys::fs::equivalent` and +`UniqueID` (`llvm/lib/Support/Unix/Path.inc`) answer any two paths the same, +and clang's `FileManager` keys every file it opens on the pair. `readdir`'s +`d_ino` is the entry's offset in the listing, which no `st_ino` equals. + +**Exit**: `st_dev` and `st_ino` tell two files apart and name one file the +same way through two paths, `d_ino` is its entry's `st_ino`, and a guest C case +reads them for two files and a link. diff --git a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md new file mode 100644 index 00000000000..d66943c4023 --- /dev/null +++ b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -0,0 +1,19 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# A symbolic link on /tmp displaces its name and lists nowhere + +Read from the code, not run. The kernel's `/tmp` (`kernel/src/tmpfs.rs`) +makes a link over whatever file its name held, where POSIX's `symlink` refuses +a name that exists `EEXIST`; libc asks first (`userland/libc/src/posix_io.rs`, +`symlink`), so a file another process makes between the question and the call +is displaced. Its `list` answers files alone, so no `readdir` of a directory +shows a link it holds, and +`tests/testcases/tinycc/207_libc_names.c` lists a directory no link is in. + +**Exit**: `SYS_SYMLINK` over a name that exists is refused `AlreadyExists`, +and a listing of `/tmp` names each link it holds, each asserted by a guest +case. diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 5cb26101f39..dd3aff700de 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -1,6 +1,6 @@ /* What libc refuses: each call answers failure in its own POSIX form, errno - says why, and nothing is done. ENOSYS where ToyOS lacks the function; mmap's - own errno where it refuses a mapping. */ + says why, and nothing is done: ENOSYS where ToyOS lacks the function, and + the errno POSIX names for a lock or a mapping it cannot take. */ #include #include #include @@ -52,9 +52,14 @@ int main(void) { printf("could not map a page\n"); return 1; } + struct stat st; + if (fstat(fd, &st) != 0) { + printf("could not stat " FILE_PATH "\n"); + return 1; + } + mode_t mode = st.st_mode; errno = 0; - said("alarm", alarm(1)); said("execv", execv("/system/bin/shell", argv)); said("execve", execve("/system/bin/shell", argv, envp)); said("setsid", setsid()); @@ -75,6 +80,10 @@ int main(void) { said("fcntl F_SETLK", fcntl(fd, F_SETLK, &lock)); said("fcntl F_SETLKW", fcntl(fd, F_SETLKW, &lock)); said("fcntl F_GETLK", fcntl(fd, F_GETLK, &lock)); + said("fcntl F_DUPFD", fcntl(fd, F_DUPFD, 10)); + said("fcntl F_GETFL", fcntl(fd, F_GETFL)); + said("fcntl F_SETFL", fcntl(fd, F_SETFL, O_NONBLOCK)); + said("fcntl 12345", fcntl(fd, 12345)); /* The _r lookups answer their error, and null for the entry. */ int answer = getpwnam_r("root", &pw, buf, sizeof buf, &found); @@ -99,10 +108,14 @@ int main(void) { printf("posix_madvise WILLNEED: %d\n", posix_madvise(page, 4096, POSIX_MADV_WILLNEED)); printf("posix_madvise 99: %s\n", errno_name(posix_madvise(page, 4096, 99))); - /* The file is as it was: its one byte, and no second name. */ - struct stat st; - printf("file: %ld bytes; second name: %s\n", fstat(fd, &st) == 0 ? (long)st.st_size : -1L, + /* Nothing refused was done: the file has its one byte, its mode and no + second name; the page is still writable; the limit is as it was. */ + long size = fstat(fd, &st) == 0 ? (long)st.st_size : -1L; + printf("file: %ld bytes; mode %s; second name: %s\n", size, st.st_mode == mode ? "as it was" : "changed", access(FILE_PATH ".link", F_OK) == 0 ? "made" : "none"); + page[0] = 'y'; + printf("page after mprotect: %c\n", page[0]); + printf("limit after getrlimit: %lu %lu\n", (unsigned long)limit.rlim_cur, (unsigned long)limit.rlim_max); close(fd); unlink(FILE_PATH); return 0; diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 3ad23e2d17d..4d08b24bf7e 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -1,4 +1,3 @@ -alarm: 0, ENOSYS execv: -1, ENOSYS execve: -1, ENOSYS setsid: -1, ENOSYS @@ -16,9 +15,13 @@ setrlimit: -1, ENOSYS msync: -1, ENOSYS mprotect: -1, ENOSYS realpath: -1, ENOSYS -fcntl F_SETLK: -1, ENOSYS -fcntl F_SETLKW: -1, ENOSYS -fcntl F_GETLK: -1, ENOSYS +fcntl F_SETLK: -1, EINVAL +fcntl F_SETLKW: -1, EINVAL +fcntl F_GETLK: -1, EINVAL +fcntl F_DUPFD: -1, ENOSYS +fcntl F_GETFL: -1, ENOSYS +fcntl F_SETFL: -1, ENOSYS +fcntl 12345: -1, EINVAL getpwnam_r: ENOSYS, entry null getpwuid_r: ENOSYS, entry null mmap a file: -1, ENODEV @@ -30,4 +33,6 @@ sysconf _SC_GETPW_R_SIZE_MAX: -1, no errno sysconf 12345: -1, EINVAL posix_madvise WILLNEED: 0 posix_madvise 99: EINVAL -file: 1 bytes; second name: none +file: 1 bytes; mode as it was; second name: none +page after mprotect: y +limit after getrlimit: 0 0 diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index d790fb1d66c..082886d6158 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -1,7 +1,8 @@ /* What libc does for the names LLVM builds against: creat, symlink and - readlink as POSIX has them; a directory listed with the name and kind of - each entry; dladdr naming the image and the exported symbol an address lies - in; and strnlen, strsignal, modf, logb and the conversions. */ + readlink as POSIX has them, each read back; a directory listed with the name + and kind of each entry; dladdr naming the image and the exported symbol an + address lies in; and strnlen, strsignal, modf, logb and the + conversions. */ #include #include #include @@ -19,6 +20,7 @@ #define DIR_PATH "/tmp/207_libc_names" #define TARGET DIR_PATH "/target" #define LINK DIR_PATH "/link" +#define LISTED DIR_PATH "/listed" /* The image's test library, which exports tls_get_label. */ #define LIB "/system/lib/libtls_lib.so" @@ -33,6 +35,11 @@ static const char *errno_name(int e) { } } +/* A call that answers -1 and sets errno: what it answered, and errno. */ +static void refused(const char *what, long answer) { + printf("%s: %ld, %s\n", what, answer, errno_name(errno)); +} + static int by_name(const void *a, const void *b) { return strcmp(*(char *const *)a, *(char *const *)b); } @@ -45,17 +52,17 @@ static void links(void) { return; } printf("symlink: %d\n", symlink(TARGET, LINK)); - printf("symlink over the link: %d, %s\n", symlink(TARGET, LINK), errno_name(errno)); - printf("symlink over a file: %d, %s\n", symlink(LINK, TARGET), errno_name(errno)); + refused("symlink over the link", symlink(TARGET, LINK)); + refused("symlink over a file", symlink(LINK, TARGET)); ssize_t n = readlink(LINK, buf, sizeof buf); printf("readlink: %zd, \"%.*s\"\n", n, n > 0 ? (int)n : 0, buf); memset(buf, '#', sizeof buf); n = readlink(LINK, buf, 4); printf("readlink into 4: %zd, \"%.5s\"\n", n, buf); - printf("readlink of a file: %zd, %s\n", readlink(TARGET, buf, sizeof buf), errno_name(errno)); - printf("readlink of nothing: %zd, %s\n", readlink(DIR_PATH "/none", buf, sizeof buf), errno_name(errno)); - printf("readlink into 0: %zd, %s\n", readlink(LINK, buf, 0), errno_name(errno)); + refused("readlink of a file", readlink(TARGET, buf, sizeof buf)); + refused("readlink of nothing", readlink(DIR_PATH "/none", buf, sizeof buf)); + refused("readlink into 0", readlink(LINK, buf, 0)); fd = open(LINK, O_RDONLY); n = fd < 0 ? -1 : read(fd, buf, sizeof buf); @@ -65,13 +72,14 @@ static void links(void) { } static void listing(void) { - if (mkdir(DIR_PATH "/sub", 0755) != 0 || close(creat(DIR_PATH "/sub/inner", 0644)) != 0) { - printf("could not make %s/sub\n", DIR_PATH); + if (mkdir(LISTED, 0755) != 0 || mkdir(LISTED "/sub", 0755) != 0 || close(creat(LISTED "/a", 0644)) != 0 + || close(creat(LISTED "/b", 0644)) != 0 || close(creat(LISTED "/sub/inner", 0644)) != 0) { + printf("could not make %s\n", LISTED); return; } - DIR *dir = opendir(DIR_PATH); + DIR *dir = opendir(LISTED); if (!dir) { - printf("opendir %s: %s\n", DIR_PATH, errno_name(errno)); + refused("opendir", -1); return; } char *names[16]; @@ -88,7 +96,7 @@ static void listing(void) { for (int i = 0; i < count; i++) printf("entry: %s\n", names[i]); errno = 0; - printf("opendir of nothing: %s, %s\n", opendir(DIR_PATH "/none") ? "a stream" : "null", errno_name(errno)); + refused("opendir of nothing", opendir(LISTED "/none") == NULL ? -1 : 0); } static int first_image(struct dl_phdr_info *info, size_t size, void *data) { @@ -141,7 +149,7 @@ int main(void) { double part = modf(-3.25, &whole); printf("modf(-3.25): %g and %g\n", whole, part); printf("logb: %g %g %g\n", logb(0.1), logb(1024.0), logb(0.0)); - printf("endian: %s %#x %#x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", + printf("endian: %s 0x%x 0x%x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", (unsigned)htobe32(0x01020304), (unsigned)le16toh(0x0102)); return 0; } diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect new file mode 100644 index 00000000000..5185175e833 --- /dev/null +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -0,0 +1,21 @@ +symlink: 0 +symlink over the link: -1, EEXIST +symlink over a file: -1, EEXIST +readlink: 26, "/tmp/207_libc_names/target" +readlink into 4: 4, "/tmp#" +readlink of a file: -1, EINVAL +readlink of nothing: -1, ENOENT +readlink into 0: -1, EINVAL +read through the link: "hello" +entry: a DT_UNKNOWN +entry: b DT_UNKNOWN +entry: sub DT_DIR +opendir of nothing: -1, ENOENT +dladdr(main): 1; names this program: yes; starts where dl_iterate_phdr says: yes; symbol: none +dladdr(a stack address): 0 +dladdr(tls_get_label + 1): 1, /system/lib/libtls_lib.so, tls_get_label at the symbol's address: yes +strnlen: 3 2 0 +strsignal: Segmentation fault; Unknown signal +modf(-3.25): -3 and -0.25 +logb: -4 10 -inf +endian: little 0x4030201 0x102 diff --git a/tests/testcases/tinycc/208_signal_mask.c b/tests/testcases/tinycc/208_signal_mask.c new file mode 100644 index 00000000000..36088c3ebcd --- /dev/null +++ b/tests/testcases/tinycc/208_signal_mask.c @@ -0,0 +1,72 @@ +/* A thread's signal mask, as pthread_sigmask and sigprocmask keep it: each + change read back by the next call, SIGKILL and SIGSTOP never in it, a how + that is none of POSIX's three refused, and a new thread starting with its + creator's mask. Signal n is bit n - 1 of a sigset_t. */ +#include +#include +#include +#include + +#define BIT(n) (1UL << ((n) - 1)) + +static sigset_t current(void) { + sigset_t now = 0xdead; + pthread_sigmask(SIG_BLOCK, NULL, &now); + return now; +} + +static void *child(void *arg) { + *(sigset_t *)arg = current(); + return NULL; +} + +/* One change: what the call answered, the mask it said was there, and the + mask the next call reads. */ +static void changed(const char *what, int answer, sigset_t old) { + sigset_t now = current(); + printf("%s: %d, old 0x%lx, now 0x%lx\n", what, answer, old, now); +} + +int main(void) { + sigset_t usr1 = BIT(SIGUSR1), usr2 = BIT(SIGUSR2), all = ~0UL, none = 0, old; + int answer; + + printf("at start: 0x%lx\n", current()); + old = 0xdead; + answer = pthread_sigmask(SIG_BLOCK, &usr1, &old); + changed("block USR1", answer, old); + old = 0xdead; + answer = sigprocmask(SIG_BLOCK, &usr2, &old); + changed("block USR2", answer, old); + old = 0xdead; + answer = pthread_sigmask(SIG_UNBLOCK, &usr1, &old); + changed("unblock USR1", answer, old); + + sigset_t seen = 0xdead; + pthread_t thread; + if (pthread_create(&thread, NULL, child, &seen) != 0 || pthread_join(thread, NULL) != 0) { + printf("could not run a thread\n"); + return 1; + } + printf("a new thread starts with: 0x%lx\n", seen); + + pthread_sigmask(SIG_SETMASK, &all, NULL); + sigset_t everything = current(); + printf("set all: KILL %s, STOP %s, the rest %s\n", everything & BIT(SIGKILL) ? "held" : "dropped", + everything & BIT(SIGSTOP) ? "held" : "dropped", + (everything | BIT(SIGKILL) | BIT(SIGSTOP)) == all ? "held" : "not all held"); + + answer = pthread_sigmask(99, &usr1, NULL); + printf("how 99: %s, the mask %s\n", answer == EINVAL ? "EINVAL" : "taken", + current() == everything ? "as it was" : "changed"); + errno = 0; + answer = sigprocmask(99, &usr1, NULL); + printf("sigprocmask how 99: %d, %s\n", answer, errno == EINVAL ? "EINVAL" : "another errno"); + old = 0xdead; + answer = pthread_sigmask(99, NULL, &old); + printf("how 99 with no set: %d, old %s\n", answer, old == everything ? "the mask" : "another"); + old = 0xdead; + answer = sigprocmask(SIG_SETMASK, &none, &old); + changed("set none", answer, old == everything ? 1 : 0); + return 0; +} diff --git a/tests/testcases/tinycc/208_signal_mask.expect b/tests/testcases/tinycc/208_signal_mask.expect new file mode 100644 index 00000000000..91ae47292ee --- /dev/null +++ b/tests/testcases/tinycc/208_signal_mask.expect @@ -0,0 +1,10 @@ +at start: 0x0 +block USR1: 0, old 0x0, now 0x200 +block USR2: 0, old 0x200, now 0xa00 +unblock USR1: 0, old 0xa00, now 0x800 +a new thread starts with: 0x800 +set all: KILL dropped, STOP dropped, the rest held +how 99: EINVAL, the mask as it was +sigprocmask how 99: -1, EINVAL +how 99 with no set: 0, old the mask +set none: 0, old 0x1, now 0x0 diff --git a/tests/toyos.rs b/tests/toyos.rs index 52ce975d3e7..32b26369f34 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2562,8 +2562,8 @@ const NOT_RUN: &[NotRun] = &[ }, NotRun { case: "114_bound_signal", - stage: Stage::Refused("use of undeclared identifier 'SIGUSR1'"), - why: Why::Declined("sigaction, sigjmp_buf and the signal numbers, which no header here declares"), + stage: Stage::Refused("'semaphore.h' file not found"), + why: Why::Declined("semaphores, sigjmp_buf and a signal's handler run, none of which libc has"), }, NotRun { case: "115_bound_setjmp", diff --git a/toyos-libc-copies/src/lib.rs b/toyos-libc-copies/src/lib.rs index edef84f207c..669b26f59e6 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -7,9 +7,10 @@ //! `core::str::from_utf8`, the number reader against the host C library's, //! AArch64's `long double` widening against compiler-builtins', and the errno //! codes against `include/errno.h`. `strnlen`, `modf` and `logb` against the -//! host C library's, `strsignal`'s texts, the readdir answer's reader, -//! `dladdr`'s symbol search on an image laid out here, and what the memory -//! calls refuse. And libc's headers against its definitions (`prototypes`). +//! host C library's, `strsignal`'s texts, a signal mask's changes, the readdir +//! answer's reader, `dladdr`'s symbol search on an image laid out here, and +//! what the memory calls refuse. And libc's headers against its definitions +//! (`prototypes`). #[cfg(test)] extern crate alloc; @@ -30,6 +31,9 @@ mod listing; #[path = "../../userland/libc/src/memreq.rs"] mod memreq; #[cfg(test)] +#[path = "../../userland/libc/src/sigmask.rs"] +mod sigmask; +#[cfg(test)] #[path = "../../userland/libc/src/strtonum.rs"] mod strtonum; #[cfg(test)] @@ -54,6 +58,8 @@ mod memory_refusals; #[cfg(test)] mod prototypes; #[cfg(test)] +mod signal_masks; +#[cfg(test)] mod strtonum_differential; #[cfg(test)] mod text_differential; diff --git a/toyos-libc-copies/src/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs new file mode 100644 index 00000000000..785ae46b0c5 --- /dev/null +++ b/toyos-libc-copies/src/signal_masks.rs @@ -0,0 +1,27 @@ +//! A signal mask as `pthread_sigmask` changes it, against POSIX's words for +//! each `how`: `SIG_BLOCK` the union, `SIG_UNBLOCK` the intersection with the +//! complement, `SIG_SETMASK` the set itself; `SIGKILL` and `SIGSTOP` never in +//! it; any other `how` refused. + +use crate::sigmask::{self, SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; + +/// Signal `n`'s bit. +fn bit(n: u32) -> u64 { + 1 << (n - 1) +} + +#[test] +fn each_how_changes_the_mask_as_posix_says() { + let (hup, int, usr1) = (bit(1), bit(2), bit(10)); + let (kill, stop) = (bit(9), bit(19)); + assert_eq!(sigmask::changed(hup, SIG_BLOCK, usr1), Some(hup | usr1)); + assert_eq!(sigmask::changed(hup | usr1, SIG_UNBLOCK, usr1 | int), Some(hup)); + assert_eq!(sigmask::changed(hup, SIG_SETMASK, int), Some(int)); + assert_eq!(sigmask::changed(hup, SIG_SETMASK, 0), Some(0)); + // Asked for, and dropped without an error. + assert_eq!(sigmask::changed(0, SIG_BLOCK, kill | stop | usr1), Some(usr1)); + assert_eq!(sigmask::changed(0, SIG_SETMASK, u64::MAX), Some(u64::MAX & !(kill | stop))); + for how in [-1, 3, 99] { + assert_eq!(sigmask::changed(hup, how, usr1), None, "how {how}"); + } +} diff --git a/userland/libc/include/fcntl.h b/userland/libc/include/fcntl.h index 60143712389..e72e33fad25 100644 --- a/userland/libc/include/fcntl.h +++ b/userland/libc/include/fcntl.h @@ -25,6 +25,9 @@ extern "C" { #define F_GETLK 5 #define F_SETLK 6 #define F_SETLKW 7 +#define F_SETOWN 8 +#define F_GETOWN 9 +#define F_DUPFD_CLOEXEC 1030 #define FD_CLOEXEC 1 #define F_RDLCK 0 diff --git a/userland/libc/include/signal.h b/userland/libc/include/signal.h index ce46709027c..4bb62cc793e 100644 --- a/userland/libc/include/signal.h +++ b/userland/libc/include/signal.h @@ -64,6 +64,7 @@ struct sigaction { int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); +int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); int kill(int pid, int sig); #define SIG_BLOCK 0 diff --git a/userland/libc/include/unistd.h b/userland/libc/include/unistd.h index e05afaa0aa6..c93aa56e297 100644 --- a/userland/libc/include/unistd.h +++ b/userland/libc/include/unistd.h @@ -38,7 +38,6 @@ ssize_t readlink(const char *path, char *buf, size_t size); int symlink(const char *target, const char *linkpath); int link(const char *existing, const char *newpath); int fchown(int fd, uid_t owner, gid_t group); -unsigned int alarm(unsigned int seconds); int gethostname(char *name, size_t len); pid_t getpid(void); diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 01799d54da9..66003b556f7 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -20,6 +20,7 @@ mod posix_io; mod printf; mod pthread; mod refused; +mod sigmask; mod socket; mod stdio; mod string; diff --git a/userland/libc/src/misc.rs b/userland/libc/src/misc.rs index 6531928d101..c49a3c12f01 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -211,11 +211,17 @@ pub unsafe extern "C" fn sigaction( 0 // success } -#[no_mangle] -pub unsafe extern "C" fn sigprocmask( - _how: i32, _set: *const u64, _oldset: *mut u64, -) -> i32 { - 0 +/// The calling thread's mask, which is what POSIX's process mask is in a +/// process of one thread (`pthread_sigmask`). +#[no_mangle] +pub unsafe extern "C" fn sigprocmask(how: i32, set: *const u64, oldset: *mut u64) -> i32 { + match unsafe { crate::pthread::pthread_sigmask(how, set, oldset) } { + 0 => 0, + refused => { + crate::errno::set(refused); + -1 + } + } } #[no_mangle] diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index 0f362f671d3..de129faecc8 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -392,21 +392,33 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { old } +const F_DUPFD: i32 = 0; +const F_GETFD: i32 = 1; +const F_SETFD: i32 = 2; +const F_GETFL: i32 = 3; +const F_SETFL: i32 = 4; const F_GETLK: i32 = 5; const F_SETLK: i32 = 6; const F_SETLKW: i32 = 7; +const F_SETOWN: i32 = 8; +const F_GETOWN: i32 = 9; +const F_DUPFD_CLOEXEC: i32 = 1030; #[no_mangle] pub unsafe extern "C" fn fcntl(_fd: i32, cmd: i32, _arg: i64) -> i32 { - match cmd { - // No file keeps a record lock, so none can be taken or tested. - F_GETLK | F_SETLK | F_SETLKW => { - crate::errno::set(crate::errno::ENOSYS); - -1 - } - // issues/build/libc-fcntl-answers-0-to-commands-it-does-not-do.md - _ => 0, - } + let refused = match cmd { + // Close-on-exec is the descriptor table's of stage 3 of + // issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md. + F_GETFD | F_SETFD => return 0, + // POSIX's answer for a file that supports no locking, which none here does. + F_GETLK | F_SETLK | F_SETLKW => EINVAL, + // A descriptor's lowest free number, its status flags and its owner are + // nothing this library knows. + F_DUPFD | F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => crate::errno::ENOSYS, + _ => EINVAL, + }; + crate::errno::set(refused); + -1 } // Directory operations diff --git a/userland/libc/src/pthread.rs b/userland/libc/src/pthread.rs index 33e45d82c96..740132809e8 100644 --- a/userland/libc/src/pthread.rs +++ b/userland/libc/src/pthread.rs @@ -47,6 +47,8 @@ struct Thread { start: StartRoutine, arg: *mut u8, result: AtomicPtr, + /// Its creator's signal mask, which POSIX has the thread start with. + mask: u64, } /// The bit a `pthread_t` of a thread this library did not start carries. @@ -62,6 +64,10 @@ static SELF: Cell<*mut Thread> = Cell::new(ptr::null_mut()); #[thread_local] static MARKER: u8 = 0; +/// The calling thread's signal mask (`sigmask`). +#[thread_local] +static MASK: Cell = Cell::new(0); + /// Blocks of detached threads that have ended, for the next creator to reap. static REAPED: Lock> = Lock::new(Vec::new()); @@ -149,6 +155,7 @@ unsafe extern "C" fn thread_entry(arg: u64) { unsafe { syscall::futex_wait(t.published.as_ptr(), 0, None) }; } SELF.set(thread); + MASK.set(t.mask); let result = unsafe { (t.start)(t.arg) }; unsafe { exit_thread(result) } } @@ -198,6 +205,7 @@ pub unsafe extern "C" fn pthread_create( start: start_routine, arg, result: AtomicPtr::new(ptr::null_mut()), + mask: MASK.get(), })); let top = (stack as usize + stack_size) & !(STACK_ALIGN - 1); // SAFETY: the entry is a function of this library, and the stack is a @@ -277,6 +285,24 @@ pub extern "C" fn pthread_equal(t1: PthreadT, t2: PthreadT) -> i32 { (t1 == t2) as i32 } +/// The calling thread's signal mask, changed as `how` says when `set` is not +/// null, answered in `old` when that is not. No signal is ever raised, so a +/// blocked one is never held back: the mask is kept, answered, and inherited. +#[no_mangle] +pub unsafe extern "C" fn pthread_sigmask(how: i32, set: *const u64, old: *mut u64) -> i32 { + let mask = MASK.get(); + if !set.is_null() { + match crate::sigmask::changed(mask, how, unsafe { *set }) { + Some(next) => MASK.set(next), + None => return EINVAL, + } + } + if !old.is_null() { + unsafe { *old = mask }; + } + 0 +} + #[no_mangle] pub extern "C" fn sched_yield() -> i32 { // No syscall gives the processor up; this is what std's `yield_now` does. diff --git a/userland/libc/src/refused.rs b/userland/libc/src/refused.rs index f52ee8a62d2..115eeb1b7f1 100644 --- a/userland/libc/src/refused.rs +++ b/userland/libc/src/refused.rs @@ -12,14 +12,6 @@ fn refuse() -> i32 { -1 } -/// No signal is ever raised, `SIGALRM` among them. POSIX reserves no value of -/// `alarm`'s for failure: the 0 says no alarm was pending, which is so. -#[no_mangle] -pub extern "C" fn alarm(_seconds: u32) -> u32 { - errno::set(ENOSYS); - 0 -} - /// A process never replaces its image: a program starts as a new process. #[no_mangle] pub unsafe extern "C" fn execv(_path: *const u8, _argv: *const *const u8) -> i32 { diff --git a/userland/libc/src/sigmask.rs b/userland/libc/src/sigmask.rs new file mode 100644 index 00000000000..4eca5646889 --- /dev/null +++ b/userland/libc/src/sigmask.rs @@ -0,0 +1,27 @@ +//! A thread's signal mask as `pthread_sigmask` and `sigprocmask` change it: +//! signal `n` is bit `n - 1` of a `sigset_t`, Linux's layout. It reads and +//! sets nothing but what it is handed, so the host tests it +//! (`toyos-libc-copies`). + +pub(crate) const SIG_BLOCK: i32 = 0; +pub(crate) const SIG_UNBLOCK: i32 = 1; +pub(crate) const SIG_SETMASK: i32 = 2; + +const SIGKILL: u32 = 9; +const SIGSTOP: u32 = 19; + +/// What no mask holds: POSIX drops `SIGKILL` and `SIGSTOP` from a mask +/// without an error. +const UNBLOCKABLE: u64 = (1 << (SIGKILL - 1)) | (1 << (SIGSTOP - 1)); + +/// `mask` changed by `set` as `how` says, or `None` for a `how` that is none +/// of the three, which POSIX refuses `EINVAL`. +pub(crate) fn changed(mask: u64, how: i32, set: u64) -> Option { + let next = match how { + SIG_BLOCK => mask | set, + SIG_UNBLOCK => mask & !set, + SIG_SETMASK => set, + _ => return None, + }; + Some(next & !UNBLOCKABLE) +} From 616a90150129ee05028d9f52be9122ce05ab8ead Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 00:51:12 +0200 Subject: [PATCH 04/12] libc: memreq and the mask test pass clippy's lints toyos-libc-copies brings libc's pure modules under the workspace's clippy: a fixed place off a page is `!addr.is_multiple_of(PAGE)`, and the test's every-signal mask is `!(kill | stop)`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- toyos-libc-copies/src/signal_masks.rs | 2 +- userland/libc/src/memreq.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/toyos-libc-copies/src/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs index 785ae46b0c5..8862e47fa85 100644 --- a/toyos-libc-copies/src/signal_masks.rs +++ b/toyos-libc-copies/src/signal_masks.rs @@ -20,7 +20,7 @@ fn each_how_changes_the_mask_as_posix_says() { assert_eq!(sigmask::changed(hup, SIG_SETMASK, 0), Some(0)); // Asked for, and dropped without an error. assert_eq!(sigmask::changed(0, SIG_BLOCK, kill | stop | usr1), Some(usr1)); - assert_eq!(sigmask::changed(0, SIG_SETMASK, u64::MAX), Some(u64::MAX & !(kill | stop))); + assert_eq!(sigmask::changed(0, SIG_SETMASK, u64::MAX), Some(!(kill | stop))); for how in [-1, 3, 99] { assert_eq!(sigmask::changed(hup, how, usr1), None, "how {how}"); } diff --git a/userland/libc/src/memreq.rs b/userland/libc/src/memreq.rs index 0498df1ca45..4f621fc3f71 100644 --- a/userland/libc/src/memreq.rs +++ b/userland/libc/src/memreq.rs @@ -26,7 +26,7 @@ pub(crate) enum MapRefusal { /// it is. An anonymous `MAP_SHARED` is not: with no `fork`, no other process /// can map it, so a private mapping is all that sharing it could mean. pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Option { - if len == 0 || (flags & MAP_FIXED != 0 && addr % PAGE != 0) { + if len == 0 || (flags & MAP_FIXED != 0 && !addr.is_multiple_of(PAGE)) { return Some(MapRefusal::Invalid); } if flags & MAP_ANONYMOUS == 0 { From efe858a69e78db9b7516fa385c3c6d9b25fe6500 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 01:57:15 +0200 Subject: [PATCH 05/12] aligned_alloc is std's in a Rust program, and fcntl's F_DUPFD duplicates #644's final review tightened what this must satisfy. - aligned_alloc in a Rust std program is std's C allocator's now (the rust pin moves to 9151571cae9, one commit on aca5f527fcb): each block carries its size and alignment in front of it, as libc's own allocator does, and free and realloc release it at that layout. libc's std-runtime aligned_alloc, which answered std's malloc for an alignment to 16 and EINVAL above, goes with memreq::std_aligns and its test. tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs allocates at 64 and 4096, reallocs the 4096 block to 1 MiB and checks its first bytes carried, and frees each; dlmalloc asserts a released block's size, so a block released at another layout ends the program. - fcntl's F_DUPFD answers a working duplicate at or above its argument, the first the kernel hands dup, the lower ones taken on the way closed again; a negative argument is EINVAL. F_DUPFD_CLOEXEC stays refused: close-on-exec is stage 3's. 206_libc_refusals fstats the duplicate for the file's byte. - src/libc.rs's test module is at the end of the file, where clippy's items_after_test_module has it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...bc-refuses-what-toyos-cannot-yet-answer.md | 10 +-- rust | 2 +- src/libc.rs | 36 ++++----- tests/testcases/tinycc/206_libc_refusals.c | 12 ++- .../testcases/tinycc/206_libc_refusals.expect | 4 +- .../src/bin/c_aligned_alloc.rs | 78 +++++++++++++++++++ toyos-libc-copies/src/memory_refusals.rs | 10 +-- userland/libc/src/memory.rs | 12 --- userland/libc/src/memreq.rs | 7 -- userland/libc/src/posix_io.rs | 27 ++++++- 10 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md index 5f9c93159db..c30db81cdb0 100644 --- a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -29,12 +29,12 @@ names: - `mmap` of a file, `ENODEV`, and of executable memory, `ENOTSUP`: the kernel maps neither. - `fcntl`: a record lock, `EINVAL`, POSIX's answer for a file that supports no - locking; `F_DUPFD`, `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and - `F_SETOWN`, `ENOSYS`. `F_GETFD` and `F_SETFD` answer 0 and keep nothing: - close-on-exec is the descriptor table of stage 3 of + locking; `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and `F_SETOWN`, + `ENOSYS`. `F_GETFD` and `F_SETFD` answer 0 and keep nothing: close-on-exec is + the descriptor table of stage 3 of `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`. -- `aligned_alloc` beside std, above 16, `EINVAL`: std's `malloc` aligns to 16 - and its `free` knows no other alignment. + `F_DUPFD` answers a duplicate at or above its argument, the first the kernel + hands `dup`, where POSIX has the lowest free number. Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no call replacing a process's image; `setsid` and `getsid`, no POSIX session; diff --git a/rust b/rust index aca5f527fcb..9151571cae9 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit aca5f527fcb5dc3b694e33a0f0a7312d8d29aa60 +Subproject commit 9151571cae9014619926872b12fcebf3b0a3c14a diff --git a/src/libc.rs b/src/libc.rs index 6de060544a7..b85cd5cbbc2 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -133,24 +133,6 @@ fn empty_libraries(lib: &Path) { } } -#[cfg(test)] -mod tests { - use super::*; - use toyos_tmpdir::TempDir; - - /// **`-lm` finds a library in the C sysroot**, and so do POSIX's other - /// three: each an `ar` archive of no members. - #[test] - fn the_c_sysroot_names_the_posix_libraries() { - let lib = TempDir::new("libc-empty"); - empty_libraries(&lib); - for name in ["m", "pthread", "dl", "rt"] { - let path = lib.join(format!("lib{name}.a")); - assert_eq!(fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())), b"!\n"); - } - } -} - /// Extract .o files from rlibs and merge them into a single GNU-format ar archive. fn merge_rlibs(rlib_paths: &[std::path::PathBuf]) -> Vec { let mut members: Vec<(String, Vec)> = Vec::new(); @@ -307,3 +289,21 @@ fn extract_rlib_objects(data: &[u8], out: &mut Vec<(String, Vec)>) { } } } + +#[cfg(test)] +mod tests { + use super::*; + use toyos_tmpdir::TempDir; + + /// **`-lm` finds a library in the C sysroot**, and so do POSIX's other + /// three: each an `ar` archive of no members. + #[test] + fn the_c_sysroot_names_the_posix_libraries() { + let lib = TempDir::new("libc-empty"); + empty_libraries(&lib); + for name in ["m", "pthread", "dl", "rt"] { + let path = lib.join(format!("lib{name}.a")); + assert_eq!(fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())), b"!\n"); + } + } +} diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index dd3aff700de..335b2c9eb76 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -80,7 +80,17 @@ int main(void) { said("fcntl F_SETLK", fcntl(fd, F_SETLK, &lock)); said("fcntl F_SETLKW", fcntl(fd, F_SETLKW, &lock)); said("fcntl F_GETLK", fcntl(fd, F_GETLK, &lock)); - said("fcntl F_DUPFD", fcntl(fd, F_DUPFD, 10)); + said("fcntl F_DUPFD -1", fcntl(fd, F_DUPFD, -1)); + said("fcntl F_DUPFD_CLOEXEC", fcntl(fd, F_DUPFD_CLOEXEC, 10)); + /* F_DUPFD itself duplicates: its answer is at or above its argument, and + fstat reads the file's one byte through it. */ + int duplicate = fcntl(fd, F_DUPFD, 10); + struct stat through; + int read_back = duplicate >= 0 && fstat(duplicate, &through) == 0 && through.st_size == 1; + printf("fcntl F_DUPFD 10: %s; fstat of it: %s\n", duplicate >= 10 ? "10 or above" : "below 10", + read_back ? "the file's one byte" : "not the file"); + if (duplicate >= 0) + close(duplicate); said("fcntl F_GETFL", fcntl(fd, F_GETFL)); said("fcntl F_SETFL", fcntl(fd, F_SETFL, O_NONBLOCK)); said("fcntl 12345", fcntl(fd, 12345)); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 4d08b24bf7e..c795375e57b 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -18,7 +18,9 @@ realpath: -1, ENOSYS fcntl F_SETLK: -1, EINVAL fcntl F_SETLKW: -1, EINVAL fcntl F_GETLK: -1, EINVAL -fcntl F_DUPFD: -1, ENOSYS +fcntl F_DUPFD -1: -1, EINVAL +fcntl F_DUPFD_CLOEXEC: -1, ENOSYS +fcntl F_DUPFD 10: 10 or above; fstat of it: the file's one byte fcntl F_GETFL: -1, ENOSYS fcntl F_SETFL: -1, ENOSYS fcntl 12345: -1, EINVAL diff --git a/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs b/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs new file mode 100644 index 00000000000..b7bd6b0d636 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs @@ -0,0 +1,78 @@ +//! std's C allocator, as C code in a Rust program reaches it: `aligned_alloc` +//! answers a block at a multiple of its alignment, and `realloc` and `free` +//! release each block at the layout it was allocated with. The allocator +//! under them, dlmalloc, asserts a released block's size, so a block released +//! at another layout ends this program rather than passing. + +extern "C" { + fn aligned_alloc(align: usize, size: usize) -> *mut u8; + fn malloc(size: usize) -> *mut u8; + fn realloc(ptr: *mut u8, size: usize) -> *mut u8; + fn free(ptr: *mut u8); +} + +const MIB: usize = 1 << 20; + +fn pattern(i: usize) -> u8 { + (i as u8).wrapping_mul(31) ^ 0x5a +} + +/// Fill `len` bytes at `block` with the pattern. +unsafe fn fill(block: *mut u8, len: usize) { + for i in 0..len { + unsafe { block.add(i).write(pattern(i)) }; + } +} + +/// Whether `len` bytes at `block` still hold the pattern. +unsafe fn holds(block: *const u8, len: usize) -> bool { + (0..len).all(|i| unsafe { block.add(i).read() } == pattern(i)) +} + +fn main() { + let mut blocks = Vec::new(); + for align in [64usize, 4096] { + // SAFETY: C11's aligned_alloc, with a power-of-two alignment. + let block = unsafe { aligned_alloc(align, 100) }; + assert!(!block.is_null(), "aligned_alloc({align}, 100) answered null"); + assert_eq!(block.addr() % align, 0, "aligned_alloc({align}, 100) answered {block:p}"); + // SAFETY: 100 bytes the allocator just gave. + unsafe { fill(block, 100) }; + println!("aligned_alloc({align}): a multiple of {align}"); + blocks.push((align, block)); + } + + // Grown past the arena its first allocation came from, so dlmalloc moves + // it: the old block is released at the layout its header names. + let (align, block) = blocks.pop().expect("the 4096 block"); + // SAFETY: a block aligned_alloc answered, not yet released. + let grown = unsafe { realloc(block, MIB) }; + assert!(!grown.is_null(), "realloc of the {align} block to 1 MiB answered null"); + assert_eq!(grown.addr() % align, 0, "realloc of the {align} block answered {grown:p}"); + // SAFETY: at least 100 bytes of the block realloc answered. + assert!(unsafe { holds(grown, 100) }, "realloc of the {align} block to 1 MiB lost its first bytes"); + println!("realloc({align} block, 1 MiB): still a multiple of {align}, its first 100 bytes carried"); + blocks.push((align, grown)); + + // A block malloc answered, grown and shrunk, is released at its own too. + // SAFETY: C's malloc and realloc, each answer checked before use. + unsafe { + let small = malloc(32); + assert!(!small.is_null()); + fill(small, 32); + let big = realloc(small, 4 * MIB); + assert!(!big.is_null() && holds(big, 32), "realloc of a malloc block lost its bytes"); + let back = realloc(big, 16); + assert!(!back.is_null() && holds(back, 16), "a shrinking realloc lost its bytes"); + free(back); + } + + for (align, block) in blocks { + // SAFETY: each a live block of this allocator. + unsafe { free(block) }; + println!("free({align} block): released"); + } + // SAFETY: C11's: a refusal, not a block. + assert!(unsafe { aligned_alloc(24, 8) }.is_null(), "aligned_alloc of an alignment that is no power of two answered a block"); + println!("aligned_alloc(24): null"); +} diff --git a/toyos-libc-copies/src/memory_refusals.rs b/toyos-libc-copies/src/memory_refusals.rs index 396457417ee..47d49072f88 100644 --- a/toyos-libc-copies/src/memory_refusals.rs +++ b/toyos-libc-copies/src/memory_refusals.rs @@ -1,7 +1,6 @@ //! What libc's memory calls refuse before the kernel or the allocator is //! asked: `mmap` a file, executable memory, no bytes, or a fixed place off a -//! page; `posix_madvise` anything but its five; `aligned_alloc` beside std any -//! alignment std's `malloc` does not give. +//! page; `posix_madvise` anything but its five. use crate::memreq::{self, MapRefusal, MAP_ANONYMOUS, MAP_FIXED, PROT_EXEC, PROT_READ, PROT_WRITE}; @@ -34,10 +33,3 @@ fn posix_madvise_takes_its_five_and_only_them() { let taken: Vec = (-2..8).filter(|&a| memreq::is_advice(a)).collect(); assert_eq!(taken, [0, 1, 2, 3, 4]); } - -#[test] -fn aligned_alloc_beside_std_gives_what_malloc_aligns_to() { - let given: Vec = (0..=64).filter(|&a| memreq::std_aligns(a)).collect(); - assert_eq!(given, [1, 2, 4, 8, 16]); - assert!(!memreq::std_aligns(1 << 12)); -} diff --git a/userland/libc/src/memory.rs b/userland/libc/src/memory.rs index c6f296545d9..906b4094a39 100644 --- a/userland/libc/src/memory.rs +++ b/userland/libc/src/memory.rs @@ -11,18 +11,6 @@ extern "C" { pub fn free(p: *mut u8); } -/// `aligned_alloc` beside std: a block std's `malloc` gives, for an alignment -/// it gives, and C11's `EINVAL` for any other (`memreq::std_aligns`). -#[cfg(feature = "std-runtime")] -#[no_mangle] -pub unsafe extern "C" fn aligned_alloc(align: usize, size: usize) -> *mut u8 { - if !crate::memreq::std_aligns(align) { - crate::errno::set(crate::errno::EINVAL); - return ptr::null_mut(); - } - unsafe { malloc(size) } -} - #[cfg(not(feature = "std-runtime"))] mod backend { use core::alloc::Layout; diff --git a/userland/libc/src/memreq.rs b/userland/libc/src/memreq.rs index 4f621fc3f71..9a142b79509 100644 --- a/userland/libc/src/memreq.rs +++ b/userland/libc/src/memreq.rs @@ -42,10 +42,3 @@ pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Op pub(crate) fn is_advice(advice: i32) -> bool { (0..=4).contains(&advice) } - -/// Whether `aligned_alloc` beside std can align to `align`: std's `malloc` -/// aligns every block to 16, and its `free` knows no other alignment. -#[cfg(feature = "std-runtime")] -pub(crate) fn std_aligns(align: usize) -> bool { - align.is_power_of_two() && align <= 16 -} diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index de129faecc8..ac6bfda176e 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -405,22 +405,41 @@ const F_GETOWN: i32 = 9; const F_DUPFD_CLOEXEC: i32 = 1030; #[no_mangle] -pub unsafe extern "C" fn fcntl(_fd: i32, cmd: i32, _arg: i64) -> i32 { +pub unsafe extern "C" fn fcntl(fd: i32, cmd: i32, arg: i64) -> i32 { let refused = match cmd { + F_DUPFD if arg >= 0 => return unsafe { dup_at_least(fd, arg) }, // Close-on-exec is the descriptor table's of stage 3 of // issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md. F_GETFD | F_SETFD => return 0, // POSIX's answer for a file that supports no locking, which none here does. F_GETLK | F_SETLK | F_SETLKW => EINVAL, - // A descriptor's lowest free number, its status flags and its owner are - // nothing this library knows. - F_DUPFD | F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => crate::errno::ENOSYS, + // A descriptor's status flags, its close-on-exec and its owner are + // nothing this library keeps. + F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => crate::errno::ENOSYS, _ => EINVAL, }; crate::errno::set(refused); -1 } +/// A duplicate of `raw_fd` numbered `floor` or above. The kernel picks each +/// number `dup` answers, so this is the first such answer and not the lowest +/// free number, and every lower one taken on the way is closed again. +unsafe fn dup_at_least(raw_fd: i32, floor: i64) -> i32 { + let mut below = alloc::vec::Vec::new(); + let answer = loop { + let duplicate = unsafe { dup(raw_fd) }; + if duplicate < 0 || i64::from(duplicate) >= floor { + break duplicate; + } + below.push(duplicate); + }; + for lower in below { + unsafe { close(lower) }; + } + answer +} + // Directory operations #[repr(C)] From e1c2103268f01de40f1390490d6e16660b41de09 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 02:03:51 +0200 Subject: [PATCH 06/12] libc: #644's issues met and closed; madvise and -lc; each case reads back #644 landed the issues this branch's work answers. Each is met here and deleted, or brought up to date: - the-c-sysroot-has-no-libm-so-llvms-configure-fails: configure exits 0 against this C sysroot with the eleven probes it names passing (measured, PR body). Its exit also names -lc: libc.a joins the empty libraries, and 206_libc_refusals links with -lc -lm -lpthread -ldl -lrt (tests/common/compile.rs's LINK_FLAGS), so a sysroot missing one builds no corpus. - libc-mmap-ignores-the-file-it-is-asked-to-map: a file, private or shared, at offset 0 or a later page, is ENODEV, each asserted by 206. - libc-readdir-calls-every-entry-a-regular-file: 207 lists a file and a directory. - libc-fcntl-and-fchmod-answer-0-and-do-nothing: every fcntl command does what POSIX says or answers -1 with errno, F_DUPFD's duplicate is fstat'd, and chmod and fchmod refuse with the mode read back through stat and fstat. What fcntl still refuses is libc-refuses-what-toyos-cannot-yet-answer's. - a-rust-std-program-defines-no-aligned-alloc: std's aligned_alloc, the previous commit. - toyos-libc-lacks-the-posix-surface-llvm-compiles-against: every name it lists is declared and defined, refused with errno, or (wait, wait4, the signal-set calls, alarm) stage 3's. madvise is new: Linux's four hints taken, its MADV_DONTNEED discard refused ENOSYS, anything else or an address off a page EINVAL, its rule memreq's and host-tested. 207 prints the macros and sizes LLVM reads, so the corpus is a compile and link probe of every name. - libc-pread-and-pwrite-move-the-offset-another-thread-shares is renamed libc-has-no-pread-or-pwrite: they are gone, and its exit stands. - toyos-builds-itself.md's M2 and M3 lists name what is left; libc-has-no- alarm.md and the bootstrap issue cite what stands instead of the closed POSIX issue, the bootstrap issue with the LLVM measurement. The sysroot recipe moves (10): a C sysroot now holds libc.a. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...st-std-program-defines-no-aligned-alloc.md | 30 ------- ...ild-llvm-clang-and-lld-for-a-toyos-host.md | 7 +- ...cntl-and-fchmod-answer-0-and-do-nothing.md | 28 ------- issues/build/libc-has-no-alarm.md | 6 +- issues/build/libc-has-no-pread-or-pwrite.md | 26 ++++++ ...map-ignores-the-file-it-is-asked-to-map.md | 28 ------- ...e-move-the-offset-another-thread-shares.md | 25 ------ ...eaddir-calls-every-entry-a-regular-file.md | 18 ----- ...ot-has-no-libm-so-llvms-configure-fails.md | 33 -------- issues/build/toyos-builds-itself.md | 23 +++--- ...the-posix-surface-llvm-compiles-against.md | 80 ------------------- src/libc.rs | 14 ++-- src/sysroot.rs | 2 +- tests/common/compile.rs | 9 ++- tests/testcases/tinycc/206_libc_refusals.c | 27 +++++-- .../testcases/tinycc/206_libc_refusals.expect | 9 ++- tests/testcases/tinycc/207_libc_names.c | 12 +++ tests/testcases/tinycc/207_libc_names.expect | 2 + toyos-libc-copies/src/memory_refusals.rs | 21 ++++- userland/libc/include/sys/mman.h | 7 ++ userland/libc/src/memreq.rs | 25 +++++- userland/libc/src/posix_io.rs | 16 ++++ 22 files changed, 168 insertions(+), 280 deletions(-) delete mode 100644 issues/build/a-rust-std-program-defines-no-aligned-alloc.md delete mode 100644 issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md create mode 100644 issues/build/libc-has-no-pread-or-pwrite.md delete mode 100644 issues/build/libc-mmap-ignores-the-file-it-is-asked-to-map.md delete mode 100644 issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md delete mode 100644 issues/build/libc-readdir-calls-every-entry-a-regular-file.md delete mode 100644 issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md delete mode 100644 issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md diff --git a/issues/build/a-rust-std-program-defines-no-aligned-alloc.md b/issues/build/a-rust-std-program-defines-no-aligned-alloc.md deleted file mode 100644 index 04d6fe8009a..00000000000 --- a/issues/build/a-rust-std-program-defines-no-aligned-alloc.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# A Rust std program defines no aligned_alloc - -In a Rust std program the C allocator is std's: -`rust/library/std/src/sys/pal/toyos/mod.rs` defines `malloc`, `calloc`, -`realloc` and `free`, and libc's allocator is not built beside std -(`userland/libc/src/memory.rs`, `feature = "std-runtime"`). Nothing defines -C11's `aligned_alloc`, which libc++abi calls for an over-aligned `operator new` -and in its fallback allocator: the link -`issues/build/a-rust-std-binary-cannot-link-the-cxx-runtime.md` describes -leaves it undefined, referenced from `libc++.a`'s `stdlib_new_delete.cpp` and -`fallback_malloc.cpp`. - -Std's `free` and `realloc` release every block at alignment 16, the one its -`malloc` allocates at. The allocator under them, dlmalloc -(`rust/library/std/src/sys/alloc/toyos.rs`), checks a released block's size and -ignores its alignment, so a block allocated at a larger one and released at 16 -is a layout mismatch nothing reports. - -**Exit**: std's C allocator defines `aligned_alloc`, whose block's address is a -multiple of the requested alignment, and `free` and `realloc` release each -block at the layout it was allocated with, the block's header carrying its -alignment as libc's own allocator's does. A guest case in a Rust std program -allocates through `aligned_alloc` at alignments 64 and 4096, asserts each -address a multiple of its alignment, and frees each. diff --git a/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md b/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md index 2539f8f9796..91ecaef0d40 100644 --- a/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md +++ b/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md @@ -28,8 +28,11 @@ step for that target: The last three are ToyOS arms at existing dispatch sites, written as upstream would take them. ToyOS joins `bit.h`'s `` list, so libc carries -POSIX's `endian.h` and no BSD name -(`issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md`). +POSIX's `endian.h` (`userland/libc/include/endian.h`) and no BSD name. With +those two arms on `849da7d62` and CMake told the system by hand, the 71 +libraries rustc links compile against the C sysroot but for five objects of +`LLVMSupport`, which stop on `alarm` and on stage 3's `wait`, `wait4` and +signal-set calls; without them, every object that includes `bit.h` stops. Bootstrap's arm names the system `ToyOS`, which LLVM's configure refuses, `Unable to determine platform`, until CMake knows ToyOS and sets `UNIX` (`issues/build/the-cxx-runtime-names-toyos-to-cmake-as-unix.md`). diff --git a/issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md b/issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md deleted file mode 100644 index b0a290e3433..00000000000 --- a/issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc's fcntl, chmod and fchmod answer 0 and do nothing - -`fcntl` (`userland/libc/src/posix_io.rs`) answers 0 to every command on every -descriptor: `F_DUPFD` names descriptor 0 as the duplicate, `F_GETFL` reads -every descriptor as read-only and blocking, `F_SETFL` sets nothing, and a -command it does not know is answered as done. `chmod` and `fchmod` answer 0 and -change nothing, and LLVM's `sys::fs::setPermissions` calls each -(`llvm/lib/Support/Unix/Path.inc`). Once `fcntl.h` declares the record locks -(`issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md`), -LLVM's `sys::fs::tryLockFile` and `lockFile`, in the same file, take a lock -nothing holds. Close-on-exec, `F_GETFD` and `F_SETFD`, is the descriptor -table's of stage 3 of -`issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`. - -**Exit**: every other command POSIX defines for `fcntl` does what POSIX says or -answers -1 with `errno` set — a record lock `EINVAL`, as POSIX has a file that -supports no locking answer — and a command POSIX does not define answers -`EINVAL`; `chmod` and `fchmod` each change the mode `stat` and `fstat` read -back, or answer -1 with `errno` set. A guest C case asserts each command's -answer: `F_GETFL` on a descriptor opened `O_RDWR | O_APPEND` answers both, and -`F_DUPFD` a descriptor no lower than its argument. It asserts what `chmod` and -`fchmod` answer, and the mode `stat` and `fstat` then read. diff --git a/issues/build/libc-has-no-alarm.md b/issues/build/libc-has-no-alarm.md index 3e9df88a5eb..b3c7054aa15 100644 --- a/issues/build/libc-has-no-alarm.md +++ b/issues/build/libc-has-no-alarm.md @@ -9,9 +9,9 @@ opened: 2026-09-30 libc neither declares nor defines `alarm`, and LLVM bounds its wait on a child with one: a `SIGALRM` handler makes `wait4` answer `EINTR` (`llvm/lib/Support/Unix/Program.inc`, `Wait`), so an LLVM built for ToyOS does -not link without it -(`issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md` -measures that link). POSIX gives `alarm` no refusal, and its `SIGALRM` reaches +not link without it: rustc's LLVM wrapper linked `-shared -z defs` with the +libraries it needs leaves it undefined, beside stage 3's `wait`, `wait4` and +signal-set calls. POSIX gives `alarm` no refusal, and its `SIGALRM` reaches a handler only through the signals libc imitates from stage 3 of `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` on; this waits on that stage. diff --git a/issues/build/libc-has-no-pread-or-pwrite.md b/issues/build/libc-has-no-pread-or-pwrite.md new file mode 100644 index 00000000000..dcac3f48c55 --- /dev/null +++ b/issues/build/libc-has-no-pread-or-pwrite.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-09-30 +--- + +# libc has no pread or pwrite + +libc defines neither `pread` nor `pwrite`. The two it had sought the +descriptor to their offset, read or wrote, and sought back: three calls, +nothing held between them, so a `read`, `write` or `lseek` of the same +descriptor on another thread landing between them read or wrote at the wrong +offset, or had its own moved. The ABI has no positional read or write +(`toyos-abi/src/syscall.rs` has `seek`). With no header declaring `pread`, +LLVM's configure finds none, and LLVM reads a file slice with `lseek` and +`read` instead (`llvm/lib/Support/Unix/Path.inc`, `readNativeFileSlice`), which +moves an offset another thread shares just as they did. + +**Exit**: `pread` and `pwrite` are declared and defined and never move the +descriptor's offset, which a guest C case shows over a file whose every 8-byte +word holds its own offset. While one thread `read`s the file through, another +`pread`s it at one offset, and every word either reads holds the offset it was +read from. While one thread `write`s such words through a second file, another +`pwrite`s one at its offset, and every word of that file then holds its own +offset. A seek, read or write, seek-back `pread` and `pwrite` are the negative +control: the case is measured red against them. diff --git a/issues/build/libc-mmap-ignores-the-file-it-is-asked-to-map.md b/issues/build/libc-mmap-ignores-the-file-it-is-asked-to-map.md deleted file mode 100644 index c3dc5d39be8..00000000000 --- a/issues/build/libc-mmap-ignores-the-file-it-is-asked-to-map.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc's mmap ignores the file it is asked to map - -`mmap` (`userland/libc/src/posix_io.rs`) never reads its `fd` or `offset`, and -maps anonymous memory, `MAP_SHARED` as `MAP_PRIVATE`: a file mapping succeeds -and reads zeros, and what is written through a shared one never reaches the -file. The kernel maps no file (`kernel/src/syscall/vm.rs`). Read from LLVM at -`849da7d62`, not run: - -- `MemoryBuffer` maps a file it reads, private and read-only, when the file is - at least four pages and needs no terminator past the mapping's end - (`llvm/lib/Support/MemoryBuffer.cpp`'s `shouldUseMmap`, with libc's - `sysconf(_SC_PAGESIZE)` answering 4096), so it reads such a file as zeros - and reports no error. -- `FileOutputBuffer`, through which lld writes its output, maps the output file - shared and writable (`llvm/lib/Support/FileOutputBuffer.cpp`), so lld writes - an output of zeros and reports success. - -Each falls back, to `read` and to a buffer in memory, when the map fails. - -**Exit**: `mmap` of a file, shared or private, at offset 0 or at a later page, -answers `MAP_FAILED` with `errno` `ENODEV`, and a guest C case asserts each and -that an anonymous mapping still maps. diff --git a/issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md b/issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md deleted file mode 100644 index fb5a52d61db..00000000000 --- a/issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc's pread and pwrite move the offset another thread shares - -`pread` and `pwrite` (`userland/libc/src/posix_io.rs`) seek the descriptor to -their offset, read or write, and seek it back: three calls, nothing held -between them. POSIX has them leave the file offset alone. Here a `read`, -`write`, `lseek`, `pread` or `pwrite` of the same descriptor on another thread, -landing between those calls, reads or writes at the wrong offset, or has its -own moved. The ABI has no positional read or write (`toyos-abi/src/syscall.rs` -has `seek`). No header declares `pread`, -so LLVM's configure finds none; once it does, LLVM reads every file slice -through it (`llvm/lib/Support/Unix/Path.inc`, `readNativeFileSlice`). - -**Exit**: `pread` and `pwrite` never move the descriptor's offset, which a -guest C case shows over a file whose every 8-byte word holds its own offset. -While one thread `read`s the file through, another `pread`s it at one offset, -and every word either reads holds the offset it was read from. While one -thread `write`s such words through a second file, another `pwrite`s one at its -offset, and every word of that file then holds its own offset. Today's `pread` -and `pwrite` are the negative control: the case is measured red against them. diff --git a/issues/build/libc-readdir-calls-every-entry-a-regular-file.md b/issues/build/libc-readdir-calls-every-entry-a-regular-file.md deleted file mode 100644 index 2c6b183653f..00000000000 --- a/issues/build/libc-readdir-calls-every-entry-a-regular-file.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# libc's readdir calls every entry a regular file - -`readdir` (`userland/libc/src/posix_io.rs`) answers `d_type` `DT_REG` for every -entry, a directory's too. libc ships no `dirent.h` yet; -once one defines `DTTOIF`, LLVM takes an entry's type from `d_type` -(`llvm/lib/Support/Unix/Path.inc`, `direntType`) and reads a directory as a -file. - -**Exit**: `readdir` answers each entry's type, or `DT_UNKNOWN` where it does -not know it, which a guest C case shows over a directory holding a file and a -directory: the directory's entry answers `DT_DIR` or `DT_UNKNOWN`, never -`DT_REG`, and the file's `DT_REG` or `DT_UNKNOWN`. diff --git a/issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md b/issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md deleted file mode 100644 index e9cc6d111b5..00000000000 --- a/issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# The C sysroot has no libm, so LLVM's configure fails - -LLVM's configure adds `m` to every link probe on a `UNIX` system that is not -Apple, BeOS or Haiku (`llvm/cmake/config-ix.cmake`), and probes `pthread`, `c`, -`dl` and `rt` as libraries. The C sysroot's `lib/` holds `libtoyos_c.a` and no -archive under any of those names, so every probe that links fails. - -Configuring `src/llvm-project/llvm` at `849da7d62` for `x86_64-unknown-toyos`, -with CMake's system `ToyOS` and `UNIX=ON`, against #637's C sysroot -(`de0de8ee7862147a`): `unable to find library -lm` 18 times, and exit 1 at -`cmake/modules/CheckAtomic.cmake:59`, "Host compiler appears to require -libatomic, but cannot find it". With empty `libm.a`, `libpthread.a`, `libdl.a` -and `librt.a` beside `libtoyos_c.a`, the same configure exits 0, and these -probes pass that failed: `HAVE_CXX_ATOMICS_WITHOUT_LIB`, -`HAVE_BUILTIN_THREAD_POINTER`, `HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC`, -`C_SUPPORTS_WERROR_UNGUARDED_AVAILABILITY_NEW`, `sysconf`, `isatty`, -`strerror_r`, `setenv`, `dlopen`, `dlopen` in `dl` and `pthread_create` in -`pthread`. - -`pread` is not found in either: no header declares it, -and once one does, LLVM reads through -`issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md`. - -**Exit**: that configure exits 0 against the C sysroot as `src/libc.rs` lays it -out, with those eleven probes passing, and a host test links a C probe with -each of `-lm`, `-lpthread`, `-lc`, `-ldl` and `-lrt` against that sysroot with -the toolchain's clang. diff --git a/issues/build/toyos-builds-itself.md b/issues/build/toyos-builds-itself.md index 1fedcf3c3f8..53db17f659b 100644 --- a/issues/build/toyos-builds-itself.md +++ b/issues/build/toyos-builds-itself.md @@ -56,23 +56,18 @@ the published crates.io crate is yanked. **What stops M2: LLVM, clang and lld built for a ToyOS host**, in the order each blocks the next. - Configure: the `clang-tblgen` and the CMake system of - `issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`, - and `issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md`. -- Compile: `issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md`, - with the names it leaves to - the header gate (`toyos-libc-copies/src/prototypes.rs`), - and the bootstrap issue's `bit.h` and `is_local_impl`. -- Link: the POSIX issue's functions, with those it leaves to the child-process - track's stage 3 and to `issues/build/libc-has-no-alarm.md`. -- Run: `issues/build/libc-mmap-ignores-the-file-it-is-asked-to-map.md`, - `issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md`, - `issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md` - and `issues/build/libc-readdir-calls-every-entry-a-regular-file.md`. + `issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`. +- Compile: the bootstrap issue's `bit.h` and `is_local_impl`, and the + signal-set calls, `wait` and `wait4` of the child-process track's stage 3 + with `issues/build/libc-has-no-alarm.md`. +- Link: the same stage 3 names and `alarm`. +- Run: what `issues/build/libc-refuses-what-toyos-cannot-yet-answer.md` lists, + `issues/build/libc-has-no-pread-or-pwrite.md`, and + `issues/build/libc-stat-answers-one-serial-number-for-every-file.md`. **What M3 adds: a rustc that carries that LLVM**, after all of M2's. - Build: `issues/build/rustc-llvm-cannot-build-for-a-toyos-host.md`. -- Link: `issues/build/a-rust-std-binary-cannot-link-the-cxx-runtime.md` and - `issues/build/a-rust-std-program-defines-no-aligned-alloc.md`. +- Link: `issues/build/a-rust-std-binary-cannot-link-the-cxx-runtime.md`. - Test: `issues/build/a-worktree-cannot-build-a-hosted-rustc-of-its-own.md`. M3's exit then waits on a linker in the guest diff --git a/issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md b/issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md deleted file mode 100644 index 496a052719a..00000000000 --- a/issues/build/toyos-libc-lacks-the-posix-surface-llvm-compiles-against.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# ToyOS's libc lacks the POSIX surface LLVM compiles against - -LLVM does not build for a ToyOS host, so neither do clang and lld (M2 of -`issues/build/toyos-builds-itself.md`) nor a rustc that carries it (M3). The 71 -libraries rustc links (`llvm-config --libnames` of -`compiler/rustc_llvm/build.rs`'s required components, with `x86` and -`aarch64`) stop in `LLVMSupport`, `LLVMTargetParser` and `LLVMObjectYAML`, on -names the C library does not declare or define. Measured by configuring -`src/llvm-project/llvm` at `849da7d62` for `x86_64-unknown-toyos`, with CMake's -system `ToyOS` and `UNIX=ON` as `src/libcxx.rs` names it, against #637's C -sysroot (`de0de8ee7862147a`), compiling with `ninja -k 0`, and declaring each -missing name in a scratch header until all 71 archives built, exit 0. Eight -objects outside those 71 also failed (ORC, the interpreter, `llvm-objcopy`'s -Mach-O, `llvm-exegesis`) and were not chased, and clang's and lld's own -libraries were not built. - -**Headers the C sysroot does not have:** `sys/resource.h`, `sys/utsname.h`, -`sys/statvfs.h`, `sys/un.h`, `pwd.h`, `sysexits.h`, and `endian.h`, which -`bit.h` includes once ToyOS joins its list -(`issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`). The -scratch headers carried `machine/endian.h` and `MNT_LOCAL`, BSD names LLVM -reads only on a system it does not list; that issue's two LLVM arms answer -them, not libc. - -**Names its headers do not declare:** -- `signal.h`: `pthread_sigmask`, `SIGUSR1`, `SIGUSR2`, `SA_ONSTACK`, - `SA_RESETHAND`, `SA_NODEFER`. -- `unistd.h`: `gethostname`, `getsid`, `setsid`, `execv`, `execve`, - `readlink`, `symlink`, `link`, `fchown`, `_SC_ARG_MAX`, `_SC_PAGE_SIZE`, - `_SC_GETPW_R_SIZE_MAX`. -- `string.h` and `stdlib.h`: `strnlen`, `strsignal`, `realpath`. -- `limits.h`: `_POSIX_ARG_MAX`. -- `sys/mman.h`: `msync`, `MS_SYNC`, `madvise`, `MADV_WILLNEED`, - `MADV_DONTNEED`. -- `fcntl.h`: `struct flock`, `F_SETLK`, `F_SETLKW`, `F_RDLCK`, `F_WRLCK`, - `F_UNLCK`, which `fcntl` answers as - `issues/build/libc-fcntl-and-fchmod-answer-0-and-do-nothing.md` says. -- `sys/socket.h`: `AF_UNIX`. `dlfcn.h`: `Dl_info`, `dladdr`. - -**Functions no archive defines**, once those compile. rustc's LLVM wrapper -(`compiler/rustc_llvm/llvm-wrapper`, whole), the 71 archives, `libc++.a` and -the Rust sysroot's archives, `libtoyos_c.a` among them, linked by `ld.lld --shared -z defs`, leave 29 C functions undefined. 22 are this issue's: -`dladdr execv execve fchown fstatvfs getpwnam_r getpwuid_r getrlimit link logb -madvise modf msync pthread_sigmask readlink realpath setrlimit setsid statvfs -strsignal symlink uname`. LLVM reaches `execv`, `execve` and `setsid` only -after libc's `fork`, which answers `ENOSYS` -(`llvm/lib/Support/Unix/Program.inc`, `Execute`). - -**A function it has and does not do.** `sigprocmask` -(`userland/libc/src/misc.rs`), which LLVM calls -(`llvm/lib/Support/Unix/Signals.inc`), answers 0 and neither sets nor reports -a mask. - -**Left to other issues.** `wait`, `wait4`, `sigemptyset`, `sigfillset` and -`sigaddset` are stage 3 of -`issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`, -with the `posix_spawn` LLVM starts a child through once libc has one. -`dirent.h`, `ftruncate` and `fchmod`, which libc defines and declares nowhere, are -the header gate's (`toyos-libc-copies/src/prototypes.rs`). -`alarm` is `issues/build/libc-has-no-alarm.md`'s, and `aligned_alloc` -`issues/build/a-rust-std-program-defines-no-aligned-alloc.md`'s. - -**Exit**: every name listed above is declared by the header POSIX puts it in, -and each function among them is defined and does what POSIX specifies — a name -POSIX does not specify, what its Linux manual page does — or refuses as POSIX -has it report an error, with `ENOSYS` where ToyOS has no such call: `setsid` -and `getsid`, the child-process track ruling out a POSIX session, and `execv` -and `execve`, no call replacing a process's image. A guest C case per function -asserts its answer, each refusal among them, and reads back the effect of each -that has one: the old set a second `pthread_sigmask` or `sigprocmask` answers, -`getrlimit` after `setrlimit`, `readlink` of what `symlink` made, and the file -read through the name `link` made. A host test compiles and links, with the -toolchain's clang against the C sysroot, one C probe per name. diff --git a/src/libc.rs b/src/libc.rs index b85cd5cbbc2..b7728248db8 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -120,12 +120,12 @@ pub fn build_c(root: &Path, toolchain: &Path, target_dir: &Path, arch: Arch) { crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.join("include")); } -/// The libraries a POSIX link names beside the C library, `-lm` first, whose +/// The libraries POSIX has a C compiler take, `-lc` and `-lm` among them, whose /// functions are all `libtoyos_c.a`'s. -const EMPTY_LIBRARIES: [&str; 4] = ["m", "pthread", "dl", "rt"]; +const EMPTY_LIBRARIES: [&str; 5] = ["c", "m", "pthread", "dl", "rt"]; -/// Put each of [`EMPTY_LIBRARIES`] in `lib` as an archive of no members, as -/// musl ships them: a link that names one finds it and takes nothing from it. +/// Put each of [`EMPTY_LIBRARIES`] in `lib` as an archive of no members: a +/// link that names one finds it and takes nothing from it. fn empty_libraries(lib: &Path) { for name in EMPTY_LIBRARIES { let path = lib.join(format!("lib{name}.a")); @@ -295,13 +295,13 @@ mod tests { use super::*; use toyos_tmpdir::TempDir; - /// **`-lm` finds a library in the C sysroot**, and so do POSIX's other - /// three: each an `ar` archive of no members. + /// **`-lc` and `-lm` find a library in the C sysroot**, and so do POSIX's + /// other three: each an `ar` archive of no members. #[test] fn the_c_sysroot_names_the_posix_libraries() { let lib = TempDir::new("libc-empty"); empty_libraries(&lib); - for name in ["m", "pthread", "dl", "rt"] { + for name in ["c", "m", "pthread", "dl", "rt"] { let path = lib.join(format!("lib{name}.a")); assert_eq!(fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())), b"!\n"); } diff --git a/src/sysroot.rs b/src/sysroot.rs index 7ff6098b86a..1112f497de1 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -67,7 +67,7 @@ const RECIPE: &str = "bootstrap stage-0 local rebuild, profile compiler, no LLVM libtoyos_c merged, libraries from the stamp, linked by rust-lld, \ a C sysroot of libc's staticlib, the empty libraries beside it, and headers \ per target, and its C++ runtime built under n2 from the runtimes' sources \ - of the compiler's LLVM; 9"; + of the compiler's LLVM; 10"; /// Every sysroot on this host. pub fn sysroots_dir(rust_dir: &Path) -> PathBuf { diff --git a/tests/common/compile.rs b/tests/common/compile.rs index 1ee15db5ac7..caed4cdfcc8 100644 --- a/tests/common/compile.rs +++ b/tests/common/compile.rs @@ -92,8 +92,13 @@ pub fn compile_c(name: &str) -> Vec { /// /// An image whose lowest address is 0 hides a table reported from the load bias /// instead of the image's first byte; the case prints its lowest `PT_LOAD`, so -/// an entry that stops reaching its case reds it. -const LINK_FLAGS: &[(&str, &[&str])] = &[("205_dl_iterate_phdr_image_base", &["-Wl,--image-base=0x200000"])]; +/// an entry that stops reaching its case reds it. The libraries POSIX has a C +/// compiler take are named by one case, so a C sysroot without one of them +/// builds no corpus. +const LINK_FLAGS: &[(&str, &[&str])] = &[ + ("205_dl_iterate_phdr_image_base", &["-Wl,--image-base=0x200000"]), + ("206_libc_refusals", &["-lc", "-lm", "-lpthread", "-ldl", "-lrt"]), +]; /// Link `objects` into a ToyOS executable through the clang driver — which /// names `ld.lld` and the sysroot's `libtoyos_c.a`, and makes a PIE — and diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 335b2c9eb76..205c125aa62 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -102,9 +102,12 @@ int main(void) { answer = getpwuid_r(0, &pw, buf, sizeof buf, &found); printf("getpwuid_r: %s, entry %s\n", errno_name(answer), found ? "set" : "null"); - /* mmap: no file, no executable page, no empty mapping. */ + /* mmap: no file, private or shared, at its start or a later page; no + executable page; no empty mapping. */ said("mmap a file", mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0) == MAP_FAILED ? -1 : 0); - said("mmap a file shared", mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 4096) == MAP_FAILED ? -1 : 0); + said("mmap a file at a page", mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 4096) == MAP_FAILED ? -1 : 0); + said("mmap a file shared", mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) == MAP_FAILED ? -1 : 0); + said("mmap a file shared at a page", mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 4096) == MAP_FAILED ? -1 : 0); said("mmap executable", mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); said("mmap nothing", mmap(NULL, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); @@ -114,14 +117,26 @@ int main(void) { said("sysconf _SC_GETPW_R_SIZE_MAX", sysconf(_SC_GETPW_R_SIZE_MAX)); said("sysconf 12345", sysconf(12345)); - /* posix_madvise takes every advice and answers an error number. */ + /* posix_madvise takes every advice and answers an error number; madvise + takes Linux's hints and refuses its discard, which would have zeroed + the page. */ + page[0] = 'x'; printf("posix_madvise WILLNEED: %d\n", posix_madvise(page, 4096, POSIX_MADV_WILLNEED)); printf("posix_madvise 99: %s\n", errno_name(posix_madvise(page, 4096, 99))); + said("madvise WILLNEED", madvise(page, 4096, MADV_WILLNEED)); + said("madvise DONTNEED", madvise(page, 4096, MADV_DONTNEED)); + said("madvise off a page", madvise(page + 1, 4096, MADV_WILLNEED)); + said("madvise 99", madvise(page, 4096, 99)); + printf("page after madvise: %c\n", page[0]); - /* Nothing refused was done: the file has its one byte, its mode and no - second name; the page is still writable; the limit is as it was. */ + /* Nothing refused was done: the file has its one byte, its mode through + its name and its descriptor, and no second name; the page is still + writable; the limit is as it was. */ + struct stat by_name; long size = fstat(fd, &st) == 0 ? (long)st.st_size : -1L; - printf("file: %ld bytes; mode %s; second name: %s\n", size, st.st_mode == mode ? "as it was" : "changed", + int named = stat(FILE_PATH, &by_name) == 0; + printf("file: %ld bytes; mode %s through fstat, %s through stat; second name: %s\n", size, + st.st_mode == mode ? "as it was" : "changed", named && by_name.st_mode == mode ? "as it was" : "changed", access(FILE_PATH ".link", F_OK) == 0 ? "made" : "none"); page[0] = 'y'; printf("page after mprotect: %c\n", page[0]); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index c795375e57b..b3629161f0b 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -27,7 +27,9 @@ fcntl 12345: -1, EINVAL getpwnam_r: ENOSYS, entry null getpwuid_r: ENOSYS, entry null mmap a file: -1, ENODEV +mmap a file at a page: -1, ENODEV mmap a file shared: -1, ENODEV +mmap a file shared at a page: -1, ENODEV mmap executable: -1, ENOTSUP mmap nothing: -1, EINVAL sysconf _SC_ARG_MAX: -1, no errno @@ -35,6 +37,11 @@ sysconf _SC_GETPW_R_SIZE_MAX: -1, no errno sysconf 12345: -1, EINVAL posix_madvise WILLNEED: 0 posix_madvise 99: EINVAL -file: 1 bytes; mode as it was; second name: none +madvise WILLNEED: 0, no errno +madvise DONTNEED: -1, ENOSYS +madvise off a page: -1, EINVAL +madvise 99: -1, EINVAL +page after madvise: x +file: 1 bytes; mode as it was through fstat, as it was through stat; second name: none page after mprotect: y limit after getrlimit: 0 0 diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index 082886d6158..91e66f09906 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -8,13 +8,17 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include +#include +#include #include #define DIR_PATH "/tmp/207_libc_names" @@ -151,5 +155,13 @@ int main(void) { printf("logb: %g %g %g\n", logb(0.1), logb(1024.0), logb(0.0)); printf("endian: %s 0x%x 0x%x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", (unsigned)htobe32(0x01020304), (unsigned)le16toh(0x0102)); + + /* The rest of what LLVM reads of these headers, by value and by size. */ + struct sockaddr_un local = { AF_UNIX, "" }; + printf("names: AF_UNIX %d, sockaddr_un %zu, EX_IOERR %d, _POSIX_ARG_MAX %d, page %ld\n", local.sun_family, + sizeof local, EX_IOERR, _POSIX_ARG_MAX, sysconf(_SC_PAGE_SIZE)); + printf("names: SA_ONSTACK|SA_NODEFER|SA_RESETHAND 0x%lx, F_RDLCK %d, F_UNLCK %d, rusage %zu, RLIM_INFINITY %s\n", + (unsigned long)(SA_ONSTACK | SA_NODEFER | SA_RESETHAND), F_RDLCK, F_UNLCK, sizeof(struct rusage), + RLIM_INFINITY == ~0UL ? "every bit" : "not every bit"); return 0; } diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect index 5185175e833..533aa110b23 100644 --- a/tests/testcases/tinycc/207_libc_names.expect +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -19,3 +19,5 @@ strsignal: Segmentation fault; Unknown signal modf(-3.25): -3 and -0.25 logb: -4 10 -inf endian: little 0x4030201 0x102 +names: AF_UNIX 1, sockaddr_un 110, EX_IOERR 74, _POSIX_ARG_MAX 4096, page 4096 +names: SA_ONSTACK|SA_NODEFER|SA_RESETHAND 0xc8000000, F_RDLCK 0, F_UNLCK 2, rusage 144, RLIM_INFINITY every bit diff --git a/toyos-libc-copies/src/memory_refusals.rs b/toyos-libc-copies/src/memory_refusals.rs index 47d49072f88..65af0825ddb 100644 --- a/toyos-libc-copies/src/memory_refusals.rs +++ b/toyos-libc-copies/src/memory_refusals.rs @@ -1,6 +1,7 @@ //! What libc's memory calls refuse before the kernel or the allocator is //! asked: `mmap` a file, executable memory, no bytes, or a fixed place off a -//! page; `posix_madvise` anything but its five. +//! page; `posix_madvise` anything but its five; `madvise` the same, an +//! address off a page, and Linux's discard. use crate::memreq::{self, MapRefusal, MAP_ANONYMOUS, MAP_FIXED, PROT_EXEC, PROT_READ, PROT_WRITE}; @@ -33,3 +34,21 @@ fn posix_madvise_takes_its_five_and_only_them() { let taken: Vec = (-2..8).filter(|&a| memreq::is_advice(a)).collect(); assert_eq!(taken, [0, 1, 2, 3, 4]); } + +#[test] +fn madvise_takes_linuxs_hints_and_refuses_its_discard() { + use memreq::AdviceRefusal::{Discard, Invalid}; + let page = 0x4000_0000; + for (addr, advice, want) in [ + (page, 0, None), + (page, 1, None), + (page, 2, None), + (page, 3, None), + (page, 4, Some(Discard)), + (page, 5, Some(Invalid)), + (page, -1, Some(Invalid)), + (page + 1, 3, Some(Invalid)), + ] { + assert_eq!(memreq::madvise_refusal(addr, advice), want, "madvise({addr:#x}, {advice})"); + } +} diff --git a/userland/libc/include/sys/mman.h b/userland/libc/include/sys/mman.h index 00a24f29103..f0d7067e8bf 100644 --- a/userland/libc/include/sys/mman.h +++ b/userland/libc/include/sys/mman.h @@ -30,10 +30,17 @@ extern "C" { #define POSIX_MADV_WILLNEED 3 #define POSIX_MADV_DONTNEED 4 +#define MADV_NORMAL 0 +#define MADV_RANDOM 1 +#define MADV_SEQUENTIAL 2 +#define MADV_WILLNEED 3 +#define MADV_DONTNEED 4 + void *mmap(void *addr, size_t length, int prot, int flags, int fd, long offset); int munmap(void *addr, size_t length); int msync(void *addr, size_t len, int flags); int posix_madvise(void *addr, size_t len, int advice); +int madvise(void *addr, size_t len, int advice); int mprotect(void *addr, size_t len, int prot); #ifdef __cplusplus diff --git a/userland/libc/src/memreq.rs b/userland/libc/src/memreq.rs index 9a142b79509..ca89db79780 100644 --- a/userland/libc/src/memreq.rs +++ b/userland/libc/src/memreq.rs @@ -38,7 +38,30 @@ pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Op None } -/// Whether `advice` is one of `posix_madvise`'s five. +/// Whether `advice` is one of `posix_madvise`'s five, which `madvise` numbers alike. pub(crate) fn is_advice(advice: i32) -> bool { (0..=4).contains(&advice) } + +/// `MADV_DONTNEED`, which Linux's `madvise` answers by discarding the range, +/// so that it reads back as the file or as zeros. +const MADV_DONTNEED: i32 = 4; + +/// Why `madvise` refuses `advice` at `addr`. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum AdviceRefusal { + /// An address off a page, or advice other than the five `posix_madvise` + /// shares with it: `EINVAL`. + Invalid, + /// `MADV_DONTNEED`'s discard, which nothing here can do: `ENOSYS`. + Discard, +} + +/// Why Linux's `madvise` of `advice` at `addr` is refused, if it is. Its four +/// hints are taken, as its manual lets a kernel ignore each. +pub(crate) fn madvise_refusal(addr: usize, advice: i32) -> Option { + if !addr.is_multiple_of(PAGE) || !is_advice(advice) { + return Some(AdviceRefusal::Invalid); + } + (advice == MADV_DONTNEED).then_some(AdviceRefusal::Discard) +} diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index ac6bfda176e..d9bfbd6c7c4 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -592,6 +592,22 @@ pub unsafe extern "C" fn posix_madvise(_addr: *mut u8, _len: usize, advice: i32) if crate::memreq::is_advice(advice) { 0 } else { EINVAL } } +/// Linux's `madvise` (`memreq::madvise_refusal`). +#[no_mangle] +pub unsafe extern "C" fn madvise(addr: *mut u8, _len: usize, advice: i32) -> i32 { + use crate::memreq::AdviceRefusal; + match crate::memreq::madvise_refusal(addr.addr(), advice) { + None => 0, + Some(refusal) => { + crate::errno::set(match refusal { + AdviceRefusal::Invalid => EINVAL, + AdviceRefusal::Discard => crate::errno::ENOSYS, + }); + -1 + } + } +} + #[no_mangle] pub unsafe extern "C" fn munmap(addr: *mut u8, len: usize) -> i32 { // SAFETY: caller is responsible for addr/len matching a previous mmap From 0674f030bf0bf1ac94ec693e2b65f8b78c40f4e2 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 02:55:49 +0200 Subject: [PATCH 07/12] issues: libc's refusals list madvise's discard, and no longer cite memory.rs madvise refuses MADV_DONTNEED with ENOSYS (206_libc_refusals asserts it), so the refusals issue lists it with what it waits on: no call discards a range's pages and keeps the range mapped. memory.rs holds no refusal since aligned_alloc in a Rust program became std's. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- issues/build/libc-refuses-what-toyos-cannot-yet-answer.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md index c30db81cdb0..68ad3b23a0d 100644 --- a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -7,7 +7,7 @@ opened: 2026-10-01 # libc refuses what ToyOS cannot yet answer These answer failure in their POSIX form and do nothing -(`userland/libc/src/refused.rs`, `posix_io.rs`, `memory.rs`), each asserted by +(`userland/libc/src/refused.rs`, `posix_io.rs`), each asserted by `tests/testcases/tinycc/206_libc_refusals.c`. A program that calls one and carries on without it runs; one that needs it does not. Each waits on what it names: @@ -28,6 +28,8 @@ names: mapped, and a mapping's protection is fixed when it is made. - `mmap` of a file, `ENODEV`, and of executable memory, `ENOTSUP`: the kernel maps neither. +- `madvise`'s `MADV_DONTNEED`, `ENOSYS`: no call discards a range's pages and + keeps it mapped, so it cannot read back as zeros as Linux's does. - `fcntl`: a record lock, `EINVAL`, POSIX's answer for a file that supports no locking; `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and `F_SETOWN`, `ENOSYS`. `F_GETFD` and `F_SETFD` answer 0 and keep nothing: close-on-exec is From a0b9e73e7c50f5a58b6431bc773fc328eb6cb6cc Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 07:17:21 +0200 Subject: [PATCH 08/12] Prose the review removed, and the corpus count the six cases moved #650's second review. - REMOVE: "the libraries POSIX has a C compiler take" (src/libc.rs, its test, tests/common/compile.rs): POSIX's c99 names no -ldl. The test is named for the empty libraries it checks. - REMOVE: the pread issue's story of the removed implementation, and the printf issue's sentence about the corpus. - BLOCKER 6: tests/testcases/LICENSE counts the six files 206 to 208 added under tinycc/: 328 files, 72 not upstream, 71 cases written here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- issues/build/libc-has-no-pread-or-pwrite.md | 6 +----- issues/build/libc-printf-ignores-the-alternate-form-flag.md | 3 +-- src/libc.rs | 6 +----- tests/common/compile.rs | 4 +--- tests/testcases/LICENSE | 4 ++-- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/issues/build/libc-has-no-pread-or-pwrite.md b/issues/build/libc-has-no-pread-or-pwrite.md index dcac3f48c55..4e6a2810cd2 100644 --- a/issues/build/libc-has-no-pread-or-pwrite.md +++ b/issues/build/libc-has-no-pread-or-pwrite.md @@ -6,11 +6,7 @@ opened: 2026-09-30 # libc has no pread or pwrite -libc defines neither `pread` nor `pwrite`. The two it had sought the -descriptor to their offset, read or wrote, and sought back: three calls, -nothing held between them, so a `read`, `write` or `lseek` of the same -descriptor on another thread landing between them read or wrote at the wrong -offset, or had its own moved. The ABI has no positional read or write +libc defines neither `pread` nor `pwrite`. The ABI has no positional read or write (`toyos-abi/src/syscall.rs` has `seek`). With no header declaring `pread`, LLVM's configure finds none, and LLVM reads a file slice with `lseek` and `read` instead (`llvm/lib/Support/Unix/Path.inc`, `readNativeFileSlice`), which diff --git a/issues/build/libc-printf-ignores-the-alternate-form-flag.md b/issues/build/libc-printf-ignores-the-alternate-form-flag.md index 3de881552e1..41bef0cc694 100644 --- a/issues/build/libc-printf-ignores-the-alternate-form-flag.md +++ b/issues/build/libc-printf-ignores-the-alternate-form-flag.md @@ -8,8 +8,7 @@ opened: 2026-10-01 `printf`'s format reader (`userland/libc/src/printf.rs`) steps over `#` and keeps nothing of it, so `%#x` prints `200` where C has `0x200`, `%#o` no -leading 0, and `%#g` drops the trailing zeros C keeps. The corpus cases that -print a hex value spell the `0x` out for it. +leading 0, and `%#g` drops the trailing zeros C keeps. **Exit**: `#` takes C's alternate form for every conversion that has one, and a corpus case prints each. diff --git a/src/libc.rs b/src/libc.rs index b7728248db8..53ee54e1f74 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -120,8 +120,6 @@ pub fn build_c(root: &Path, toolchain: &Path, target_dir: &Path, arch: Arch) { crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.join("include")); } -/// The libraries POSIX has a C compiler take, `-lc` and `-lm` among them, whose -/// functions are all `libtoyos_c.a`'s. const EMPTY_LIBRARIES: [&str; 5] = ["c", "m", "pthread", "dl", "rt"]; /// Put each of [`EMPTY_LIBRARIES`] in `lib` as an archive of no members: a @@ -295,10 +293,8 @@ mod tests { use super::*; use toyos_tmpdir::TempDir; - /// **`-lc` and `-lm` find a library in the C sysroot**, and so do POSIX's - /// other three: each an `ar` archive of no members. #[test] - fn the_c_sysroot_names_the_posix_libraries() { + fn the_c_sysroot_names_the_empty_libraries() { let lib = TempDir::new("libc-empty"); empty_libraries(&lib); for name in ["c", "m", "pthread", "dl", "rt"] { diff --git a/tests/common/compile.rs b/tests/common/compile.rs index caed4cdfcc8..bdd005b3ccb 100644 --- a/tests/common/compile.rs +++ b/tests/common/compile.rs @@ -92,9 +92,7 @@ pub fn compile_c(name: &str) -> Vec { /// /// An image whose lowest address is 0 hides a table reported from the load bias /// instead of the image's first byte; the case prints its lowest `PT_LOAD`, so -/// an entry that stops reaching its case reds it. The libraries POSIX has a C -/// compiler take are named by one case, so a C sysroot without one of them -/// builds no corpus. +/// an entry that stops reaching its case reds it. const LINK_FLAGS: &[(&str, &[&str])] = &[ ("205_dl_iterate_phdr_image_base", &["-Wl,--image-base=0x200000"]), ("206_libc_refusals", &["-lc", "-lm", "-lpthread", "-ldl", "-lrt"]), diff --git a/tests/testcases/LICENSE b/tests/testcases/LICENSE index 224dc5c6603..94e8eee4c7b 100644 --- a/tests/testcases/LICENSE +++ b/tests/testcases/LICENSE @@ -21,9 +21,9 @@ against tinycc upstream at 64552b3faa39ee7948a9ea21bfcc11045b90c70d (repo.or.cz/tinycc.git, 2026-08-05), allowing for the `-` → `_` renames this project made to some filenames. - tinycc/ 322 files: 239 byte-identical to tinycc's `tests/tests2`, + tinycc/ 328 files: 239 byte-identical to tinycc's `tests/tests2`, 17 tinycc files this project modified, - 66 not upstream at all — 65 cases written here, plus + 72 not upstream at all — 71 cases written here, plus `fred.txt`, which is output `40_stdio.c` writes when it runs and which was committed by accident. From 08516be071248c9478434defa716a83dc08030ad Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 07:17:45 +0200 Subject: [PATCH 09/12] libc: fcntl, dladdr, readdir and logb as the second review has them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #650's second review: every BLOCKER, NOTE and REMOVE it raised about libc, toyos-elf, toyos-abi and the fork. BLOCKER 1, fcntl's F_DUPFD with -1 emptied the handle table. clang passes an int variadic argument with `movl`, which zeroes the register's upper half (measured on 206's object: `movl $0xffffffff, %edx`), so -1 arrived as 4294967295, passed `arg >= 0`, and the dup loop ran the table to full. fcntl's decision is now `fdreq`, pure and host-tested (descriptor_requests): F_DUPFD and F_SETFD read their int from the lower half of the register, and F_DUPFD refuses a floor that is negative or at or above the slots a table has (POSIX's EINVAL for a floor at or above {OPEN_MAX}). The walk to the floor closes each duplicate below it at once, so it holds at most one: a slot at its first generation closed is the next dup's one generation on, numbered past every slot. The host test walks every floor over a table numbered as the kernel numbers one, unchurned and churned. NOTE, F_GETFD and F_SETFD: each descriptor's close-on-exec mark is kept, by number, for stage 3's spawn; open's O_CLOEXEC sets it, close and dup2's answer clear it. 206 reads each back, and F_DUPFD's duplicate without it. NOTE, set_errno's catch-all EINVAL is filed: issues/build/libc-answers-einval-for-every-error-it-does-not-name.md. BLOCKER 2, elfsym.rs was a second reader of ELF dynamic tables. glibc's dladdr choice is `SymTab::dladdr` in toyos-elf, tested there (dladdr_names_the_symbol_glibc_names); `Sym` reads st_other and `is_exported` refuses a hidden or internal symbol; the symbol count where no section header says it is `Dynamic::sym_count`, read by the kernel's executable loader and by libc alike, so libc's DT_HASH arm goes. elfsym.rs is the image walk alone (program headers by `ProgramHeader::parse`, each table a slice of the PT_LOAD that maps it). libc depends on toyos-elf, so the sysroot key reads toyos-elf's sources and manifest. BLOCKER 3, symlink asked whether the name existed and then called SYS_SYMLINK, which displaces what the name holds: check-then-act, and the contention it hides is where LLVM's LockFileManager takes its lock through EEXIST. Making the kernel refuse an existing name changes a syscall, which is the owner's to discuss, so symlink refuses ENOSYS and the pre-check goes; the filesystem issue's exit names the kernel's refusal, libc's call with nothing asked first, and that consumer. BLOCKER 4, logb(±0) set no ERANGE though math.h declares MATH_ERRNO. fparts::logb answers a zero `Err(Pole)`, so its caller cannot forget it; math.rs's wrapper sets ERANGE and answers -inf; the host differential holds a zero to the pole and every other input to the host's bits; 207 reads errno. NOTE, readdir's listing had two decoders and its kind bytes no declaration: toyos-abi declares DIRENT_FILE and DIRENT_DIR beside syscall::readdir and decodes a listing (syscall::dirent, host-tested there); the kernel's encoder writes those bytes; libc and std (fork 3f6050fc829, merged into the fork's main as dc36cdce3b1, which the rust pin now names) both read with it. NOTE, opendir answered EAGAIN when the listing grew between its two asks: `listing::whole` asks again at the length the kernel answered until one fits, for opendir and for SYS_QUERY_MODULES alike, host-tested with an answer that grows. `listing::d_name` decides readdir's EOVERFLOW, host-tested at 255 and 256 bytes. DIR holds its listing as a Vec. NOTE, mmap with neither MAP_PRIVATE nor MAP_SHARED, or both, refuses EINVAL (memreq, with rows in memory_refusals). NOTE, readlink refuses a size above isize::MAX EINVAL, which 207 asks. NOTE, sysconf(_SC_PAGESIZE) answers memreq's page. NOTE, std's C allocator sets no errno: filed with libc's own malloc, calloc and realloc, which set none either, as issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md, since setting it from std puts a libc symbol in every Rust program's link. BLOCKER 5: c_aligned_alloc allocates, fills and frees at alignments 1 and 8 too, below the 16 bytes a block's header takes. REMOVE: toyos-libc-copies' restated list of what it tests and its manifest's restated description; refused.rs's false "each sets ENOSYS"; c_aligned_alloc's "grown past the arena"; the refusals issue's sentence on programs that carry on, and the symlink issue's clause on 207's directory. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- Cargo.lock | 4 + ...einval-for-every-error-it-does-not-name.md | 21 ++ ...bc-refuses-what-toyos-cannot-yet-answer.md | 15 +- ...ators-set-no-errno-where-posix-has-them.md | 22 +++ ...mp-displaces-its-name-and-lists-nowhere.md | 17 +- kernel/src/loader/mod.rs | 33 ++-- kernel/src/syscall/fs.rs | 2 +- rust | 2 +- src/sysroot.rs | 14 +- tests/testcases/tinycc/206_libc_refusals.c | 25 ++- .../testcases/tinycc/206_libc_refusals.expect | 8 +- tests/testcases/tinycc/207_libc_names.c | 33 ++-- tests/testcases/tinycc/207_libc_names.expect | 11 +- .../src/bin/c_aligned_alloc.rs | 5 +- toyos-abi/src/syscall.rs | 82 ++++++++ toyos-elf/src/dynamic.rs | 19 +- toyos-elf/src/sym.rs | 46 ++++- toyos-elf/tests/tables.rs | 91 +++++++++ toyos-libc-copies/Cargo.toml | 16 +- toyos-libc-copies/src/descriptor_requests.rs | 113 +++++++++++ toyos-libc-copies/src/dladdr_symbols.rs | 130 +++++-------- toyos-libc-copies/src/fparts_differential.rs | 10 +- toyos-libc-copies/src/lib.rs | 11 +- toyos-libc-copies/src/listing_reader.rs | 83 ++++---- toyos-libc-copies/src/memory_refusals.rs | 13 +- userland/libc/Cargo.lock | 5 + userland/libc/Cargo.toml | 1 + userland/libc/src/elfsym.rs | 137 ++++--------- userland/libc/src/fdreq.rs | 73 +++++++ userland/libc/src/fparts.rs | 19 +- userland/libc/src/lib.rs | 1 + userland/libc/src/link.rs | 15 +- userland/libc/src/listing.rs | 52 ++--- userland/libc/src/math.rs | 6 +- userland/libc/src/memreq.rs | 13 +- userland/libc/src/misc.rs | 2 +- userland/libc/src/posix_io.rs | 181 +++++++----------- userland/libc/src/refused.rs | 11 +- 38 files changed, 845 insertions(+), 497 deletions(-) create mode 100644 issues/build/libc-answers-einval-for-every-error-it-does-not-name.md create mode 100644 issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md create mode 100644 toyos-libc-copies/src/descriptor_requests.rs create mode 100644 userland/libc/src/fdreq.rs diff --git a/Cargo.lock b/Cargo.lock index f3c87c3c550..78a377303ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,6 +1112,10 @@ dependencies = [ [[package]] name = "toyos-libc-copies" version = "0.1.0" +dependencies = [ + "toyos-abi", + "toyos-elf", +] [[package]] name = "toyos-logstream" diff --git a/issues/build/libc-answers-einval-for-every-error-it-does-not-name.md b/issues/build/libc-answers-einval-for-every-error-it-does-not-name.md new file mode 100644 index 00000000000..cd47f2cb168 --- /dev/null +++ b/issues/build/libc-answers-einval-for-every-error-it-does-not-name.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# libc answers EINVAL for every error it does not name + +Read from the code, not run. `set_errno` (`userland/libc/src/posix_io.rs`) +sends `Unknown`, `BadAddress`, `ResourceExhausted` and `NotSupported` through +`_ => EINVAL`, so each reaches a C caller as `EINVAL`, which POSIX spells +"invalid argument". A full handle table (`kernel/src/object/handle.rs`) +answers `dup`, `open` and `pipe` `EINVAL` where POSIX has `EMFILE`, a full +bcachefs volume (`kernel/src/bcachefs_adapter.rs`) `write` `EINVAL` where it +has `ENOSPC`, and an operation a filesystem lacks `EINVAL` where it has +`ENOTSUP`. A refusal that is truly `EINVAL` reads the same as each of them, so +a case asserting `EINVAL` passes whichever happened. + +**Exit**: `set_errno` matches every `SyscallError` by name, with no catch-all, +each call that can meet `ResourceExhausted` answering the POSIX errno its own +page names for it, and a host test holds each call's mapping. diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md index 68ad3b23a0d..3e910d30e8c 100644 --- a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -8,11 +8,11 @@ opened: 2026-10-01 These answer failure in their POSIX form and do nothing (`userland/libc/src/refused.rs`, `posix_io.rs`), each asserted by -`tests/testcases/tinycc/206_libc_refusals.c`. A program that calls one and -carries on without it runs; one that needs it does not. Each waits on what it -names: +`tests/testcases/tinycc/206_libc_refusals.c`. Each waits on what it names: - `link`, `ENOSYS`: no call gives a file a second name. +- `symlink`, `ENOSYS`: `SYS_SYMLINK` displaces what its name held + (`issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md`). - `chmod` and `fchmod`, `ENOSYS`: a file has no mode bits to set. - `statvfs` and `fstatvfs`, `ENOSYS`: no call answers a filesystem's size or free space. @@ -32,11 +32,10 @@ names: keeps it mapped, so it cannot read back as zeros as Linux's does. - `fcntl`: a record lock, `EINVAL`, POSIX's answer for a file that supports no locking; `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and `F_SETOWN`, - `ENOSYS`. `F_GETFD` and `F_SETFD` answer 0 and keep nothing: close-on-exec is - the descriptor table of stage 3 of - `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`. - `F_DUPFD` answers a duplicate at or above its argument, the first the kernel - hands `dup`, where POSIX has the lowest free number. + `ENOSYS`. `F_DUPFD` answers a duplicate at or above its argument, the first + the kernel hands `dup`, where POSIX has the lowest free number, and refuses + `EINVAL` an argument at or above the slots a handle table has + (`RawHandle::MAX_SLOTS`). Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no call replacing a process's image; `setsid` and `getsid`, no POSIX session; diff --git a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md new file mode 100644 index 00000000000..9dc1693e7ff --- /dev/null +++ b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# The C allocators set no errno where POSIX has them + +Read from the code, not run. POSIX.1-2024 has `malloc`, `calloc`, `realloc` +and `aligned_alloc` set `ENOMEM` when they answer null for want of memory, and +`aligned_alloc` set `EINVAL` for an alignment it does not support. + +- In a Rust program, C code allocates through std's C allocator + (`rust/library/std/src/sys/pal/toyos/mod.rs`, `c_allocator`), which sets + `errno` in none of the four. `errno` is libc's (`userland/libc/src/errno.rs`), + and std names no libc symbol: setting it from std puts `__errno_location` in + every Rust program's link. +- In a C program, libc's own allocator (`userland/libc/src/memory.rs`) sets it + in `aligned_alloc` alone. + +**Exit**: each of the four sets `errno` where POSIX has it, in a C program and +in a Rust one, and a C call in each reads `ENOMEM` and `EINVAL` back. diff --git a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md index d66943c4023..7ba44e0f727 100644 --- a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md +++ b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -8,12 +8,13 @@ opened: 2026-10-01 Read from the code, not run. The kernel's `/tmp` (`kernel/src/tmpfs.rs`) makes a link over whatever file its name held, where POSIX's `symlink` refuses -a name that exists `EEXIST`; libc asks first (`userland/libc/src/posix_io.rs`, -`symlink`), so a file another process makes between the question and the call -is displaced. Its `list` answers files alone, so no `readdir` of a directory -shows a link it holds, and -`tests/testcases/tinycc/207_libc_names.c` lists a directory no link is in. +a name that exists `EEXIST`, so libc's `symlink` refuses `ENOSYS` +(`userland/libc/src/refused.rs`). Its `list` answers files alone, so no +`readdir` of a directory shows a link it holds. -**Exit**: `SYS_SYMLINK` over a name that exists is refused `AlreadyExists`, -and a listing of `/tmp` names each link it holds, each asserted by a guest -case. +**Exit**: `SYS_SYMLINK` over a name that exists is refused `AlreadyExists` +under the lock that makes the link, and a listing of `/tmp` names each link it +holds, each asserted by a guest case. libc's `symlink` calls it with nothing +asked first and answers that refusal `EEXIST`, which is how LLVM's +`LockFileManager` takes its lock (`create_link`, `::symlink` in +`llvm/lib/Support/Unix/Path.inc`). diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index f29c16ca715..40c60ef88f6 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -40,7 +40,7 @@ use toyos_abi::syscall::SyscallError; use toyos_elf::section::SectionTable; use toyos_elf::sym::{self, SymTab}; use toyos_elf::rela::{FillLattice, Rules, FILL_GRANULE}; -use toyos_elf::{GnuHash, Layout, RelocError, TlsSegment}; +use toyos_elf::{Layout, RelocError, TlsSegment}; const USER_STACK_SIZE: usize = 4 * PAGE_2M as usize; // 8 MB @@ -295,32 +295,25 @@ fn read_exe_tables( Ok(ExeTables { needed, dynstr, dynsym, relas }) } -/// `.dynsym`'s entry count, from `.gnu.hash` if present, else the `DT_SYMTAB`–`DT_STRTAB` gap. +/// `.dynsym`'s entry count, by the rule `Dynamic::sym_count` states. fn exe_sym_count( backing: &dyn crate::file_backing::FileBacking, layout: &Layout, dyn_info: &toyos_elf::Dynamic, path: &str, ) -> Result { - if let Some(vaddr) = dyn_info.gnu_hash { - let off = file_off(layout, path, "DT_GNU_HASH", vaddr)?; - let len = layout - .file_bytes_from(vaddr) - .unwrap_or(0) - .min(MAX_GNU_HASH_BYTES) as usize; - let data = read_file_range(backing, off, len); - if let Some(count) = GnuHash::parse(&data).and_then(|h| h.sym_count()) { - return Ok(count); + let gnu_hash = match dyn_info.gnu_hash { + Some(vaddr) => { + let off = file_off(layout, path, "DT_GNU_HASH", vaddr)?; + let len = layout.file_bytes_from(vaddr).unwrap_or(0).min(MAX_GNU_HASH_BYTES) as usize; + Some(read_file_range(backing, off, len)) } - log!("spawn: {}: .gnu.hash does not describe a symbol count in {} bytes", path, len); - } - // Adjacent in every linker-produced layout, so the gap between them is the table size. - match (dyn_info.symtab, dyn_info.strtab) { - (Some(symtab), Some(strtab)) if strtab > symtab => { - Ok(((strtab - symtab) / sym::ENTRY_SIZE as u64) as usize) - } - _ => Ok(0), - } + None => None, + }; + Ok(dyn_info.sym_count(gnu_hash.as_deref()).unwrap_or_else(|gap| { + log!("spawn: {}: .gnu.hash does not describe a symbol count in its {} bytes", path, gnu_hash.as_ref().map_or(0, |t| t.len())); + gap + })) } /// `.rela.dyn` located through section headers, for a file with no `PT_DYNAMIC`. diff --git a/kernel/src/syscall/fs.rs b/kernel/src/syscall/fs.rs index 90af32bc9f1..d2d195d2aae 100644 --- a/kernel/src/syscall/fs.rs +++ b/kernel/src/syscall/fs.rs @@ -57,7 +57,7 @@ pub(super) fn sys_readdir(path: &str, out: &mut UserBytesMut) -> u64 { for (name, size) in &entries { let is_dir = name.ends_with('/'); let clean_name = if is_dir { &name[..name.len() - 1] } else { name.as_str() }; - out.write_at(pos, &[if is_dir { 2 } else { 1 }]); + out.write_at(pos, &[if is_dir { DIRENT_DIR } else { DIRENT_FILE }]); pos += 1; out.write_at(pos, clean_name.as_bytes()); pos += clean_name.len(); diff --git a/rust b/rust index 9151571cae9..3f6050fc829 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit 9151571cae9014619926872b12fcebf3b0a3c14a +Subproject commit 3f6050fc8293153ce890879442735fdaeded9c63 diff --git a/src/sysroot.rs b/src/sysroot.rs index e7d00966114..4fee89ae830 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -3,7 +3,7 @@ //! //! **A sysroot is a function of its key.** The key ([`key`]) is the identity //! (`src/identity.rs`, so a comment is no change) of everything a sysroot is -//! built from: the three trees std and `libtoyos_c.a` compile +//! built from: the trees std and `libtoyos_c.a` compile //! ([`SYSROOT_SOURCES`]), the std fork's `library/` and `src/bootstrap/` in the //! checkout that builds it, and the compiler that builds it. `rust/build/ //! sysroots//` is a whole toolchain — the compiler's files cloned from its @@ -49,13 +49,13 @@ use crate::identity; use crate::toolchain::{self, host_triple, Owner, GUEST_TARGETS}; /// The per-worktree sources that end up inside a sysroot: std links `toyos-abi` -/// and `toyos`, and `libtoyos_c.a` is `userland/libc`. -pub const SYSROOT_SOURCES: [&str; 4] = - ["toyos-abi/src", "toyos/src", "userland/libc/src", "userland/libc/include"]; +/// and `toyos`, and `libtoyos_c.a` is `userland/libc` with `toyos-elf`. +pub const SYSROOT_SOURCES: [&str; 5] = + ["toyos-abi/src", "toyos/src", "toyos-elf/src", "userland/libc/src", "userland/libc/include"]; /// Their manifests, whose features and versions decide the same build. -pub(crate) const SYSROOT_MANIFESTS: [&str; 3] = - ["toyos-abi/Cargo.toml", "toyos/Cargo.toml", "userland/libc/Cargo.toml"]; +pub(crate) const SYSROOT_MANIFESTS: [&str; 4] = + ["toyos-abi/Cargo.toml", "toyos/Cargo.toml", "toyos-elf/Cargo.toml", "userland/libc/Cargo.toml"]; /// The file a finished sysroot carries last, naming what it was built from. /// A directory without it is a build that did not finish. @@ -689,7 +689,7 @@ mod tests { fs::write(path, text).unwrap(); } - /// A worktree's three trees, a fork checkout and a compiler, laid out the + /// A worktree's trees, a fork checkout and a compiler, laid out the /// way the key reads them. fn keyed(base: &Path) -> (PathBuf, PathBuf, PathBuf) { let root = base.join("root"); diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 205c125aa62..31e5af953a6 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -67,6 +67,7 @@ int main(void) { said("gethostname", gethostname(buf, sizeof buf)); said("uname", uname(&uts)); said("link", link(FILE_PATH, FILE_PATH ".link")); + said("symlink", symlink(FILE_PATH, FILE_PATH ".symlink")); said("fchown", fchown(fd, 0, 0)); said("chmod", chmod(FILE_PATH, 0600)); said("fchmod", fchmod(fd, 0600)); @@ -80,17 +81,27 @@ int main(void) { said("fcntl F_SETLK", fcntl(fd, F_SETLK, &lock)); said("fcntl F_SETLKW", fcntl(fd, F_SETLKW, &lock)); said("fcntl F_GETLK", fcntl(fd, F_GETLK, &lock)); - said("fcntl F_DUPFD -1", fcntl(fd, F_DUPFD, -1)); - said("fcntl F_DUPFD_CLOEXEC", fcntl(fd, F_DUPFD_CLOEXEC, 10)); - /* F_DUPFD itself duplicates: its answer is at or above its argument, and - fstat reads the file's one byte through it. */ + /* Close-on-exec is kept per descriptor. F_DUPFD, the first call to + duplicate anything, answers at or above its argument a descriptor + fstat reads the file's one byte through, and not closed on exec. */ + printf("fcntl F_GETFD: %d\n", fcntl(fd, F_GETFD)); + int set = fcntl(fd, F_SETFD, FD_CLOEXEC); + printf("fcntl F_SETFD FD_CLOEXEC: %d; F_GETFD: %d\n", set, fcntl(fd, F_GETFD)); int duplicate = fcntl(fd, F_DUPFD, 10); struct stat through; int read_back = duplicate >= 0 && fstat(duplicate, &through) == 0 && through.st_size == 1; - printf("fcntl F_DUPFD 10: %s; fstat of it: %s\n", duplicate >= 10 ? "10 or above" : "below 10", - read_back ? "the file's one byte" : "not the file"); + printf("fcntl F_DUPFD 10: %s; fstat of it: %s; F_GETFD of it: %d\n", duplicate >= 10 ? "10 or above" : "below 10", + read_back ? "the file's one byte" : "not the file", fcntl(duplicate, F_GETFD)); if (duplicate >= 0) close(duplicate); + set = fcntl(fd, F_SETFD, 0); + printf("fcntl F_SETFD 0: %d; F_GETFD: %d\n", set, fcntl(fd, F_GETFD)); + int cloexec = open(FILE_PATH, O_RDONLY | O_CLOEXEC); + printf("open O_CLOEXEC; F_GETFD: %d\n", fcntl(cloexec, F_GETFD)); + close(cloexec); + said("fcntl F_DUPFD -1", fcntl(fd, F_DUPFD, -1)); + said("fcntl F_DUPFD 4096", fcntl(fd, F_DUPFD, 4096)); + said("fcntl F_DUPFD_CLOEXEC", fcntl(fd, F_DUPFD_CLOEXEC, 10)); said("fcntl F_GETFL", fcntl(fd, F_GETFL)); said("fcntl F_SETFL", fcntl(fd, F_SETFL, O_NONBLOCK)); said("fcntl 12345", fcntl(fd, 12345)); @@ -137,7 +148,7 @@ int main(void) { int named = stat(FILE_PATH, &by_name) == 0; printf("file: %ld bytes; mode %s through fstat, %s through stat; second name: %s\n", size, st.st_mode == mode ? "as it was" : "changed", named && by_name.st_mode == mode ? "as it was" : "changed", - access(FILE_PATH ".link", F_OK) == 0 ? "made" : "none"); + access(FILE_PATH ".link", F_OK) == 0 || access(FILE_PATH ".symlink", F_OK) == 0 ? "made" : "none"); page[0] = 'y'; printf("page after mprotect: %c\n", page[0]); printf("limit after getrlimit: %lu %lu\n", (unsigned long)limit.rlim_cur, (unsigned long)limit.rlim_max); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index b3629161f0b..67836898eb6 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -5,6 +5,7 @@ getsid: -1, ENOSYS gethostname: -1, ENOSYS uname: -1, ENOSYS link: -1, ENOSYS +symlink: -1, ENOSYS fchown: -1, ENOSYS chmod: -1, ENOSYS fchmod: -1, ENOSYS @@ -18,9 +19,14 @@ realpath: -1, ENOSYS fcntl F_SETLK: -1, EINVAL fcntl F_SETLKW: -1, EINVAL fcntl F_GETLK: -1, EINVAL +fcntl F_GETFD: 0 +fcntl F_SETFD FD_CLOEXEC: 0; F_GETFD: 1 +fcntl F_DUPFD 10: 10 or above; fstat of it: the file's one byte; F_GETFD of it: 0 +fcntl F_SETFD 0: 0; F_GETFD: 0 +open O_CLOEXEC; F_GETFD: 1 fcntl F_DUPFD -1: -1, EINVAL +fcntl F_DUPFD 4096: -1, EINVAL fcntl F_DUPFD_CLOEXEC: -1, ENOSYS -fcntl F_DUPFD 10: 10 or above; fstat of it: the file's one byte fcntl F_GETFL: -1, ENOSYS fcntl F_SETFL: -1, ENOSYS fcntl 12345: -1, EINVAL diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index 91e66f09906..591be91a019 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -1,5 +1,5 @@ -/* What libc does for the names LLVM builds against: creat, symlink and - readlink as POSIX has them, each read back; a directory listed with the name +/* What libc does for the names LLVM builds against: creat and readlink as + POSIX has them, each read back; a directory listed with the name and kind of each entry; dladdr naming the image and the exported symbol an address lies in; and strnlen, strsignal, modf, logb and the conversions. */ @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,6 @@ #define DIR_PATH "/tmp/207_libc_names" #define TARGET DIR_PATH "/target" -#define LINK DIR_PATH "/link" #define LISTED DIR_PATH "/listed" /* The image's test library, which exports tls_get_label. */ #define LIB "/system/lib/libtls_lib.so" @@ -55,24 +55,16 @@ static void links(void) { printf("creat %s failed\n", TARGET); return; } - printf("symlink: %d\n", symlink(TARGET, LINK)); - refused("symlink over the link", symlink(TARGET, LINK)); - refused("symlink over a file", symlink(LINK, TARGET)); + fd = open(TARGET, O_RDONLY); + ssize_t n = fd < 0 ? -1 : read(fd, buf, sizeof buf); + printf("read what creat wrote: \"%.*s\"\n", n > 0 ? (int)n : 0, buf); + if (fd >= 0) + close(fd); - ssize_t n = readlink(LINK, buf, sizeof buf); - printf("readlink: %zd, \"%.*s\"\n", n, n > 0 ? (int)n : 0, buf); - memset(buf, '#', sizeof buf); - n = readlink(LINK, buf, 4); - printf("readlink into 4: %zd, \"%.5s\"\n", n, buf); refused("readlink of a file", readlink(TARGET, buf, sizeof buf)); refused("readlink of nothing", readlink(DIR_PATH "/none", buf, sizeof buf)); - refused("readlink into 0", readlink(LINK, buf, 0)); - - fd = open(LINK, O_RDONLY); - n = fd < 0 ? -1 : read(fd, buf, sizeof buf); - printf("read through the link: \"%.*s\"\n", n > 0 ? (int)n : 0, buf); - if (fd >= 0) - close(fd); + refused("readlink into 0", readlink(TARGET, buf, 0)); + refused("readlink into SIZE_MAX", readlink(DIR_PATH "/none", buf, SIZE_MAX)); } static void listing(void) { @@ -152,7 +144,10 @@ int main(void) { double whole; double part = modf(-3.25, &whole); printf("modf(-3.25): %g and %g\n", whole, part); - printf("logb: %g %g %g\n", logb(0.1), logb(1024.0), logb(0.0)); + printf("logb: %g %g\n", logb(0.1), logb(1024.0)); + errno = 0; + double pole = logb(0.0); + printf("logb(0): %g, %s\n", pole, errno == ERANGE ? "ERANGE" : errno_name(errno)); printf("endian: %s 0x%x 0x%x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", (unsigned)htobe32(0x01020304), (unsigned)le16toh(0x0102)); diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect index 533aa110b23..e9cab6abf9a 100644 --- a/tests/testcases/tinycc/207_libc_names.expect +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -1,12 +1,8 @@ -symlink: 0 -symlink over the link: -1, EEXIST -symlink over a file: -1, EEXIST -readlink: 26, "/tmp/207_libc_names/target" -readlink into 4: 4, "/tmp#" +read what creat wrote: "hello" readlink of a file: -1, EINVAL readlink of nothing: -1, ENOENT readlink into 0: -1, EINVAL -read through the link: "hello" +readlink into SIZE_MAX: -1, EINVAL entry: a DT_UNKNOWN entry: b DT_UNKNOWN entry: sub DT_DIR @@ -17,7 +13,8 @@ dladdr(tls_get_label + 1): 1, /system/lib/libtls_lib.so, tls_get_label at the sy strnlen: 3 2 0 strsignal: Segmentation fault; Unknown signal modf(-3.25): -3 and -0.25 -logb: -4 10 -inf +logb: -4 10 +logb(0): -inf, ERANGE endian: little 0x4030201 0x102 names: AF_UNIX 1, sockaddr_un 110, EX_IOERR 74, _POSIX_ARG_MAX 4096, page 4096 names: SA_ONSTACK|SA_NODEFER|SA_RESETHAND 0xc8000000, F_RDLCK 0, F_UNLCK 2, rusage 144, RLIM_INFINITY every bit diff --git a/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs b/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs index b7bd6b0d636..ea39b59f952 100644 --- a/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs +++ b/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs @@ -31,7 +31,8 @@ unsafe fn holds(block: *const u8, len: usize) -> bool { fn main() { let mut blocks = Vec::new(); - for align in [64usize, 4096] { + // Below the 16 bytes in front of a block that hold its layout, and above. + for align in [1usize, 8, 64, 4096] { // SAFETY: C11's aligned_alloc, with a power-of-two alignment. let block = unsafe { aligned_alloc(align, 100) }; assert!(!block.is_null(), "aligned_alloc({align}, 100) answered null"); @@ -42,8 +43,6 @@ fn main() { blocks.push((align, block)); } - // Grown past the arena its first allocation came from, so dlmalloc moves - // it: the old block is released at the layout its header names. let (align, block) = blocks.pop().expect("the 4096 block"); // SAFETY: a block aligned_alloc answered, not yet released. let grown = unsafe { realloc(block, MIB) }; diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 1ec384059a9..d27fa343893 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1083,6 +1083,37 @@ pub fn readdir(path: &[u8], buf: &mut [u8]) -> Result { } } +/// The kind byte a [`readdir`] entry opens with: a file. +pub const DIRENT_FILE: u8 = 1; +/// The kind byte a [`readdir`] entry opens with: a directory. +pub const DIRENT_DIR: u8 = 2; + +/// One entry of a [`readdir`] listing: its kind byte, its name, a NUL, and its +/// size as eight bytes, little-endian. +pub struct Dirent<'a> { + pub is_dir: bool, + pub name: &'a [u8], + pub size: u64, +} + +/// The entry at `*at` in a whole [`readdir`] listing, with `*at` moved past +/// it; `None` once `*at` is at its end. A listing of any other shape is a +/// kernel that broke this ABI, and panics. +pub fn dirent<'a>(listing: &'a [u8], at: &mut usize) -> Option> { + let rest = listing.get(*at..).filter(|r| !r.is_empty())?; + let is_dir = match rest[0] { + DIRENT_FILE => false, + DIRENT_DIR => true, + kind => panic!("SYS_READDIR answered an entry of kind {kind}"), + }; + let len = rest[1..].iter().position(|&b| b == 0).expect("SYS_READDIR answered a name with no NUL"); + let size = rest + .get(len + 2..len + 10) + .expect("SYS_READDIR answered an entry with no size"); + *at += len + 10; + Some(Dirent { is_dir, name: &rest[1..1 + len], size: u64::from_le_bytes(size.try_into().unwrap()) }) +} + /// Delete a file or directory. pub fn delete(path: &[u8]) -> Result<(), SyscallError> { check_unit(syscall(SYS_DELETE, path.as_ptr() as u64, path.len() as u64, 0, 0)) @@ -2517,4 +2548,55 @@ mod tests { } assert_eq!(PciId::from_wire(1 << 32), None); } + + /// A listing of `entries`, each a name, whether it is a directory and a + /// size, encoded as `sys_readdir` (`kernel/src/syscall/fs.rs`) encodes it. + fn listing(entries: &[(&str, bool, u64)]) -> std::vec::Vec { + let mut out = std::vec::Vec::new(); + for (name, is_dir, size) in entries { + out.push(if *is_dir { 2 } else { 1 }); + out.extend_from_slice(name.as_bytes()); + out.push(0); + out.extend_from_slice(&size.to_le_bytes()); + } + out + } + + fn read_listing(bytes: &[u8]) -> std::vec::Vec<(std::string::String, bool, u64)> { + let mut at = 0; + let mut out = std::vec::Vec::new(); + while let Some(entry) = dirent(bytes, &mut at) { + out.push((std::string::String::from_utf8(entry.name.to_vec()).unwrap(), entry.is_dir, entry.size)); + } + assert_eq!(at, bytes.len(), "the reader stopped short of the listing's end"); + out + } + + #[test] + fn every_dirent_is_read_back_whatever_its_size_holds() { + // Sizes whose bytes are NULs, kind bytes and letters: a reader that + // does not step over all eight takes them for names. + let entries = [ + ("a", false, 0), + ("dir", true, 0x0201_0000_0000_0000), + ("b.txt", false, 0x6162_6300_0102_0304), + ("", false, u64::MAX), + ("last", true, 17), + ]; + let want: std::vec::Vec<_> = entries.iter().map(|(n, d, s)| ((*n).into(), *d, *s)).collect(); + assert_eq!(read_listing(&listing(&entries)), want); + assert!(read_listing(&[]).is_empty()); + } + + #[test] + #[should_panic(expected = "of kind 3")] + fn a_dirent_of_another_kind_is_a_broken_kernel() { + read_listing(&[3, b'x', 0, 0, 0, 0, 0, 0, 0, 0, 0]); + } + + #[test] + #[should_panic(expected = "no size")] + fn a_short_dirent_is_a_broken_kernel() { + read_listing(&[1, b'x', 0, 0, 0]); + } } diff --git a/toyos-elf/src/dynamic.rs b/toyos-elf/src/dynamic.rs index 8422f05d95f..361572afbe7 100644 --- a/toyos-elf/src/dynamic.rs +++ b/toyos-elf/src/dynamic.rs @@ -5,8 +5,9 @@ //! and the loader has to tell "the file did not say" from "the file said //! zero", because those two get different treatment at every use site. +use crate::gnu_hash::GnuHash; use crate::layout::{Extent, ImageRange}; -use crate::{read, Error}; +use crate::{read, sym, Error}; /// A table named by a (location, size) pair of tags. /// @@ -85,6 +86,22 @@ impl Dynamic { Table::from_tags(self.strtab, self.strsz) } + /// `.dynsym`'s entry count where no section header says it: what the + /// `.gnu.hash` table describes, `gnu_hash` being its bytes from + /// `DT_GNU_HASH` on, or else the gap from `DT_SYMTAB` to `DT_STRTAB`, + /// adjacent in every layout a linker makes. A `.gnu.hash` that describes + /// no count is `Err`, carrying the gap. + pub fn sym_count(&self, gnu_hash: Option<&[u8]>) -> Result { + let gap = match (self.symtab, self.strtab) { + (Some(symtab), Some(strtab)) if strtab > symtab => ((strtab - symtab) / sym::ENTRY_SIZE as u64) as usize, + _ => 0, + }; + match gnu_hash { + None => Ok(gap), + Some(table) => GnuHash::parse(table).and_then(|h| h.sym_count()).ok_or(gap), + } + } + /// `DT_NEEDED` offsets into the string table, in the order they appear. /// /// An iterator rather than a collection: the count is one per entry of a diff --git a/toyos-elf/src/sym.rs b/toyos-elf/src/sym.rs index ede60cbfeb6..ef8ea9090ff 100644 --- a/toyos-elf/src/sym.rs +++ b/toyos-elf/src/sym.rs @@ -5,6 +5,8 @@ //! [`ImageOffset`] inside its module ([`Sym::address`]) or a [`TlsOffset`] //! inside its module's TLS segment ([`Sym::tls_offset`]). +use core::ffi::CStr; + use crate::layout::{Extent, ImageOffset, TlsSegment}; use crate::read; use crate::tls::TlsOffset; @@ -17,6 +19,9 @@ pub const STB_GLOBAL: u8 = 1; pub const STB_WEAK: u8 = 2; pub const STT_FUNC: u8 = 2; pub const STT_TLS: u8 = 6; +pub const SHN_ABS: u16 = 0xfff1; +pub const STV_INTERNAL: u8 = 1; +pub const STV_HIDDEN: u8 = 2; /// An `r_sym` below the symbol count it was parsed against: made only by the /// relocation parse. @@ -41,6 +46,7 @@ fn widen(v: u32) -> usize { pub struct Sym { name: u32, info: u8, + other: u8, shndx: u16, value: u64, size: u64, @@ -61,9 +67,12 @@ impl Sym { self.info & 0xf } - /// Whether another module may bind to this symbol. + /// Whether another module may bind to this symbol: defined, global or + /// weak, and visible outside its own. pub const fn is_exported(&self) -> bool { - self.is_defined() && matches!(self.bind(), STB_GLOBAL | STB_WEAK) + self.is_defined() + && matches!(self.bind(), STB_GLOBAL | STB_WEAK) + && !matches!(self.other & 3, STV_INTERNAL | STV_HIDDEN) } /// This symbol's name in `strings`, `""` when it names nothing readable. @@ -233,6 +242,38 @@ impl<'a> SymTab<'a> { (!name.is_empty()).then_some((name, within)) } + /// The symbol `dladdr` names for `offset`, and how far into it that is. + /// + /// glibc's choice (`elf/dl-addr.c`): an exported symbol + /// ([`Sym::is_exported`]), not `STT_TLS` and not `SHN_ABS`, whose name is + /// a C string in the string table, holding `offset` in + /// `[value, value + size)` or at `value` for a size of 0; of several, the + /// first at the highest value. `offset` is relative to the module's load + /// base, as `st_value` is. + pub fn dladdr(&self, offset: u64) -> Option<(&'a CStr, u64)> { + let mut best: Option<(&'a CStr, u64)> = None; + for sym in (0..self.count()).filter_map(|i| self.get(i)) { + if !sym.is_exported() || sym.kind() == STT_TLS || sym.shndx == SHN_ABS { + continue; + } + let Some(within) = offset.checked_sub(sym.value) else { continue }; + if !(within < sym.size || (sym.size == 0 && within == 0)) { + continue; + } + let Some(name) = self.c_name(sym) else { continue }; + if best.is_none_or(|(_, best_within)| within < best_within) { + best = Some((name, within)); + } + } + best + } + + /// `sym`'s name as the C string at `st_name`, or `None` when no NUL ends + /// it inside the string table. + fn c_name(&self, sym: Sym) -> Option<&'a CStr> { + CStr::from_bytes_until_nul(self.strs.get(usize::try_from(sym.name).ok()?..)?).ok() + } + /// Every index whose symbol is defined, in order. pub fn defined(self) -> impl Iterator + 'a { (1..self.count()).filter_map(move |i| { @@ -251,6 +292,7 @@ pub fn parse_at(data: &[u8], off: usize) -> Option { Some(Sym { name: read::u32_at(data, off)?, info: *data.get(off.checked_add(4)?)?, + other: *data.get(off.checked_add(5)?)?, shndx: read::u16_at(data, off.checked_add(6)?)?, value: read::u64_at(data, off.checked_add(8)?)?, size: read::u64_at(data, off.checked_add(16)?)?, diff --git a/toyos-elf/tests/tables.rs b/toyos-elf/tests/tables.rs index de9c183c1e5..c7ee70c07d7 100644 --- a/toyos-elf/tests/tables.rs +++ b/toyos-elf/tests/tables.rs @@ -758,3 +758,94 @@ fn each_machine_reads_its_own_relocation_numbers() { let parsed = rela::parse(entry, &rules, SymTab::empty()).unwrap().unwrap(); assert_eq!((parsed.offset(), parsed.op()), (0x10, Op::Relative(rules.extent.offset(4).unwrap()))); } + +// ── dladdr ────────────────────────────────────────────────────────────── + +/// One `Elf64_Sym` with its `st_other`, which carries the visibility. +fn sym_visible(st_name: u32, st_info: u8, st_other: u8, st_shndx: u16, st_value: u64, st_size: u64) -> [u8; 24] { + let mut out = sym_sized(st_name, st_info, st_shndx, st_value, st_size); + out[5] = st_other; + out +} + +/// The symbol `dladdr` names for each offset is glibc's (`elf/dl-addr.c`): +/// exported, not thread-local, not absolute, holding the offset or at it with +/// no size, the highest of several, the first of two at one value. +#[test] +fn dladdr_names_the_symbol_glibc_names() { + const WEAK_FUNC: u8 = (2 << 4) | 2; + const LOCAL_FUNC: u8 = 2; + const GLOBAL_TLS: u8 = (1 << 4) | 6; + const GLOBAL_NOTYPE: u8 = 1 << 4; + const HIDDEN: u8 = 2; + const INTERNAL: u8 = 1; + const PROTECTED: u8 = 3; + const ABS: u16 = 0xfff1; + let names = b"\0local\0func\0big\0weak\0mark\0hidden\0internal\0tls\0undef\0abs\0last\0guarded\0twin\0"; + let at = |name: &str| { + let needle = [b"\0", name.as_bytes(), b"\0"].concat(); + names.windows(needle.len()).position(|w| w == needle.as_slice()).unwrap() as u32 + 1 + }; + let syms = [ + sym(0, 0, 0, 0), + sym_visible(at("local"), LOCAL_FUNC, 0, 1, 0x1000, 0x800), + sym_visible(at("func"), FUNC, 0, 1, 0x1100, 0x100), + sym_visible(at("big"), FUNC, 0, 1, 0x1000, 0x300), + sym_visible(at("weak"), WEAK_FUNC, 0, 1, 0x1200, 0x80), + sym_visible(at("mark"), GLOBAL_NOTYPE, 0, 1, 0x1280, 0), + sym_visible(at("hidden"), FUNC, HIDDEN, 1, 0x1300, 0x20), + sym_visible(at("internal"), FUNC, INTERNAL, 1, 0x1320, 0x20), + sym_visible(at("tls"), GLOBAL_TLS, 0, 1, 0x1340, 0x40), + sym_visible(at("undef"), FUNC, 0, 0, 0x1380, 0x40), + sym_visible(at("abs"), GLOBAL_NOTYPE, 0, ABS, 0x1400, 0x40), + sym_visible(at("last"), OBJECT, 0, 1, 0x1500, 0x10), + sym_visible(at("guarded"), FUNC, PROTECTED, 1, 0x1600, 0x10), + sym_visible(at("twin"), FUNC, 0, 1, 0x1700, 0x10), + sym_visible(at("func"), FUNC, 0, 1, 0x1700, 0x10), + // A name past the string table: never an answer. + sym_visible(names.len() as u32 + 40, FUNC, 0, 1, 0x1800, 0x10), + ] + .concat(); + let table = SymTab::new(&syms, names); + for (offset, want) in [ + (0x1000, Some(("big", 0))), + (0x1150, Some(("func", 0x50))), + (0x1210, Some(("weak", 0x10))), + (0x1280, Some(("mark", 0))), + (0x1281, Some(("big", 0x281))), + (0x1310, None), + (0x1330, None), + (0x1350, None), + (0x1390, None), + (0x1410, None), + (0x1505, Some(("last", 5))), + (0x1510, None), + (0x1605, Some(("guarded", 5))), + (0x1705, Some(("twin", 5))), + (0x1805, None), + (0x0800, None), + ] { + let got = table.dladdr(offset).map(|(name, within)| (name.to_str().unwrap(), within)); + assert_eq!(got, want, "offset {offset:#x}"); + } + // A name no NUL ends inside the string table: never an answer either. + let unended = [sym(0, 0, 0, 0), sym_visible(1, FUNC, 0, 1, 0x1000, 0x10)].concat(); + assert_eq!(SymTab::new(&unended, b"\0abc").dladdr(0x1005), None); + assert!(SymTab::new(&unended, b"\0abc\0").dladdr(0x1005).is_some()); +} + +/// `.dynsym`'s count, where no section header says it, is what `.gnu.hash` +/// describes, or the `DT_SYMTAB`–`DT_STRTAB` gap, which is also the answer, +/// as an error, for a `.gnu.hash` that describes none. +#[test] +fn the_symbol_count_is_the_hash_tables_or_the_gap() { + let tables = Dynamic::parse(&dynamic(&[(dynamic::DT_SYMTAB, 0x200), (dynamic::DT_STRTAB, 0x200 + 24 * 9)])); + let hashed = hash_table(1, 2, 1, 6, &[2], &[4, 6, 9]); + assert_eq!(tables.sym_count(Some(&hashed)), Ok(5)); + assert_eq!(tables.sym_count(None), Ok(9)); + assert_eq!(tables.sym_count(Some(&[])), Err(9)); + assert_eq!(tables.sym_count(Some(&hash_table(1, 0, 1, 0, &[1], &[0; 4]))), Err(9)); + let backwards = Dynamic::parse(&dynamic(&[(dynamic::DT_SYMTAB, 0x400), (dynamic::DT_STRTAB, 0x200)])); + assert_eq!(backwards.sym_count(None), Ok(0)); + assert_eq!(Dynamic::default().sym_count(None), Ok(0)); +} diff --git a/toyos-libc-copies/Cargo.toml b/toyos-libc-copies/Cargo.toml index d63bdb410e4..a421e5b0e8d 100644 --- a/toyos-libc-copies/Cargo.toml +++ b/toyos-libc-copies/Cargo.toml @@ -6,14 +6,14 @@ edition = "2021" license = "MIT OR Apache-2.0" publish = false -# `userland/libc/src/`'s modules that read and set nothing but what they are -# handed, compiled for the host this runs on and held against Rust's own and -# the host C library's; and `userland/libc/include/` read against the -# definitions in `userland/libc/src/`. A package of its own because libc -# cross-compiles and exports `memcpy`: this compiles only those modules, the way -# `kernel-loom` compiles kernel files. `std-runtime` is libc's own switch, on -# here so the architecture module's `_start` stays out of a host binary that -# has an entry of its own. +# A package of its own because libc cross-compiles and exports `memcpy`: this +# compiles only those modules, the way `kernel-loom` compiles kernel files. +# `std-runtime` is libc's own switch, on here so the architecture module's +# `_start` stays out of a host binary that has an entry of its own. [features] default = ["std-runtime"] std-runtime = [] + +[dev-dependencies] +toyos-abi = { path = "../toyos-abi" } +toyos-elf = { path = "../toyos-elf" } diff --git a/toyos-libc-copies/src/descriptor_requests.rs b/toyos-libc-copies/src/descriptor_requests.rs new file mode 100644 index 00000000000..3e15b23e57b --- /dev/null +++ b/toyos-libc-copies/src/descriptor_requests.rs @@ -0,0 +1,113 @@ +//! What `fcntl` decides before the kernel is asked, and `F_DUPFD`'s walk to +//! its floor over a handle table numbered as the kernel numbers one +//! (`kernel/src/object/handle.rs`). + +use crate::fdreq::{self, Command}; + +/// What clang leaves in the register of an `int` argument of -1: `movl +/// $0xffffffff, %edx`, which zeroes the upper half (measured on +/// `206_libc_refusals.o`). +const MINUS_ONE_AS_CLANG_PASSES_IT: u64 = 0xffff_ffff; + +#[test] +fn fcntl_reads_an_int_argument_from_the_lower_half_of_its_register() { + for (cmd, arg, want) in [ + (0, MINUS_ONE_AS_CLANG_PASSES_IT, Command::Invalid), + (0, u64::MAX, Command::Invalid), + (0, 0xdead_beef_0000_000a, Command::DupAtLeast(10)), + (0, 0, Command::DupAtLeast(0)), + (0, 4095, Command::DupAtLeast(4095)), + // `{OPEN_MAX}`, the slots a table has. + (0, 4096, Command::Invalid), + (0, 0x7fff_ffff, Command::Invalid), + (1, u64::MAX, Command::GetFd), + (2, 1, Command::SetFd(true)), + (2, 0xffff_ffff_0000_0000, Command::SetFd(false)), + (2, 0, Command::SetFd(false)), + (3, 0, Command::Unsupported), + (4, 0, Command::Unsupported), + (5, 0, Command::Invalid), + (6, 0, Command::Invalid), + (7, 0, Command::Invalid), + (8, 0, Command::Unsupported), + (9, 0, Command::Unsupported), + (1030, 10, Command::Unsupported), + (12345, 0, Command::Invalid), + (-1, 0, Command::Invalid), + ] { + assert_eq!(fdreq::command(cmd, arg), want, "fcntl(_, {cmd}, {arg:#x})"); + } +} + +/// A handle table as the kernel keeps one: a fresh slot is the next index at +/// generation 0, numbered as the bare index; a closed slot goes on a free list +/// one generation on, and a `dup` takes the free list's last first. +struct Table { + generations: Vec, + held: Vec, + free: Vec, +} + +impl Table { + fn with(open: usize) -> Table { + Table { generations: vec![0; open], held: vec![true; open], free: Vec::new() } + } + + fn dup(&mut self) -> Result { + let slot = match self.free.pop() { + Some(slot) => slot, + None if self.generations.len() < 4096 => { + self.generations.push(0); + self.held.push(false); + self.generations.len() - 1 + } + None => return Err(()), + }; + self.held[slot] = true; + Ok((self.generations[slot] << 12) | slot as u32) + } + + fn close(&mut self, number: u32) { + let slot = (number & 0xfff) as usize; + assert!(self.held[slot] && self.generations[slot] == number >> 12, "closed {number:#x}, which is not held"); + self.held[slot] = false; + self.generations[slot] += 1; + self.free.push(slot); + } + + fn holding(&self) -> usize { + self.held.iter().filter(|&&h| h).count() + } +} + +/// Every floor under the slot count, over a table nothing has churned and over +/// one whose every slot has been closed once: the answer is at or above the +/// floor, and the walk never holds more than one duplicate besides it. +#[test] +fn f_dupfd_meets_its_floor_holding_one_duplicate_at_most() { + for churned in [false, true] { + for floor in [0u32, 1, 3, 4, 10, 255, 4094, 4095] { + let mut table = Table::with(4); + if churned { + let all: Vec = (0..4092).map(|_| table.dup().unwrap()).collect(); + all.into_iter().for_each(|n| table.close(n)); + } + let before = table.holding(); + let table = core::cell::RefCell::new(table); + let mut most = 0; + let answer = fdreq::dup_at_least( + floor, + || { + let mut t = table.borrow_mut(); + most = most.max(t.holding() + 1 - before); + t.dup() + }, + |below| table.borrow_mut().close(below), + ) + .unwrap(); + assert!(answer >= floor, "floor {floor} answered {answer:#x} (churned: {churned})"); + assert_eq!(most, 1, "floor {floor} held {most} duplicates at once (churned: {churned})"); + assert_eq!(table.borrow().holding(), before + 1, "floor {floor} left duplicates open (churned: {churned})"); + } + } +} diff --git a/toyos-libc-copies/src/dladdr_symbols.rs b/toyos-libc-copies/src/dladdr_symbols.rs index 56314faf8d2..a75fedd8e2b 100644 --- a/toyos-libc-copies/src/dladdr_symbols.rs +++ b/toyos-libc-copies/src/dladdr_symbols.rs @@ -1,8 +1,7 @@ -//! `dladdr`'s search on an image laid out here, with each way a table says how -//! many symbols it has: `.gnu.hash`, `DT_HASH`, and neither. The answers are -//! glibc's `dladdr` rule (`elf/dl-addr.c`): an exported symbol, no TLS, -//! defined in a section, holding the address in `[value, value + size)` or at -//! `value` when its size is 0, the highest value of several. +//! `dladdr`'s walk of an image laid out here: the segment holding an address, +//! the image's first byte, and the tables it hands `toyos-elf`'s choice, as +//! many symbols as `.gnu.hash` describes, or with none as the gap to the +//! string table holds. use crate::elfsym::Image; @@ -11,42 +10,27 @@ const PHDRS: usize = 0x40; const DYNAMIC: usize = 0x100; const SYMTAB: usize = 0x200; const STRTAB: usize = 0x400; +/// Below the symbols, so that only the `.gnu.hash` counts them. +const STRTAB_FIRST: usize = 0x180; const HASH: usize = 0x500; -const GLOBAL: u8 = 1 << 4; -const WEAK: u8 = 2 << 4; -const FUNC: u8 = 2; -const OBJECT: u8 = 1; -const TLS: u8 = 6; -const HIDDEN: u8 = 2; -const ABS: u16 = 0xfff1; +const GLOBAL_FUNC: u8 = (1 << 4) | 2; +const LOCAL_FUNC: u8 = 2; -/// Name, `st_info`, `st_other`, `st_shndx`, value, size; index 0 is the null -/// symbol, and 1 the one local, which a `.gnu.hash` leaves unhashed. -const SYMBOLS: &[(&str, u8, u8, u16, u64, u64)] = &[ - ("", 0, 0, 0, 0, 0), - ("local", FUNC, 0, 1, 0x1000, 0x800), - ("func", GLOBAL | FUNC, 0, 1, 0x1100, 0x100), - ("big", GLOBAL | FUNC, 0, 1, 0x1000, 0x300), - ("weak", WEAK | FUNC, 0, 1, 0x1200, 0x80), - ("mark", GLOBAL, 0, 1, 0x1280, 0), - ("hidden", GLOBAL | FUNC, HIDDEN, 1, 0x1300, 0x40), - ("tls", GLOBAL | TLS, 0, 1, 0x1340, 0x40), - ("undef", GLOBAL | FUNC, 0, 0, 0, 0), - ("abs", GLOBAL, 0, ABS, 0x1400, 0x40), - ("last", GLOBAL | OBJECT, 0, 1, 0x1500, 0x10), +/// Name, `st_info`, value, size; index 0 is the null symbol, and 1 the one +/// local, which a `.gnu.hash` leaves unhashed. +const SYMBOLS: &[(&str, u8, u64, u64)] = &[ + ("", 0, 0, 0), + ("local", LOCAL_FUNC, 0x1000, 0x800), + ("func", GLOBAL_FUNC, 0x1100, 0x100), + ("big", GLOBAL_FUNC, 0x1000, 0x300), + ("last", GLOBAL_FUNC, 0x1500, 0x10), ]; -#[derive(Clone, Copy)] -enum Count { - GnuHash, - SysvHash, - Gap, -} - /// An image at the start of an 8-aligned buffer, every table addressed from -/// it, `.dynamic` naming the symbols as `count` says. -fn image(count: Count) -> Vec { +/// it, its symbols counted by a `.gnu.hash` or else by the gap to the string +/// table. +fn image(hashed: bool) -> Vec { let mut bytes = vec![0u8; SIZE]; let mut put = |at: usize, value: &[u8]| bytes[at..at + value.len()].copy_from_slice(value); // PT_LOAD over the whole image, then PT_DYNAMIC. @@ -55,8 +39,9 @@ fn image(count: Count) -> Vec { put(PHDRS + 40, &(SIZE as u64).to_le_bytes()); put(PHDRS + 56, &2u32.to_le_bytes()); put(PHDRS + 56 + 16, &(DYNAMIC as u64).to_le_bytes()); + put(PHDRS + 56 + 40, &0x100u64.to_le_bytes()); let mut names = vec![0u8]; - for (i, (name, info, other, shndx, value, size)) in SYMBOLS.iter().enumerate() { + for (i, (name, info, value, size)) in SYMBOLS.iter().enumerate() { let at = SYMTAB + i * 24; let offset = if name.is_empty() { 0 } else { names.len() as u32 }; if !name.is_empty() { @@ -64,37 +49,28 @@ fn image(count: Count) -> Vec { names.push(0); } put(at, &offset.to_le_bytes()); - put(at + 4, &[*info, *other]); - put(at + 6, &shndx.to_le_bytes()); + put(at + 4, &[*info]); + put(at + 6, &1u16.to_le_bytes()); put(at + 8, &value.to_le_bytes()); put(at + 16, &size.to_le_bytes()); } - put(STRTAB, &names); - let symbols = SYMBOLS.len() as u32; - let mut tags = vec![(6u64, SYMTAB as u64), (5, STRTAB as u64), (10, names.len() as u64)]; - match count { - Count::GnuHash => { - // One bucket, one bloom word, every symbol from 2 hashed in one - // chain whose last value has its low bit set. - let (nbuckets, symoffset, bloom_words) = (1u32, 2u32, 1u32); - put(HASH, &nbuckets.to_le_bytes()); - put(HASH + 4, &symoffset.to_le_bytes()); - put(HASH + 8, &bloom_words.to_le_bytes()); - put(HASH + 12, &6u32.to_le_bytes()); - put(HASH + 16, &u64::MAX.to_le_bytes()); - put(HASH + 24, &symoffset.to_le_bytes()); - for i in symoffset..symbols { - let last = u32::from(i == symbols - 1); - put(HASH + 28 + (i - symoffset) as usize * 4, &((i << 1) | last).to_le_bytes()); - } - tags.push((0x6fff_fef5, HASH as u64)); + let strtab = if hashed { STRTAB_FIRST } else { STRTAB }; + put(strtab, &names); + let mut tags = vec![(6u64, SYMTAB as u64), (5, strtab as u64), (10, names.len() as u64)]; + if hashed { + // One bucket, one bloom word, every symbol from 2 hashed in one chain + // whose last value has its low bit set. + let (symbols, symoffset) = (SYMBOLS.len() as u32, 2u32); + for (i, word) in [1u32, symoffset, 1, 6].into_iter().enumerate() { + put(HASH + i * 4, &word.to_le_bytes()); } - Count::SysvHash => { - put(HASH, &1u32.to_le_bytes()); - put(HASH + 4, &symbols.to_le_bytes()); - tags.push((4, HASH as u64)); + put(HASH + 16, &u64::MAX.to_le_bytes()); + put(HASH + 24, &symoffset.to_le_bytes()); + for i in symoffset..symbols { + let last = u32::from(i == symbols - 1); + put(HASH + 28 + (i - symoffset) as usize * 4, &((i << 1) | last).to_le_bytes()); } - Count::Gap => {} + tags.push((0x6fff_fef5, HASH as u64)); } for (i, (tag, value)) in tags.into_iter().chain([(0, 0)]).enumerate() { put(DYNAMIC + i * 16, &tag.to_le_bytes()); @@ -108,43 +84,29 @@ fn loaded(words: &[u64]) -> Image { Image { base, phdr: (base as usize + PHDRS) as *const u8, phnum: 2 } } -/// The symbol `dladdr` names for `offset` into the image, by name, with the -/// offset of its value. -fn named(image: Image, offset: u64) -> Option<(String, u64)> { - // SAFETY: the image is the buffer `image` laid out, whole. - let (name, at) = unsafe { image.symbol(image.base + offset) }?; - // SAFETY: a name in the image's string table, NUL-terminated there. - let name = unsafe { std::ffi::CStr::from_ptr(name.cast()) }; - Some((name.to_str().unwrap().to_string(), at - image.base)) -} - #[test] -fn every_count_finds_glibcs_symbol() { - for count in [Count::GnuHash, Count::SysvHash, Count::Gap] { - let words = image(count); +fn either_count_reaches_the_symbol_at_its_address() { + for hashed in [true, false] { + let words = image(hashed); let image = loaded(&words); for (offset, want) in [ (0x1000, Some(("big", 0x1000))), (0x1150, Some(("func", 0x1100))), - (0x1210, Some(("weak", 0x1200))), - (0x1280, Some(("mark", 0x1280))), - (0x1281, Some(("big", 0x1000))), - (0x1310, None), - (0x1350, None), - (0x1410, None), (0x1505, Some(("last", 0x1500))), (0x1510, None), (0x0800, None), ] { - let want = want.map(|(n, v)| (n.to_string(), v)); - assert_eq!(named(image, offset), want, "offset {offset:#x}"); + // SAFETY: the image is the buffer `image` laid out, whole. + let got = unsafe { image.symbol(image.base + offset) } + .map(|(name, at)| (name.to_str().unwrap(), at - image.base)); + assert_eq!(got, want, "offset {offset:#x}, hashed {hashed}"); } } } #[test] fn an_image_holds_its_load_segment_and_starts_at_its_page() { - let words = image(Count::GnuHash); + let words = image(true); let image = loaded(&words); let base = image.base; // SAFETY: the image's program headers, in the buffer. diff --git a/toyos-libc-copies/src/fparts_differential.rs b/toyos-libc-copies/src/fparts_differential.rs index 992318b9984..43f513e8c6a 100644 --- a/toyos-libc-copies/src/fparts_differential.rs +++ b/toyos-libc-copies/src/fparts_differential.rs @@ -63,10 +63,16 @@ fn modf_is_the_host_libraries() { } #[test] -fn logb_is_the_host_libraries() { +fn logb_is_the_host_libraries_and_a_zero_its_pole() { for x in inputs() { // SAFETY: a pure function of its argument. let host = unsafe { host_logb(x) }; - assert!(same(fparts::logb(x), host), "logb({x:e} = {:#x}): {}, the host {host}", x.to_bits(), fparts::logb(x)); + let ours = fparts::logb(x); + let agrees = match ours { + Ok(exponent) => x != 0.0 && same(exponent, host), + // The pole's value is the one the host answers. + Err(fparts::Pole) => x == 0.0 && host == f64::NEG_INFINITY, + }; + assert!(agrees, "logb({x:e} = {:#x}): {ours:?}, the host {host}", x.to_bits()); } } diff --git a/toyos-libc-copies/src/lib.rs b/toyos-libc-copies/src/lib.rs index 669b26f59e6..625b9493b0b 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -6,11 +6,7 @@ //! architecture checks its own module. The UTF-8 reader against //! `core::str::from_utf8`, the number reader against the host C library's, //! AArch64's `long double` widening against compiler-builtins', and the errno -//! codes against `include/errno.h`. `strnlen`, `modf` and `logb` against the -//! host C library's, `strsignal`'s texts, a signal mask's changes, the readdir -//! answer's reader, `dladdr`'s symbol search on an image laid out here, and -//! what the memory calls refuse. And libc's headers against its definitions -//! (`prototypes`). +//! codes against `include/errno.h`. #[cfg(test)] extern crate alloc; @@ -22,6 +18,9 @@ mod arch; #[path = "../../userland/libc/src/elfsym.rs"] mod elfsym; #[cfg(test)] +#[path = "../../userland/libc/src/fdreq.rs"] +mod fdreq; +#[cfg(test)] #[path = "../../userland/libc/src/fparts.rs"] mod fparts; #[cfg(test)] @@ -43,6 +42,8 @@ mod text; #[path = "../../userland/libc/src/utf8.rs"] mod utf8; +#[cfg(test)] +mod descriptor_requests; #[cfg(test)] mod dladdr_symbols; #[cfg(test)] diff --git a/toyos-libc-copies/src/listing_reader.rs b/toyos-libc-copies/src/listing_reader.rs index 92121f7423c..2554abababf 100644 --- a/toyos-libc-copies/src/listing_reader.rs +++ b/toyos-libc-copies/src/listing_reader.rs @@ -1,56 +1,49 @@ -//! The readdir answer's reader against answers encoded as the kernel's -//! `sys_readdir` encodes them (`kernel/src/syscall/fs.rs`): a kind byte, the -//! name, a NUL and eight bytes of size, for each entry. +//! What `opendir`, `readdir` and `dladdr` make of the kernel's answers: a +//! whole answer however often it grows between two asks, and a name `d_name` +//! holds or `readdir` refuses. use crate::listing; -/// An answer for `entries`, each a name, whether it is a directory, and a size. -fn answer(entries: &[(&str, bool, u64)]) -> Vec { - let mut out = Vec::new(); - for (name, is_dir, size) in entries { - out.push(if *is_dir { 2 } else { 1 }); - out.extend_from_slice(name.as_bytes()); - out.push(0); - out.extend_from_slice(&size.to_le_bytes()); +/// A call answering `sizes` in turn, each the length its answer then needs, +/// writing it only into a buffer that holds it. +fn growing(sizes: &[usize]) -> impl FnMut(&mut [u8]) -> Result + '_ { + let mut asked = 0; + move |buf| { + let need = sizes[asked.min(sizes.len() - 1)]; + asked += 1; + if need <= buf.len() { + buf[..need].iter_mut().enumerate().for_each(|(i, b)| *b = i as u8); + } + Ok(need) } - out -} - -fn read(bytes: &[u8]) -> Vec<(String, bool)> { - let mut pos = 0; - let mut out = Vec::new(); - while let Some(entry) = listing::next(bytes, &mut pos) { - out.push((String::from_utf8(entry.name.to_vec()).unwrap(), entry.is_dir)); - } - assert_eq!(pos, bytes.len(), "the reader stopped short of the answer's end"); - out } #[test] -fn every_entry_is_read_back_whatever_its_size_holds() { - // Sizes whose bytes are NULs, kind bytes and letters: a reader that does - // not step over all eight takes them for names. - let entries = [ - ("a", false, 0), - ("dir", true, 0x0201_0000_0000_0000), - ("b.txt", false, 0x6162_6300_0102_0304), - ("", false, u64::MAX), - ("last", true, 17), - ]; - let got = read(&answer(&entries)); - let want: Vec<(String, bool)> = entries.iter().map(|(n, d, _)| ((*n).to_string(), *d)).collect(); - assert_eq!(got, want); - assert!(read(&[]).is_empty()); -} - -#[test] -#[should_panic(expected = "of kind 3")] -fn an_unknown_kind_is_a_broken_kernel() { - read(&[3, b'x', 0, 0, 0, 0, 0, 0, 0, 0, 0]); +fn an_answer_is_asked_again_until_it_fits() { + // The first ask is of an empty buffer; each growth is met at the next. + for (sizes, want) in [ + (&[0][..], 0), + (&[5], 5), + (&[5, 9], 9), + (&[5, 9, 4000, 70_000], 70_000), + (&[70_000, 3], 3), + ] { + let whole = listing::whole(growing(sizes)).unwrap(); + assert_eq!(whole.len(), want, "{sizes:?}"); + assert!(whole.iter().enumerate().all(|(i, &b)| b == i as u8), "{sizes:?}"); + } + assert_eq!(listing::whole(|_| Err::(7)), Err(7)); } #[test] -#[should_panic(expected = "no size")] -fn a_short_entry_is_a_broken_kernel() { - read(&[1, b'x', 0, 0, 0]); +fn a_name_fits_d_name_with_its_nul_or_is_refused() { + for len in [0, 1, 254, 255] { + let name = vec![b'n'; len]; + let held = listing::d_name(&name).unwrap_or_else(|| panic!("a {len}-byte name was refused")); + assert_eq!(&held[..len], &name[..]); + assert!(held[len..].iter().all(|&b| b == 0), "a {len}-byte name is not NUL-terminated"); + } + for len in [256, 257, 765] { + assert!(listing::d_name(&vec![b'n'; len]).is_none(), "a {len}-byte name was held"); + } } diff --git a/toyos-libc-copies/src/memory_refusals.rs b/toyos-libc-copies/src/memory_refusals.rs index 65af0825ddb..4366343e090 100644 --- a/toyos-libc-copies/src/memory_refusals.rs +++ b/toyos-libc-copies/src/memory_refusals.rs @@ -1,12 +1,9 @@ //! What libc's memory calls refuse before the kernel or the allocator is -//! asked: `mmap` a file, executable memory, no bytes, or a fixed place off a -//! page; `posix_madvise` anything but its five; `madvise` the same, an +//! asked: `mmap` a file, executable memory, no bytes, a fixed place off a +//! page, or no one disposition; `posix_madvise` anything but its five; `madvise` the same, an //! address off a page, and Linux's discard. -use crate::memreq::{self, MapRefusal, MAP_ANONYMOUS, MAP_FIXED, PROT_EXEC, PROT_READ, PROT_WRITE}; - -const MAP_SHARED: i32 = 0x01; -const MAP_PRIVATE: i32 = 0x02; +use crate::memreq::{self, MapRefusal, MAP_ANONYMOUS, MAP_FIXED, MAP_PRIVATE, MAP_SHARED, PROT_EXEC, PROT_READ, PROT_WRITE}; #[test] fn mmap_refuses_what_the_kernel_cannot_map_and_nothing_else() { @@ -21,6 +18,10 @@ fn mmap_refuses_what_the_kernel_cannot_map_and_nothing_else() { (0x4000_0001, 4096, rw, anonymous, None), (0x4000_0001, 4096, rw, anonymous | MAP_FIXED, Some(MapRefusal::Invalid)), (0, 0, rw, anonymous, Some(MapRefusal::Invalid)), + // Neither disposition, and both. + (0, 4096, rw, MAP_ANONYMOUS, Some(MapRefusal::Invalid)), + (0, 4096, rw, MAP_SHARED | MAP_PRIVATE | MAP_ANONYMOUS, Some(MapRefusal::Invalid)), + (0, 4096, PROT_READ, 0, Some(MapRefusal::Invalid)), (0, 4096, PROT_READ, MAP_PRIVATE, Some(MapRefusal::File)), (0, 4096, rw, MAP_SHARED, Some(MapRefusal::File)), (0, 4096, PROT_READ | PROT_EXEC, anonymous, Some(MapRefusal::Exec)), diff --git a/userland/libc/Cargo.lock b/userland/libc/Cargo.lock index df18e043698..17bddce55cc 100644 --- a/userland/libc/Cargo.lock +++ b/userland/libc/Cargo.lock @@ -36,6 +36,10 @@ dependencies = [ name = "toyos-abi" version = "0.16.0" +[[package]] +name = "toyos-elf" +version = "0.1.0" + [[package]] name = "toyos-libc" version = "0.1.0" @@ -43,6 +47,7 @@ dependencies = [ "dlmalloc", "toyos", "toyos-abi", + "toyos-elf", ] [[package]] diff --git a/userland/libc/Cargo.toml b/userland/libc/Cargo.toml index 8a1140f4399..91181462d6e 100644 --- a/userland/libc/Cargo.toml +++ b/userland/libc/Cargo.toml @@ -11,6 +11,7 @@ std-runtime = [] [dependencies] toyos-abi = { path = "../../toyos-abi" } toyos = { path = "../../toyos" } +toyos-elf = { path = "../../toyos-elf" } dlmalloc = "0.2.7" [profile.dev] diff --git a/userland/libc/src/elfsym.rs b/userland/libc/src/elfsym.rs index c140215a31f..a41ee60a54b 100644 --- a/userland/libc/src/elfsym.rs +++ b/userland/libc/src/elfsym.rs @@ -1,31 +1,16 @@ //! What `dladdr` reads of a loaded image: whether an address lies in it, where -//! its first mapped byte is, and which of its dynamic symbols holds the -//! address, chosen as glibc's `dladdr` chooses. It reads nothing but the image +//! its first mapped byte is, and the tables `toyos-elf` chooses its symbol +//! from, each a slice of what the image maps. It reads nothing but the image //! it is handed, so the host tests it on an image it lays out //! (`toyos-libc-copies`). //! //! The loader relocates nothing in `PT_DYNAMIC`, so every address the dynamic //! table holds is relative to the image's base. -const PT_LOAD: u32 = 1; -const PT_DYNAMIC: u32 = 2; -const PHDR_SIZE: usize = 56; +use core::ffi::CStr; -const DT_NULL: u64 = 0; -const DT_HASH: u64 = 4; -const DT_STRTAB: u64 = 5; -const DT_SYMTAB: u64 = 6; -const DT_STRSZ: u64 = 10; -const DT_GNU_HASH: u64 = 0x6fff_fef5; - -const SYM_SIZE: usize = 24; -const SHN_UNDEF: u16 = 0; -const SHN_ABS: u16 = 0xfff1; -const STB_GLOBAL: u8 = 1; -const STB_WEAK: u8 = 2; -const STT_TLS: u8 = 6; -const STV_INTERNAL: u8 = 1; -const STV_HIDDEN: u8 = 2; +use toyos_elf::header::{ProgramHeader, PROGRAM_HEADER_SIZE, PT_DYNAMIC, PT_LOAD}; +use toyos_elf::{sym, Dynamic, SymTab}; /// A loaded image, as `dl_iterate_phdr` describes it. #[derive(Clone, Copy)] @@ -35,22 +20,13 @@ pub(crate) struct Image { pub(crate) phnum: usize, } -/// The fields of a program header this module reads. -struct Segment { - kind: u32, - vaddr: u64, - memsz: u64, -} - impl Image { /// # Safety /// `phdr` points at `phnum` readable program headers. - unsafe fn segments(self) -> impl Iterator { - (0..self.phnum).map(move |i| { - // SAFETY: the caller's. - let p = unsafe { self.phdr.add(i * PHDR_SIZE) }; - unsafe { Segment { kind: read(p, 0), vaddr: read(p, 16), memsz: read(p, 40) } } - }) + unsafe fn segments(self) -> impl Iterator { + // SAFETY: the caller's. + let table = unsafe { core::slice::from_raw_parts(self.phdr, self.phnum * PROGRAM_HEADER_SIZE) }; + (0..self.phnum).map_while(move |i| ProgramHeader::parse(table, i)) } /// Whether a `PT_LOAD` segment of the image holds `addr`. @@ -70,80 +46,35 @@ impl Image { self.base + (low & !0xfff) } + /// The image's bytes from `vaddr` to the end of the `PT_LOAD` segment + /// holding it, and at most `len` of them. + /// + /// # Safety + /// As [`Image::segments`], and every `PT_LOAD` segment is mapped. + unsafe fn mapped(self, vaddr: u64, len: u64) -> Option<&'static [u8]> { + let load = unsafe { self.segments() }.find(|s| s.kind == PT_LOAD && vaddr.wrapping_sub(s.vaddr) < s.memsz)?; + let len = len.min(load.vaddr + load.memsz - vaddr); + // SAFETY: inside a mapped segment, which the image keeps mapped. + Some(unsafe { core::slice::from_raw_parts((self.base + vaddr) as *const u8, len as usize) }) + } + /// The name and address of the dynamic symbol of the image that holds - /// `addr`, as glibc chooses one: exported (global or weak, neither hidden - /// nor internal), no thread-local, defined in a section, with `addr` in - /// `[value, value + size)` or at `value` for a size of 0; of several, the - /// highest value. + /// `addr`, as [`SymTab::dladdr`] chooses it, from as many symbols as + /// [`Dynamic::sym_count`] counts. /// /// # Safety - /// As [`Image::segments`], and every table the image's `PT_DYNAMIC` names - /// is mapped. - pub(crate) unsafe fn symbol(self, addr: u64) -> Option<(*const u8, u64)> { + /// As [`Image::mapped`]. + pub(crate) unsafe fn symbol(self, addr: u64) -> Option<(&'static CStr, u64)> { let dynamic = unsafe { self.segments() }.find(|s| s.kind == PT_DYNAMIC)?; - let at = |vaddr: u64| (self.base + vaddr) as *const u8; - let (mut symtab, mut strtab, mut strsz, mut hash, mut gnu_hash) = (None, None, 0, None, None); - let mut entry = at(dynamic.vaddr); - loop { - let (tag, value): (u64, u64) = unsafe { (read(entry, 0), read(entry, 8)) }; - match tag { - DT_NULL => break, - DT_SYMTAB => symtab = Some(value), - DT_STRTAB => strtab = Some(value), - DT_STRSZ => strsz = value, - DT_HASH => hash = Some(value), - DT_GNU_HASH => gnu_hash = Some(value), - _ => {} - } - entry = unsafe { entry.add(16) }; - } - let (symtab, strtab) = (symtab?, strtab?); - let count = match (gnu_hash, hash) { - (Some(table), _) => unsafe { gnu_hash_count(at(table)) }, - (None, Some(table)) => unsafe { read::(at(table), 4) as usize }, - // Adjacent in every layout a linker makes, as the loader also reads them. - (None, None) => strtab.saturating_sub(symtab) as usize / SYM_SIZE, + let dynamic = Dynamic::parse(unsafe { self.mapped(dynamic.vaddr, dynamic.memsz) }?); + let gnu_hash = match dynamic.gnu_hash { + Some(vaddr) => Some(unsafe { self.mapped(vaddr, u64::MAX) }?), + None => None, }; - let mut best: Option<(u64, u32)> = None; - for i in 0..count { - let sym = unsafe { at(symtab).add(i * SYM_SIZE) }; - let (name, info, other, shndx, value, size): (u32, u8, u8, u16, u64, u64) = - unsafe { (read(sym, 0), read(sym, 4), read(sym, 5), read(sym, 6), read(sym, 8), read(sym, 16)) }; - let (bind, visibility) = (info >> 4, other & 3); - let exported = (bind == STB_GLOBAL || bind == STB_WEAK) && visibility != STV_HIDDEN && visibility != STV_INTERNAL; - if !exported || info & 0xf == STT_TLS || shndx == SHN_UNDEF || shndx == SHN_ABS || u64::from(name) >= strsz { - continue; - } - let start = self.base + value; - let holds = addr >= start && (addr - start < size || (size == 0 && addr == start)); - if holds && best.is_none_or(|(v, _)| v < value) { - best = Some((value, name)); - } - } - best.map(|(value, name)| (unsafe { at(strtab).add(name as usize) }, self.base + value)) - } -} - -/// How many symbols the `.gnu.hash` table at `table` covers: through the end -/// of the chain the highest bucket starts, or up to its first hashed symbol -/// when every bucket is empty. -unsafe fn gnu_hash_count(table: *const u8) -> usize { - let (nbuckets, symoffset, bloom_words): (u32, u32, u32) = unsafe { (read(table, 0), read(table, 4), read(table, 8)) }; - let (nbuckets, symoffset) = (nbuckets as usize, symoffset as usize); - let buckets = unsafe { table.add(16 + bloom_words as usize * 8) }; - let chains = unsafe { buckets.add(nbuckets * 4) }; - let Some(mut i) = (0..nbuckets).map(|b| unsafe { read::(buckets, b * 4) as usize }).filter(|&s| s != 0).max() else { - return symoffset; - }; - // A chain's last hash has its low bit set. - while unsafe { read::(chains, (i - symoffset) * 4) } & 1 == 0 { - i += 1; + let count = dynamic.sym_count(gnu_hash).unwrap_or_else(|gap| gap); + let syms = unsafe { self.mapped(dynamic.symtab?, (count as u64).saturating_mul(sym::ENTRY_SIZE as u64)) }?; + let strs = unsafe { self.mapped(dynamic.strtab?, dynamic.strsz?) }?; + let (name, within) = SymTab::new(syms, strs).dladdr(addr.checked_sub(self.base)?)?; + Some((name, addr - within)) } - i + 1 -} - -/// # Safety -/// `at + offset` holds a `T`, however aligned. -unsafe fn read(at: *const u8, offset: usize) -> T { - unsafe { at.add(offset).cast::().read_unaligned() } } diff --git a/userland/libc/src/fdreq.rs b/userland/libc/src/fdreq.rs new file mode 100644 index 00000000000..ff627368a2d --- /dev/null +++ b/userland/libc/src/fdreq.rs @@ -0,0 +1,73 @@ +//! What `fcntl` decides before the kernel is asked. It reads nothing but what +//! it is handed, so the host tests it (`toyos-libc-copies`). + +use toyos_abi::RawHandle; + +const F_DUPFD: i32 = 0; +const F_GETFD: i32 = 1; +const F_SETFD: i32 = 2; +const F_GETFL: i32 = 3; +const F_SETFL: i32 = 4; +const F_GETLK: i32 = 5; +const F_SETLK: i32 = 6; +const F_SETLKW: i32 = 7; +const F_SETOWN: i32 = 8; +const F_GETOWN: i32 = 9; +const F_DUPFD_CLOEXEC: i32 = 1030; +const FD_CLOEXEC: i32 = 1; + +/// What `fcntl` does for one command. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Command { + /// A duplicate numbered `floor` or above ([`dup_at_least`]). + DupAtLeast(u32), + /// The descriptor's close-on-exec flag, read. + GetFd, + /// The descriptor's close-on-exec flag, set to this. + SetFd(bool), + /// A record lock, which no file here supports, or a command or `F_DUPFD` + /// floor POSIX refuses: `EINVAL`. + Invalid, + /// A descriptor's status flags or owner, or a duplicate closed on `exec`, + /// none of which this library keeps: `ENOSYS`. + Unsupported, +} + +/// `cmd`, its third argument read from `arg`, the register a variadic one +/// arrives in. `F_DUPFD` and `F_SETFD` take an `int`, which leaves the +/// register's upper half unspecified, so only the lower is read. +pub(crate) fn command(cmd: i32, arg: u64) -> Command { + let int = arg as u32 as i32; + match cmd { + // POSIX's `EINVAL` for a floor at or above `{OPEN_MAX}`, the slots a + // table has: past them only a slot's generation reaches the floor. + F_DUPFD => match u32::try_from(int) { + Ok(floor) if (floor as usize) < RawHandle::MAX_SLOTS => Command::DupAtLeast(floor), + _ => Command::Invalid, + }, + F_GETFD => Command::GetFd, + F_SETFD => Command::SetFd(int & FD_CLOEXEC != 0), + F_GETLK | F_SETLK | F_SETLKW => Command::Invalid, + F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => Command::Unsupported, + _ => Command::Invalid, + } +} + +/// A duplicate numbered `floor` or above, from `dup`, holding at most one +/// other duplicate at a time. The kernel picks each number, so this is not the +/// lowest free one: a duplicate below a floor under the slot count is a slot at +/// its first generation, and closed again it is the next `dup`'s, one +/// generation on and numbered past every slot. +pub(crate) fn dup_at_least( + floor: u32, + mut dup: impl FnMut() -> Result, + mut close: impl FnMut(u32), +) -> Result { + loop { + let duplicate = dup()?; + if duplicate >= floor { + return Ok(duplicate); + } + close(duplicate); + } +} diff --git a/userland/libc/src/fparts.rs b/userland/libc/src/fparts.rs index 007682f3ba7..ff89184e8eb 100644 --- a/userland/libc/src/fparts.rs +++ b/userland/libc/src/fparts.rs @@ -37,24 +37,27 @@ pub(crate) fn modf(x: f64) -> (f64, f64) { (int, x - int) } +/// `logb` at a zero: POSIX's pole error, `-inf` with `errno` `ERANGE`. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Pole; + /// `logb`: the exponent of `x` as a double, the one a normalized `x` would -/// have for a subnormal; `-inf` for a zero, `+inf` for an infinity, the NaN -/// for a NaN. -pub(crate) fn logb(x: f64) -> f64 { +/// have for a subnormal; `+inf` for an infinity, the NaN for a NaN. +pub(crate) fn logb(x: f64) -> Result { if x.is_nan() { - return x; + return Ok(x); } if x.is_infinite() { - return f64::INFINITY; + return Ok(f64::INFINITY); } if x == 0.0 { - return f64::NEG_INFINITY; + return Err(Pole); } let e = exponent(x); if e > -BIAS { - return f64::from(e); + return Ok(f64::from(e)); } // Subnormal: `mantissa * 2^-1074`, whose top set bit is the exponent. let mantissa = x.to_bits() & ((1u64 << MANTISSA_BITS) - 1); - f64::from(63 - mantissa.leading_zeros() as i32 - 1074) + Ok(f64::from(63 - mantissa.leading_zeros() as i32 - 1074)) } diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 66003b556f7..b73fe28fafc 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -8,6 +8,7 @@ mod arch; mod ctype; mod elfsym; mod errno; +mod fdreq; mod fparts; mod link; mod listing; diff --git a/userland/libc/src/link.rs b/userland/libc/src/link.rs index ef4e9a67255..c417fc106d0 100644 --- a/userland/libc/src/link.rs +++ b/userland/libc/src/link.rs @@ -81,7 +81,7 @@ pub unsafe extern "C" fn dladdr(addr: *const u8, info: *mut DlInfo) -> i32 { *info = DlInfo { dli_fname: named(module.base, path), dli_fbase: core::ptr::with_exposed_provenance_mut(image.start() as usize), - dli_sname: symbol.map_or(core::ptr::null(), |(name, _)| name), + dli_sname: symbol.map_or(core::ptr::null(), |(name, _)| name.as_ptr().cast()), dli_saddr: symbol.map_or(core::ptr::null_mut(), |(_, at)| core::ptr::with_exposed_provenance_mut(at as usize)), }; } @@ -128,16 +128,7 @@ fn named(base: u64, path: &[u8]) -> *const u8 { } } -/// One whole `SYS_QUERY_MODULES` answer: asked again while a `dlopen` -/// elsewhere grows it between the size and the read. +/// One whole `SYS_QUERY_MODULES` answer. fn answer() -> Vec { - let mut buf = Vec::new(); - loop { - let need = syscall::query_modules(&mut buf).expect("SYS_QUERY_MODULES refused a buffer this process owns"); - if need <= buf.len() { - buf.truncate(need); - return buf; - } - buf.resize(need, 0); - } + crate::listing::whole(syscall::query_modules).expect("SYS_QUERY_MODULES refused a buffer this process owns") } diff --git a/userland/libc/src/listing.rs b/userland/libc/src/listing.rs index 1fcabe549aa..40a4e476d20 100644 --- a/userland/libc/src/listing.rs +++ b/userland/libc/src/listing.rs @@ -1,27 +1,35 @@ -//! The kernel's answer to `SYS_READDIR`, one entry at a time: a kind byte (1 -//! for a file, 2 for a directory), the name, a NUL, and the size as eight -//! bytes, little-endian. It reads nothing but what it is handed, so the host +//! What `opendir`, `readdir` and `dladdr` make of the kernel's answers before +//! a caller sees them. It reads nothing but what it is handed, so the host //! tests it (`toyos-libc-copies`). -/// One entry of a listing. -pub(crate) struct Entry<'a> { - pub(crate) is_dir: bool, - pub(crate) name: &'a [u8], +use alloc::vec::Vec; + +/// The whole answer of a call that writes nothing into a buffer too short for +/// it and answers the length it needs: asked again at that length until one +/// fits, since a listing or a module table can grow between two asks. Each ask +/// is longer than the last, and the kernel bounds what it answers. +pub(crate) fn whole(mut ask: impl FnMut(&mut [u8]) -> Result) -> Result, E> { + let mut buf = Vec::new(); + loop { + let need = ask(&mut buf)?; + if need <= buf.len() { + buf.truncate(need); + return Ok(buf); + } + buf.resize(need, 0); + } } -/// The entry at `*pos` in `answer`, with `*pos` moved past it; `None` once -/// `*pos` is at the end. An answer of any other shape is a kernel that broke -/// its ABI, and panics. -pub(crate) fn next<'a>(answer: &'a [u8], pos: &mut usize) -> Option> { - let rest = answer.get(*pos..).filter(|r| !r.is_empty())?; - let is_dir = match rest[0] { - 1 => false, - 2 => true, - kind => panic!("SYS_READDIR answered an entry of kind {kind}"), - }; - let len = rest[1..].iter().position(|&b| b == 0).expect("SYS_READDIR answered a name with no NUL"); - let end = 1 + len + 1 + 8; - assert!(rest.len() >= end, "SYS_READDIR answered an entry with no size"); - *pos += end; - Some(Entry { is_dir, name: &rest[1..1 + len] }) +/// The bytes of `struct dirent`'s `d_name`: `{NAME_MAX}` and its NUL. +pub(crate) const D_NAME: usize = 256; + +/// `name` as `d_name` holds it, NUL-terminated, or `None` for a name too long +/// for it, which `readdir` answers `EOVERFLOW`. +pub(crate) fn d_name(name: &[u8]) -> Option<[u8; D_NAME]> { + if name.len() >= D_NAME { + return None; + } + let mut out = [0; D_NAME]; + out[..name.len()].copy_from_slice(name); + Some(out) } diff --git a/userland/libc/src/math.rs b/userland/libc/src/math.rs index 27b1b342dbc..3176755636b 100644 --- a/userland/libc/src/math.rs +++ b/userland/libc/src/math.rs @@ -248,9 +248,13 @@ pub unsafe extern "C" fn modf(x: f64, iptr: *mut f64) -> f64 { fraction } +/// `math.h`'s `math_errhandling` is `MATH_ERRNO`, so a pole sets `errno`. #[no_mangle] pub extern "C" fn logb(x: f64) -> f64 { - crate::fparts::logb(x) + crate::fparts::logb(x).unwrap_or_else(|crate::fparts::Pole| { + crate::errno::set(crate::errno::ERANGE); + f64::NEG_INFINITY + }) } // --- Utility functions --- diff --git a/userland/libc/src/memreq.rs b/userland/libc/src/memreq.rs index ca89db79780..fdfead9dbf0 100644 --- a/userland/libc/src/memreq.rs +++ b/userland/libc/src/memreq.rs @@ -5,16 +5,19 @@ pub(crate) const PROT_READ: i32 = 0x1; pub(crate) const PROT_WRITE: i32 = 0x2; pub(crate) const PROT_EXEC: i32 = 0x4; +pub(crate) const MAP_SHARED: i32 = 0x01; +pub(crate) const MAP_PRIVATE: i32 = 0x02; pub(crate) const MAP_FIXED: i32 = 0x10; pub(crate) const MAP_ANONYMOUS: i32 = 0x20; /// The page `sysconf(_SC_PAGESIZE)` answers. -const PAGE: usize = 4096; +pub(crate) const PAGE: usize = 4096; /// Why `mmap` cannot give what it was asked. #[derive(Debug, PartialEq, Eq)] pub(crate) enum MapRefusal { - /// No bytes, or a fixed place off a page boundary: `EINVAL`. + /// No bytes, a fixed place off a page boundary, or not exactly one of + /// `MAP_SHARED` and `MAP_PRIVATE`: `EINVAL`. Invalid, /// A file's bytes, which the kernel maps for no process: `ENODEV`. File, @@ -26,7 +29,11 @@ pub(crate) enum MapRefusal { /// it is. An anonymous `MAP_SHARED` is not: with no `fork`, no other process /// can map it, so a private mapping is all that sharing it could mean. pub(crate) fn mmap_refusal(addr: usize, len: usize, prot: i32, flags: i32) -> Option { - if len == 0 || (flags & MAP_FIXED != 0 && !addr.is_multiple_of(PAGE)) { + let disposition = flags & (MAP_SHARED | MAP_PRIVATE); + if len == 0 + || (flags & MAP_FIXED != 0 && !addr.is_multiple_of(PAGE)) + || !(disposition == MAP_SHARED || disposition == MAP_PRIVATE) + { return Some(MapRefusal::Invalid); } if flags & MAP_ANONYMOUS == 0 { diff --git a/userland/libc/src/misc.rs b/userland/libc/src/misc.rs index c49a3c12f01..547e1f54e35 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -245,7 +245,7 @@ const _SC_CLK_TCK: i32 = 2; #[no_mangle] pub unsafe extern "C" fn sysconf(name: i32) -> i64 { match name { - _SC_PAGESIZE => 4096, + _SC_PAGESIZE => crate::memreq::PAGE as i64, _SC_NPROCESSORS_ONLN => syscall::cpu_count() as i64, _SC_CLK_TCK => 100, // POSIX's -1 with `errno` untouched: the function each bounds, `exec` diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index d9bfbd6c7c4..ba24e383b7d 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -2,12 +2,16 @@ #![allow(non_camel_case_types)] +use alloc::boxed::Box; +use alloc::collections::BTreeSet; +use alloc::vec::Vec; use core::ptr; use toyos_abi::RawHandle; use toyos_abi::syscall::{self, OpenFlags, SeekFrom}; use crate::errno::{EACCES, EAGAIN, EEXIST, EINVAL, EIO, ENOENT, EPIPE}; +use crate::pthread::Lock; use crate::time::Timespec; // Constants (matching POSIX / Linux values) @@ -18,6 +22,7 @@ const O_RDWR: i32 = 2; const O_CREAT: i32 = 0x40; const O_TRUNC: i32 = 0x200; const O_APPEND: i32 = 0x400; +const O_CLOEXEC: i32 = 0x80000; const SEEK_SET: i32 = 0; const SEEK_CUR: i32 = 1; @@ -85,7 +90,12 @@ pub unsafe extern "C" fn open(path: *const u8, flags: i32, _mode: u32) -> i32 { if flags & O_APPEND != 0 { oflags |= OpenFlags::APPEND; } match syscall::open(path_bytes, oflags) { - Ok(f) => f.0 as i32, + Ok(f) => { + if flags & O_CLOEXEC != 0 { + mark_cloexec(f.0 as i32, true); + } + f.0 as i32 + } Err(e) => set_errno(e), } } @@ -97,6 +107,7 @@ pub unsafe extern "C" fn creat(path: *const u8, mode: u32) -> i32 { #[no_mangle] pub unsafe extern "C" fn close(raw_fd: i32) -> i32 { + mark_cloexec(raw_fd, false); syscall::close(fd(raw_fd)); 0 } @@ -199,6 +210,8 @@ pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { }; match syscall::dup2(fd(old_fd), slot) { Ok(f) => { + // POSIX's: the descriptor `dup2` answers is not closed on `exec`. + mark_cloexec(f.0 as i32, false); // The slot holds something else now, so the stream is asked again. match slot { 1 => toyos::log::stdio::forget(toyos::log::stdio::Stream::Out), @@ -312,27 +325,12 @@ pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { stat_impl(path, buf) } -/// `SYS_SYMLINK` displaces whatever `link` names, and POSIX refuses a name -/// that exists: asked first, so only a name another process makes in between -/// is still displaced. -#[no_mangle] -pub unsafe extern "C" fn symlink(target: *const u8, link: *const u8) -> i32 { - let is_link = syscall::readlink(c_str_to_bytes(link), &mut [0u8; 1]).is_ok(); - if is_link || unsafe { stat_impl(link, ptr::null_mut()) } == 0 { - crate::errno::set(EEXIST); - return -1; - } - match syscall::symlink(c_str_to_bytes(target), c_str_to_bytes(link)) { - Ok(()) => 0, - Err(e) => set_errno(e), - } -} - /// `SYS_READLINK` answers `NotFound` for a path that is no link as for one /// that names nothing, and POSIX tells them apart: `EINVAL` for the first. #[no_mangle] pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> isize { - if size == 0 { + // No slice is longer than `isize::MAX`, and the answer is an `isize`. + if size == 0 || isize::try_from(size).is_err() { crate::errno::set(EINVAL); return -1; } @@ -392,68 +390,62 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { old } -const F_DUPFD: i32 = 0; -const F_GETFD: i32 = 1; -const F_SETFD: i32 = 2; -const F_GETFL: i32 = 3; -const F_SETFL: i32 = 4; -const F_GETLK: i32 = 5; -const F_SETLK: i32 = 6; -const F_SETLKW: i32 = 7; -const F_SETOWN: i32 = 8; -const F_GETOWN: i32 = 9; -const F_DUPFD_CLOEXEC: i32 = 1030; +/// The descriptors marked close-on-exec, by number: kept for stage 3 of +/// `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`, +/// whose spawn reads them. A number is a slot at one generation, so a handle +/// made later in the slot is never taken for one marked here. +static CLOEXEC: Lock> = Lock::new(BTreeSet::new()); -#[no_mangle] -pub unsafe extern "C" fn fcntl(fd: i32, cmd: i32, arg: i64) -> i32 { - let refused = match cmd { - F_DUPFD if arg >= 0 => return unsafe { dup_at_least(fd, arg) }, - // Close-on-exec is the descriptor table's of stage 3 of - // issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md. - F_GETFD | F_SETFD => return 0, - // POSIX's answer for a file that supports no locking, which none here does. - F_GETLK | F_SETLK | F_SETLKW => EINVAL, - // A descriptor's status flags, its close-on-exec and its owner are - // nothing this library keeps. - F_DUPFD_CLOEXEC | F_GETFL | F_SETFL | F_GETOWN | F_SETOWN => crate::errno::ENOSYS, - _ => EINVAL, - }; - crate::errno::set(refused); - -1 +fn mark_cloexec(raw_fd: i32, cloexec: bool) { + let mut marked = CLOEXEC.lock(); + if cloexec { + marked.insert(raw_fd); + } else { + marked.remove(&raw_fd); + } } -/// A duplicate of `raw_fd` numbered `floor` or above. The kernel picks each -/// number `dup` answers, so this is the first such answer and not the lowest -/// free number, and every lower one taken on the way is closed again. -unsafe fn dup_at_least(raw_fd: i32, floor: i64) -> i32 { - let mut below = alloc::vec::Vec::new(); - let answer = loop { - let duplicate = unsafe { dup(raw_fd) }; - if duplicate < 0 || i64::from(duplicate) >= floor { - break duplicate; +/// `arg` is the register C's variadic third argument arrives in (`fdreq`). +#[no_mangle] +pub unsafe extern "C" fn fcntl(raw_fd: i32, cmd: i32, arg: u64) -> i32 { + use crate::fdreq::Command; + let refused = match crate::fdreq::command(cmd, arg) { + Command::DupAtLeast(floor) => { + let duplicate = crate::fdreq::dup_at_least( + floor, + || syscall::dup(fd(raw_fd)).map(|h| h.0), + |below| syscall::close(RawHandle(below)), + ); + return match duplicate { + Ok(n) => n as i32, + Err(e) => set_errno(e), + }; } - below.push(duplicate); + Command::GetFd => return i32::from(CLOEXEC.lock().contains(&raw_fd)), + Command::SetFd(cloexec) => { + mark_cloexec(raw_fd, cloexec); + return 0; + } + Command::Invalid => EINVAL, + Command::Unsupported => crate::errno::ENOSYS, }; - for lower in below { - unsafe { close(lower) }; - } - answer + crate::errno::set(refused); + -1 } // Directory operations -#[repr(C)] +/// An open directory: its whole listing, and where the next `readdir` starts. pub struct DIR { - buf: *mut u8, - len: usize, - pos: usize, + listing: Vec, + at: usize, } #[repr(C)] pub struct dirent { pub d_ino: u64, pub d_type: u8, - pub d_name: [u8; 256], + pub d_name: [u8; crate::listing::D_NAME], } /// `d_type`s: the listing tells a directory from everything else, and no more. @@ -463,67 +455,38 @@ const DT_DIR: u8 = 4; #[no_mangle] pub unsafe extern "C" fn opendir(path: *const u8) -> *mut DIR { let path_bytes = c_str_to_bytes(path); - let mut buf_size = 65536; - let mut buf = super::memory::malloc(buf_size); - if buf.is_null() { return ptr::null_mut(); } - - // `readdir` returns the size the listing *needs* and writes nothing when - // it does not fit, so taking the return as a length without checking it - // would hand `DIR` a `len` past the end of its own buffer. One retry at - // the reported size; the kernel bounds the listing, so it cannot run away. - let mut n = match syscall::readdir(path_bytes, core::slice::from_raw_parts_mut(buf, buf_size)) { - Ok(n) => n, - Err(e) => { super::memory::free(buf); set_errno(e); return ptr::null_mut(); } - }; - if n > buf_size { - super::memory::free(buf); - buf_size = n; - buf = super::memory::malloc(buf_size); - if buf.is_null() { return ptr::null_mut(); } - n = match syscall::readdir(path_bytes, core::slice::from_raw_parts_mut(buf, buf_size)) { - Ok(n) if n <= buf_size => n, - Ok(_) => { super::memory::free(buf); crate::errno::set(EAGAIN); return ptr::null_mut(); } - Err(e) => { super::memory::free(buf); set_errno(e); return ptr::null_mut(); } - }; - } - - let dir = super::memory::malloc(core::mem::size_of::()) as *mut DIR; - if dir.is_null() { - super::memory::free(buf); - return ptr::null_mut(); + match crate::listing::whole(|buf| syscall::readdir(path_bytes, buf)) { + Ok(listing) => Box::into_raw(Box::new(DIR { listing, at: 0 })), + Err(e) => { + set_errno(e); + ptr::null_mut() + } } - ptr::write(dir, DIR { buf, len: n, pos: 0 }); - dir } #[no_mangle] pub unsafe extern "C" fn readdir(dir: *mut DIR) -> *mut dirent { if dir.is_null() { return ptr::null_mut(); } let d = &mut *dir; - let start = d.pos; - let listing = core::slice::from_raw_parts(d.buf, d.len); - let Some(entry) = crate::listing::next(listing, &mut d.pos) else { return ptr::null_mut() }; + let start = d.at; + let Some(entry) = syscall::dirent(&d.listing, &mut d.at) else { return ptr::null_mut() }; + let Some(d_name) = crate::listing::d_name(entry.name) else { + crate::errno::set(crate::errno::EOVERFLOW); + return ptr::null_mut(); + }; // Use a static buffer for the dirent (not thread-safe, matching POSIX convention) - static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; 256] }; + static mut DIRENT_BUF: dirent = dirent { d_ino: 0, d_type: 0, d_name: [0; crate::listing::D_NAME] }; let ent = &raw mut DIRENT_BUF; - if entry.name.len() >= (*ent).d_name.len() { - crate::errno::set(crate::errno::EOVERFLOW); - return ptr::null_mut(); - } - (*ent).d_ino = (start + 1) as u64; - (*ent).d_type = if entry.is_dir { DT_DIR } else { DT_UNKNOWN }; - ptr::copy_nonoverlapping(entry.name.as_ptr(), (*ent).d_name.as_mut_ptr(), entry.name.len()); - (*ent).d_name[entry.name.len()] = 0; + ent.write(dirent { d_ino: (start + 1) as u64, d_type: if entry.is_dir { DT_DIR } else { DT_UNKNOWN }, d_name }); ent } #[no_mangle] pub unsafe extern "C" fn closedir(dir: *mut DIR) -> i32 { if dir.is_null() { return -1; } - let d = &*dir; - super::memory::free(d.buf); - super::memory::free(dir as *mut u8); + // SAFETY: a `DIR` `opendir` answered, closed once. + drop(unsafe { Box::from_raw(dir) }); 0 } diff --git a/userland/libc/src/refused.rs b/userland/libc/src/refused.rs index 115eeb1b7f1..ea313350d88 100644 --- a/userland/libc/src/refused.rs +++ b/userland/libc/src/refused.rs @@ -1,6 +1,4 @@ -//! What this C library refuses. Each function sets `errno` to `ENOSYS`, which -//! POSIX spells "functionality not supported", answers failure in its own -//! POSIX form, and does nothing else. What each waits on is +//! What this C library refuses. What each waits on is //! `issues/build/libc-refuses-what-toyos-cannot-yet-answer.md`'s. use core::ptr; @@ -54,6 +52,13 @@ pub unsafe extern "C" fn link(_existing: *const u8, _new: *const u8) -> i32 { refuse() } +/// `SYS_SYMLINK` displaces whatever holds the name, where POSIX refuses one +/// that exists, and a question asked first races its answer. +#[no_mangle] +pub unsafe extern "C" fn symlink(_target: *const u8, _link: *const u8) -> i32 { + refuse() +} + /// A file has no owner. #[no_mangle] pub extern "C" fn fchown(_fd: i32, _owner: u32, _group: u32) -> i32 { From e14f7af23f6fc24cb970d97f14036b48544c5158 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 08:31:11 +0200 Subject: [PATCH 10/12] libc: readlink's size refusal host-tested, the signal-set calls, ENOMEM #650's third review. readlink refused a size above isize::MAX before the kernel was asked, and no test could see it: the kernel refuses such a buffer as well, and set_errno's catch-all maps that to the same EINVAL. The size decision is now linkreq::target_len, pure, and toyos-libc-copies' link_requests holds it to 0, isize::MAX + 1 and usize::MAX refused and 1 to isize::MAX taken. 207 reads /system/bin/cat, a link the image holds, whole, into 4 bytes and into 0: the kernel answers 0 for a link read into no bytes, so the 0 case reds if libc stops refusing it. The two readlink lines that could not fail on libc's check go. sigemptyset, sigfillset and sigaddset: bit operations on the sigset_t layout sigmask.rs fixes, the first error in M2's LLVM build. sigaddset refuses a number no bit stands for, EINVAL, as POSIX has it; sigfillset answers every signal a sigset_t has a bit for, since no number is the library's own. signal_masks holds both to POSIX's words and the full set to every signal signal.h defines; 208 reads each back, and errno. The issues that named them as stage 3's lose them. fcntl's close-on-exec marks are fdreq::CloseOnExec, whose transitions (open, F_SETFD, F_GETFD, close, dup2's answer) descriptor_requests now walks with fcntl.h's values; F_DUPFD at a floor the first duplicate meets answers that duplicate, pinned. F_GETFD and F_SETFD of a descriptor not held answering 0 is recorded in the refusals issue. libc's own malloc, calloc and realloc set ENOMEM when they answer null, as aligned_alloc did; 206 reads it back for each. The allocator issue keeps std's half. The /tmp symlink issue names CMake's create_symlink, which an LLVM build on ToyOS reaches through add_llvm_tool_symlink. 207's header loses the clause about creat and readlink. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...ild-llvm-clang-and-lld-for-a-toyos-host.md | 5 +- issues/build/libc-has-no-alarm.md | 4 +- ...bc-refuses-what-toyos-cannot-yet-answer.md | 4 +- ...ators-set-no-errno-where-posix-has-them.md | 16 +++--- issues/build/toyos-builds-itself.md | 4 +- ...mp-displaces-its-name-and-lists-nowhere.md | 5 +- tests/testcases/tinycc/206_libc_refusals.c | 16 +++++- .../testcases/tinycc/206_libc_refusals.expect | 3 ++ tests/testcases/tinycc/207_libc_names.c | 19 ++++--- tests/testcases/tinycc/207_libc_names.expect | 5 +- tests/testcases/tinycc/208_signal_mask.c | 18 ++++++- tests/testcases/tinycc/208_signal_mask.expect | 5 ++ toyos-libc-copies/src/descriptor_requests.rs | 53 +++++++++++++++++-- toyos-libc-copies/src/lib.rs | 5 ++ toyos-libc-copies/src/link_requests.rs | 14 +++++ toyos-libc-copies/src/signal_masks.rs | 37 ++++++++++++- userland/libc/include/signal.h | 3 ++ userland/libc/src/fdreq.rs | 43 ++++++++++++++- userland/libc/src/lib.rs | 1 + userland/libc/src/linkreq.rs | 10 ++++ userland/libc/src/memory.rs | 25 +++++---- userland/libc/src/misc.rs | 26 +++++++++ userland/libc/src/posix_io.rs | 39 ++++---------- userland/libc/src/sigmask.rs | 14 ++++- 24 files changed, 300 insertions(+), 74 deletions(-) create mode 100644 toyos-libc-copies/src/link_requests.rs create mode 100644 userland/libc/src/linkreq.rs diff --git a/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md b/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md index 91ecaef0d40..d495d5193b1 100644 --- a/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md +++ b/issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md @@ -30,9 +30,8 @@ The last three are ToyOS arms at existing dispatch sites, written as upstream would take them. ToyOS joins `bit.h`'s `` list, so libc carries POSIX's `endian.h` (`userland/libc/include/endian.h`) and no BSD name. With those two arms on `849da7d62` and CMake told the system by hand, the 71 -libraries rustc links compile against the C sysroot but for five objects of -`LLVMSupport`, which stop on `alarm` and on stage 3's `wait`, `wait4` and -signal-set calls; without them, every object that includes `bit.h` stops. +libraries rustc links compile against the C sysroot but for objects of +`LLVMSupport`; without them, every object that includes `bit.h` stops. Bootstrap's arm names the system `ToyOS`, which LLVM's configure refuses, `Unable to determine platform`, until CMake knows ToyOS and sets `UNIX` (`issues/build/the-cxx-runtime-names-toyos-to-cmake-as-unix.md`). diff --git a/issues/build/libc-has-no-alarm.md b/issues/build/libc-has-no-alarm.md index b3c7054aa15..1a5a26eda49 100644 --- a/issues/build/libc-has-no-alarm.md +++ b/issues/build/libc-has-no-alarm.md @@ -10,8 +10,8 @@ libc neither declares nor defines `alarm`, and LLVM bounds its wait on a child with one: a `SIGALRM` handler makes `wait4` answer `EINTR` (`llvm/lib/Support/Unix/Program.inc`, `Wait`), so an LLVM built for ToyOS does not link without it: rustc's LLVM wrapper linked `-shared -z defs` with the -libraries it needs leaves it undefined, beside stage 3's `wait`, `wait4` and -signal-set calls. POSIX gives `alarm` no refusal, and its `SIGALRM` reaches +libraries it needs leaves it undefined, beside stage 3's `wait` and `wait4`. +POSIX gives `alarm` no refusal, and its `SIGALRM` reaches a handler only through the signals libc imitates from stage 3 of `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` on; this waits on that stage. diff --git a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md index 3e910d30e8c..aec0c62ef78 100644 --- a/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -35,7 +35,9 @@ These answer failure in their POSIX form and do nothing `ENOSYS`. `F_DUPFD` answers a duplicate at or above its argument, the first the kernel hands `dup`, where POSIX has the lowest free number, and refuses `EINVAL` an argument at or above the slots a handle table has - (`RawHandle::MAX_SLOTS`). + (`RawHandle::MAX_SLOTS`). `F_GETFD` and `F_SETFD` of a descriptor the + process does not hold answer 0, where POSIX has `EBADF`: the kernel ends a + process that names a handle it does not hold, so libc asks it nothing. Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no call replacing a process's image; `setsid` and `getsid`, no POSIX session; diff --git a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md index 9dc1693e7ff..3a20fb2ccc2 100644 --- a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md +++ b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -10,13 +10,11 @@ Read from the code, not run. POSIX.1-2024 has `malloc`, `calloc`, `realloc` and `aligned_alloc` set `ENOMEM` when they answer null for want of memory, and `aligned_alloc` set `EINVAL` for an alignment it does not support. -- In a Rust program, C code allocates through std's C allocator - (`rust/library/std/src/sys/pal/toyos/mod.rs`, `c_allocator`), which sets - `errno` in none of the four. `errno` is libc's (`userland/libc/src/errno.rs`), - and std names no libc symbol: setting it from std puts `__errno_location` in - every Rust program's link. -- In a C program, libc's own allocator (`userland/libc/src/memory.rs`) sets it - in `aligned_alloc` alone. +In a Rust program, C code allocates through std's C allocator +(`rust/library/std/src/sys/pal/toyos/mod.rs`, `c_allocator`), which sets +`errno` in none of the four. `errno` is libc's (`userland/libc/src/errno.rs`), +and std names no libc symbol: setting it from std puts `__errno_location` in +every Rust program's link. -**Exit**: each of the four sets `errno` where POSIX has it, in a C program and -in a Rust one, and a C call in each reads `ENOMEM` and `EINVAL` back. +**Exit**: each of the four sets `errno` where POSIX has it in a Rust program, +and a C call there reads `ENOMEM` and `EINVAL` back. diff --git a/issues/build/toyos-builds-itself.md b/issues/build/toyos-builds-itself.md index 53db17f659b..ee911f995f3 100644 --- a/issues/build/toyos-builds-itself.md +++ b/issues/build/toyos-builds-itself.md @@ -57,8 +57,8 @@ the published crates.io crate is yanked. each blocks the next. - Configure: the `clang-tblgen` and the CMake system of `issues/build/bootstrap-cannot-build-llvm-clang-and-lld-for-a-toyos-host.md`. -- Compile: the bootstrap issue's `bit.h` and `is_local_impl`, and the - signal-set calls, `wait` and `wait4` of the child-process track's stage 3 +- Compile: the bootstrap issue's `bit.h` and `is_local_impl`, and `wait` and + `wait4` of the child-process track's stage 3 with `issues/build/libc-has-no-alarm.md`. - Link: the same stage 3 names and `alarm`. - Run: what `issues/build/libc-refuses-what-toyos-cannot-yet-answer.md` lists, diff --git a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md index 7ba44e0f727..770b54b37ae 100644 --- a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md +++ b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -17,4 +17,7 @@ under the lock that makes the link, and a listing of `/tmp` names each link it holds, each asserted by a guest case. libc's `symlink` calls it with nothing asked first and answers that refusal `EEXIST`, which is how LLVM's `LockFileManager` takes its lock (`create_link`, `::symlink` in -`llvm/lib/Support/Unix/Path.inc`). +`llvm/lib/Support/Unix/Path.inc`). An LLVM build on ToyOS reaches it too: +`LLVM_USE_SYMLINKS` is on for a UNIX host, so `add_llvm_tool_symlink` +(`llvm/cmake/modules/AddLLVM.cmake`) makes each tool's aliases with CMake's +`create_symlink`. diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 31e5af953a6..09d3195c5d2 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -1,9 +1,11 @@ /* What libc refuses: each call answers failure in its own POSIX form, errno says why, and nothing is done: ENOSYS where ToyOS lacks the function, and - the errno POSIX names for a lock or a mapping it cannot take. */ + the errno POSIX names for a lock or a mapping it cannot take, or memory it + cannot give. */ #include #include #include +#include #include #include #include @@ -22,6 +24,7 @@ static const char *errno_name(int e) { case EINVAL: return "EINVAL"; case ENODEV: return "ENODEV"; case ENOTSUP: return "ENOTSUP"; + case ENOMEM: return "ENOMEM"; default: return "another errno"; } } @@ -122,6 +125,17 @@ int main(void) { said("mmap executable", mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); said("mmap nothing", mmap(NULL, 0, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == MAP_FAILED ? -1 : 0); + /* The allocators: null and ENOMEM for a size no block has. Each answer is + stored, so the compiler cannot fold an unused allocation to non-null. */ + void *volatile got = malloc(SIZE_MAX); + said("malloc SIZE_MAX", got == NULL ? -1 : 0); + got = calloc(SIZE_MAX, 2); + said("calloc SIZE_MAX 2", got == NULL ? -1 : 0); + char *block = malloc(16); + got = realloc(block, SIZE_MAX); + said("realloc SIZE_MAX", got == NULL ? -1 : 0); + free(block); + /* sysconf: -1 with errno untouched for a limit on what ToyOS lacks, and EINVAL for a name it does not know. */ said("sysconf _SC_ARG_MAX", sysconf(_SC_ARG_MAX)); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 67836898eb6..9b2923e5553 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -38,6 +38,9 @@ mmap a file shared: -1, ENODEV mmap a file shared at a page: -1, ENODEV mmap executable: -1, ENOTSUP mmap nothing: -1, EINVAL +malloc SIZE_MAX: -1, ENOMEM +calloc SIZE_MAX 2: -1, ENOMEM +realloc SIZE_MAX: -1, ENOMEM sysconf _SC_ARG_MAX: -1, no errno sysconf _SC_GETPW_R_SIZE_MAX: -1, no errno sysconf 12345: -1, EINVAL diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index 591be91a019..00302a630b0 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -1,8 +1,7 @@ -/* What libc does for the names LLVM builds against: creat and readlink as - POSIX has them, each read back; a directory listed with the name - and kind of each entry; dladdr naming the image and the exported symbol an - address lies in; and strnlen, strsignal, modf, logb and the - conversions. */ +/* What libc does for the names LLVM builds against: a directory listed with + the name and kind of each entry; dladdr naming the image and the exported + symbol an address lies in; and strnlen, strsignal, modf, logb and the + conversions. */ #include #include #include @@ -25,6 +24,8 @@ #define DIR_PATH "/tmp/207_libc_names" #define TARGET DIR_PATH "/target" #define LISTED DIR_PATH "/listed" +/* A link the image holds, to /system/bin/toybox. */ +#define LINK "/system/bin/cat" /* The image's test library, which exports tls_get_label. */ #define LIB "/system/lib/libtls_lib.so" @@ -61,10 +62,14 @@ static void links(void) { if (fd >= 0) close(fd); + n = readlink(LINK, buf, sizeof buf); + printf("readlink: %zd, \"%.*s\"\n", n, n > 0 ? (int)n : 0, buf); + memset(buf, '#', sizeof buf); + n = readlink(LINK, buf, 4); + printf("readlink into 4: %zd, \"%.5s\"\n", n, buf); + refused("readlink into 0", readlink(LINK, buf, 0)); refused("readlink of a file", readlink(TARGET, buf, sizeof buf)); refused("readlink of nothing", readlink(DIR_PATH "/none", buf, sizeof buf)); - refused("readlink into 0", readlink(TARGET, buf, 0)); - refused("readlink into SIZE_MAX", readlink(DIR_PATH "/none", buf, SIZE_MAX)); } static void listing(void) { diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect index e9cab6abf9a..3f482d5dbde 100644 --- a/tests/testcases/tinycc/207_libc_names.expect +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -1,8 +1,9 @@ read what creat wrote: "hello" +readlink: 18, "/system/bin/toybox" +readlink into 4: 4, "/sys#" +readlink into 0: -1, EINVAL readlink of a file: -1, EINVAL readlink of nothing: -1, ENOENT -readlink into 0: -1, EINVAL -readlink into SIZE_MAX: -1, EINVAL entry: a DT_UNKNOWN entry: b DT_UNKNOWN entry: sub DT_DIR diff --git a/tests/testcases/tinycc/208_signal_mask.c b/tests/testcases/tinycc/208_signal_mask.c index 36088c3ebcd..5cba12f24fa 100644 --- a/tests/testcases/tinycc/208_signal_mask.c +++ b/tests/testcases/tinycc/208_signal_mask.c @@ -1,7 +1,9 @@ /* A thread's signal mask, as pthread_sigmask and sigprocmask keep it: each change read back by the next call, SIGKILL and SIGSTOP never in it, a how that is none of POSIX's three refused, and a new thread starting with its - creator's mask. Signal n is bit n - 1 of a sigset_t. */ + creator's mask; and a set as sigemptyset, sigaddset and sigfillset make + one, a number no signal has refused. Signal n is bit n - 1 of a + sigset_t. */ #include #include #include @@ -68,5 +70,19 @@ int main(void) { old = 0xdead; answer = sigprocmask(SIG_SETMASK, &none, &old); changed("set none", answer, old == everything ? 1 : 0); + + sigset_t made = 0xdead; + answer = sigemptyset(&made); + printf("sigemptyset: %d, 0x%lx\n", answer, made); + answer = sigaddset(&made, SIGUSR1); + printf("sigaddset USR1: %d, 0x%lx\n", answer, made); + errno = 0; + answer = sigaddset(&made, 0); + printf("sigaddset 0: %d, %s, 0x%lx\n", answer, errno == EINVAL ? "EINVAL" : "another errno", made); + errno = 0; + answer = sigaddset(&made, 65); + printf("sigaddset 65: %d, %s, 0x%lx\n", answer, errno == EINVAL ? "EINVAL" : "another errno", made); + answer = sigfillset(&made); + printf("sigfillset: %d, 0x%lx\n", answer, made); return 0; } diff --git a/tests/testcases/tinycc/208_signal_mask.expect b/tests/testcases/tinycc/208_signal_mask.expect index 91ae47292ee..31ed13d99b2 100644 --- a/tests/testcases/tinycc/208_signal_mask.expect +++ b/tests/testcases/tinycc/208_signal_mask.expect @@ -8,3 +8,8 @@ how 99: EINVAL, the mask as it was sigprocmask how 99: -1, EINVAL how 99 with no set: 0, old the mask set none: 0, old 0x1, now 0x0 +sigemptyset: 0, 0x0 +sigaddset USR1: 0, 0x200 +sigaddset 0: -1, EINVAL, 0x200 +sigaddset 65: -1, EINVAL, 0x200 +sigfillset: 0, 0xffffffffffffffff diff --git a/toyos-libc-copies/src/descriptor_requests.rs b/toyos-libc-copies/src/descriptor_requests.rs index 3e15b23e57b..fd2840ccfcd 100644 --- a/toyos-libc-copies/src/descriptor_requests.rs +++ b/toyos-libc-copies/src/descriptor_requests.rs @@ -1,6 +1,6 @@ -//! What `fcntl` decides before the kernel is asked, and `F_DUPFD`'s walk to -//! its floor over a handle table numbered as the kernel numbers one -//! (`kernel/src/object/handle.rs`). +//! What `fcntl` decides before the kernel is asked, `F_DUPFD`'s walk to its +//! floor over a handle table numbered as the kernel numbers one +//! (`kernel/src/object/handle.rs`), and each descriptor's close-on-exec mark. use crate::fdreq::{self, Command}; @@ -106,8 +106,55 @@ fn f_dupfd_meets_its_floor_holding_one_duplicate_at_most() { ) .unwrap(); assert!(answer >= floor, "floor {floor} answered {answer:#x} (churned: {churned})"); + if !churned && floor <= 4 { + // The first duplicate, slot 4, meets the floor and is answered. + assert_eq!(answer, 4, "floor {floor}"); + } assert_eq!(most, 1, "floor {floor} held {most} duplicates at once (churned: {churned})"); assert_eq!(table.borrow().holding(), before + 1, "floor {floor} left duplicates open (churned: {churned})"); } } } + +/// `fcntl.h`'s value for `name`, which a C program passes. +fn fcntl_h(name: &str) -> i32 { + let line = include_str!("../../userland/libc/include/fcntl.h") + .lines() + .find_map(|line| line.strip_prefix("#define ")?.strip_prefix(name)?.strip_prefix(char::is_whitespace)) + .unwrap_or_else(|| panic!("fcntl.h defines no {name}")); + let value = line.trim(); + match value.strip_prefix("0x") { + Some(hex) => i32::from_str_radix(hex, 16), + None => value.parse(), + } + .unwrap_or_else(|_| panic!("fcntl.h: {name} {value}")) +} + +/// A descriptor's close-on-exec mark through the calls that touch it: `open` +/// with and without `O_CLOEXEC`, `F_SETFD` either way, `F_GETFD`, `close`, and +/// `dup2`'s answer, which POSIX has not closed on `exec`. +#[test] +fn each_descriptor_keeps_its_own_close_on_exec_mark() { + let (cloexec, read_only, fd_cloexec) = (fcntl_h("O_CLOEXEC"), fcntl_h("O_RDONLY"), fcntl_h("FD_CLOEXEC")); + let mut marks = fdreq::CloseOnExec::new(); + marks.opened(3, read_only | cloexec); + marks.opened(4, read_only); + assert_eq!((marks.flags(3), marks.flags(4)), (fd_cloexec, 0), "open"); + + marks.set(3, false); + marks.set(4, true); + assert_eq!((marks.flags(3), marks.flags(4)), (0, fd_cloexec), "F_SETFD"); + + // `close`, and a later handle in slot 4 (one generation on), which is a + // number of its own. + marks.cleared(4); + assert_eq!(marks.flags(4), 0, "close"); + marks.set(4, true); + assert_eq!(marks.flags(4 | 1 << 12), 0, "the next generation of slot 4"); + + // `dup2`'s answer, and `open` without `O_CLOEXEC` of a number marked. + marks.cleared(4); + marks.set(5, true); + marks.opened(5, read_only); + assert_eq!((marks.flags(4), marks.flags(5)), (0, 0), "dup2, and open"); +} diff --git a/toyos-libc-copies/src/lib.rs b/toyos-libc-copies/src/lib.rs index 625b9493b0b..95265fd8f18 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -24,6 +24,9 @@ mod fdreq; #[path = "../../userland/libc/src/fparts.rs"] mod fparts; #[cfg(test)] +#[path = "../../userland/libc/src/linkreq.rs"] +mod linkreq; +#[cfg(test)] #[path = "../../userland/libc/src/listing.rs"] mod listing; #[cfg(test)] @@ -51,6 +54,8 @@ mod errno_codes; #[cfg(test)] mod fparts_differential; #[cfg(test)] +mod link_requests; +#[cfg(test)] mod listing_reader; #[cfg(all(test, target_arch = "aarch64"))] mod long_double; diff --git a/toyos-libc-copies/src/link_requests.rs b/toyos-libc-copies/src/link_requests.rs new file mode 100644 index 00000000000..3ba3ef1bce6 --- /dev/null +++ b/toyos-libc-copies/src/link_requests.rs @@ -0,0 +1,14 @@ +//! The buffer `readlink` refuses before the kernel is asked: none, and one +//! longer than `{SSIZE_MAX}`. + +use crate::linkreq; + +#[test] +fn readlink_refuses_an_empty_buffer_and_one_no_ssize_t_counts() { + for size in [1, 4, 4096, isize::MAX as usize] { + assert_eq!(linkreq::target_len(size), Some(size), "{size}"); + } + for size in [0, isize::MAX as usize + 1, usize::MAX] { + assert_eq!(linkreq::target_len(size), None, "{size}"); + } +} diff --git a/toyos-libc-copies/src/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs index 8862e47fa85..cf5dacdee75 100644 --- a/toyos-libc-copies/src/signal_masks.rs +++ b/toyos-libc-copies/src/signal_masks.rs @@ -1,7 +1,8 @@ //! A signal mask as `pthread_sigmask` changes it, against POSIX's words for //! each `how`: `SIG_BLOCK` the union, `SIG_UNBLOCK` the intersection with the //! complement, `SIG_SETMASK` the set itself; `SIGKILL` and `SIGSTOP` never in -//! it; any other `how` refused. +//! it; any other `how` refused. A set as `sigaddset` and `sigfillset` make one, +//! against POSIX's words for each. use crate::sigmask::{self, SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; @@ -25,3 +26,37 @@ fn each_how_changes_the_mask_as_posix_says() { assert_eq!(sigmask::changed(hup, how, usr1), None, "how {how}"); } } + +/// POSIX's `sigaddset`: a number no signal has is refused, `EINVAL` (`None`), +/// and any other is added alone, however often. +#[test] +fn sigaddset_adds_its_signal_alone_and_refuses_a_number_no_signal_has() { + for n in 1..=64 { + assert_eq!(sigmask::with(0, n), Some(bit(n as u32)), "signal {n}"); + assert_eq!(sigmask::with(bit(n as u32), n), Some(bit(n as u32)), "signal {n} twice"); + } + assert_eq!(sigmask::with(bit(1), 10), Some(bit(1) | bit(10))); + for n in [0, -1, 65, i32::MIN, i32::MAX] { + assert_eq!(sigmask::with(bit(1), n), None, "signal {n}"); + } +} + +/// POSIX's `sigfillset`: every signal `signal.h` defines is in the full set, +/// and the full set is every signal `sigaddset` takes. +#[test] +fn the_full_set_holds_every_signal() { + let defined: Vec = include_str!("../../userland/libc/include/signal.h") + .lines() + .filter_map(|line| line.strip_prefix("#define SIG")) + .filter(|rest| !rest.starts_with('_')) + .map(|rest| { + let value = rest.split_whitespace().nth(1); + value.and_then(|n| n.parse().ok()).unwrap_or_else(|| panic!("signal.h: SIG{rest}")) + }) + .collect(); + assert!(defined.len() > 15, "signal.h defines {} signals", defined.len()); + for n in defined { + assert_ne!(sigmask::FULL & bit(n), 0, "signal {n}"); + } + assert_eq!((1..=64).fold(0, |set, n| sigmask::with(set, n).unwrap()), sigmask::FULL); +} diff --git a/userland/libc/include/signal.h b/userland/libc/include/signal.h index 4bb62cc793e..e430ed95528 100644 --- a/userland/libc/include/signal.h +++ b/userland/libc/include/signal.h @@ -63,6 +63,9 @@ struct sigaction { }; int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); +int sigemptyset(sigset_t *set); +int sigfillset(sigset_t *set); +int sigaddset(sigset_t *set, int signo); int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); int kill(int pid, int sig); diff --git a/userland/libc/src/fdreq.rs b/userland/libc/src/fdreq.rs index ff627368a2d..50da5303729 100644 --- a/userland/libc/src/fdreq.rs +++ b/userland/libc/src/fdreq.rs @@ -1,8 +1,13 @@ -//! What `fcntl` decides before the kernel is asked. It reads nothing but what +//! What `fcntl` decides before the kernel is asked, and the close-on-exec +//! marks `open`, `fcntl`, `close` and `dup2` keep. It reads nothing but what //! it is handed, so the host tests it (`toyos-libc-copies`). +use alloc::collections::BTreeSet; + use toyos_abi::RawHandle; +const O_CLOEXEC: i32 = 0x80000; + const F_DUPFD: i32 = 0; const F_GETFD: i32 = 1; const F_SETFD: i32 = 2; @@ -71,3 +76,39 @@ pub(crate) fn dup_at_least( close(duplicate); } } + +/// The descriptors marked close-on-exec, by number: kept for stage 3 of +/// `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`, +/// whose spawn reads them. A number is a slot at one generation, so a handle +/// made later in the slot is never taken for one marked here. +pub(crate) struct CloseOnExec(BTreeSet); + +impl CloseOnExec { + pub(crate) const fn new() -> CloseOnExec { + CloseOnExec(BTreeSet::new()) + } + + /// `fd` as `open` answered it for `flags`: marked if they hold `O_CLOEXEC`. + pub(crate) fn opened(&mut self, fd: i32, flags: i32) { + self.set(fd, flags & O_CLOEXEC != 0); + } + + /// `F_SETFD`'s. + pub(crate) fn set(&mut self, fd: i32, cloexec: bool) { + if cloexec { + self.0.insert(fd); + } else { + self.0.remove(&fd); + } + } + + /// `F_GETFD`'s answer. + pub(crate) fn flags(&self, fd: i32) -> i32 { + if self.0.contains(&fd) { FD_CLOEXEC } else { 0 } + } + + /// `fd` closed, or answered by `dup2`, which POSIX has not closed on `exec`. + pub(crate) fn cleared(&mut self, fd: i32) { + self.0.remove(&fd); + } +} diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index b73fe28fafc..8b184115473 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -11,6 +11,7 @@ mod errno; mod fdreq; mod fparts; mod link; +mod linkreq; mod listing; mod locale; mod math; diff --git a/userland/libc/src/linkreq.rs b/userland/libc/src/linkreq.rs new file mode 100644 index 00000000000..5793b382cb4 --- /dev/null +++ b/userland/libc/src/linkreq.rs @@ -0,0 +1,10 @@ +//! What `readlink` decides before the kernel is asked. It reads nothing but +//! what it is handed, so the host tests it (`toyos-libc-copies`). + +/// The bytes a `readlink` buffer of `size` is read as, or `None` for a size it +/// refuses `EINVAL`: 0, as Linux's does, and one above `{SSIZE_MAX}`, whose +/// result POSIX leaves to the implementation: no slice is that long, and no +/// `ssize_t` counts it. +pub(crate) fn target_len(size: usize) -> Option { + (size != 0 && isize::try_from(size).is_ok()).then_some(size) +} diff --git a/userland/libc/src/memory.rs b/userland/libc/src/memory.rs index 906b4094a39..bc4d60e99a7 100644 --- a/userland/libc/src/memory.rs +++ b/userland/libc/src/memory.rs @@ -85,7 +85,17 @@ pub unsafe extern "C" fn malloc(size: usize) -> *mut u8 { if size == 0 { return ptr::null_mut(); } - unsafe { backend::alloc(size, 16) } + or_enomem(unsafe { backend::alloc(size, 16) }) +} + +/// `p`, with `errno` set to `ENOMEM` if it is null: POSIX's for an allocator +/// that answers null for want of memory. +#[cfg(not(feature = "std-runtime"))] +fn or_enomem(p: *mut u8) -> *mut u8 { + if p.is_null() { + crate::errno::set(crate::errno::ENOMEM); + } + p } #[cfg(not(feature = "std-runtime"))] @@ -95,11 +105,7 @@ pub unsafe extern "C" fn aligned_alloc(align: usize, size: usize) -> *mut u8 { crate::errno::set(crate::errno::EINVAL); return ptr::null_mut(); } - let p = unsafe { backend::alloc(size, align) }; - if p.is_null() { - crate::errno::set(crate::errno::ENOMEM); - } - p + or_enomem(unsafe { backend::alloc(size, align) }) } #[cfg(not(feature = "std-runtime"))] @@ -128,10 +134,7 @@ pub unsafe extern "C" fn free(p: *mut u8) { #[cfg(not(feature = "std-runtime"))] #[no_mangle] pub unsafe extern "C" fn calloc(count: usize, size: usize) -> *mut u8 { - let total = match count.checked_mul(size) { - Some(t) => t, - None => return ptr::null_mut(), - }; + let Some(total) = count.checked_mul(size) else { return or_enomem(ptr::null_mut()) }; let p = unsafe { malloc(total) }; if !p.is_null() { unsafe { ptr::write_bytes(p, 0, total); } @@ -149,7 +152,7 @@ pub unsafe extern "C" fn realloc(p: *mut u8, new_size: usize) -> *mut u8 { unsafe { free(p); } return ptr::null_mut(); } - unsafe { backend::realloc(p, new_size) } + or_enomem(unsafe { backend::realloc(p, new_size) }) } // memcpy, memmove and memset are the architecture's (`arch`): Rust's diff --git a/userland/libc/src/misc.rs b/userland/libc/src/misc.rs index 547e1f54e35..ba2b635eeb6 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -224,6 +224,32 @@ pub unsafe extern "C" fn sigprocmask(how: i32, set: *const u64, oldset: *mut u64 } } +#[no_mangle] +pub unsafe extern "C" fn sigemptyset(set: *mut u64) -> i32 { + unsafe { *set = 0 }; + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn sigfillset(set: *mut u64) -> i32 { + unsafe { *set = crate::sigmask::FULL }; + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn sigaddset(set: *mut u64, signo: i32) -> i32 { + match crate::sigmask::with(unsafe { *set }, signo) { + Some(next) => { + unsafe { *set = next }; + 0 + } + None => { + crate::errno::set(crate::errno::EINVAL); + -1 + } + } +} + #[no_mangle] pub unsafe extern "C" fn raise(_sig: i32) -> i32 { 0 diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index ba24e383b7d..8ed0d75b82a 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -3,7 +3,6 @@ #![allow(non_camel_case_types)] use alloc::boxed::Box; -use alloc::collections::BTreeSet; use alloc::vec::Vec; use core::ptr; @@ -11,6 +10,7 @@ use toyos_abi::RawHandle; use toyos_abi::syscall::{self, OpenFlags, SeekFrom}; use crate::errno::{EACCES, EAGAIN, EEXIST, EINVAL, EIO, ENOENT, EPIPE}; +use crate::fdreq::CloseOnExec; use crate::pthread::Lock; use crate::time::Timespec; @@ -22,7 +22,6 @@ const O_RDWR: i32 = 2; const O_CREAT: i32 = 0x40; const O_TRUNC: i32 = 0x200; const O_APPEND: i32 = 0x400; -const O_CLOEXEC: i32 = 0x80000; const SEEK_SET: i32 = 0; const SEEK_CUR: i32 = 1; @@ -91,9 +90,7 @@ pub unsafe extern "C" fn open(path: *const u8, flags: i32, _mode: u32) -> i32 { match syscall::open(path_bytes, oflags) { Ok(f) => { - if flags & O_CLOEXEC != 0 { - mark_cloexec(f.0 as i32, true); - } + CLOEXEC.lock().opened(f.0 as i32, flags); f.0 as i32 } Err(e) => set_errno(e), @@ -107,7 +104,7 @@ pub unsafe extern "C" fn creat(path: *const u8, mode: u32) -> i32 { #[no_mangle] pub unsafe extern "C" fn close(raw_fd: i32) -> i32 { - mark_cloexec(raw_fd, false); + CLOEXEC.lock().cleared(raw_fd); syscall::close(fd(raw_fd)); 0 } @@ -210,8 +207,7 @@ pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { }; match syscall::dup2(fd(old_fd), slot) { Ok(f) => { - // POSIX's: the descriptor `dup2` answers is not closed on `exec`. - mark_cloexec(f.0 as i32, false); + CLOEXEC.lock().cleared(f.0 as i32); // The slot holds something else now, so the stream is asked again. match slot { 1 => toyos::log::stdio::forget(toyos::log::stdio::Stream::Out), @@ -329,12 +325,12 @@ pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { /// that names nothing, and POSIX tells them apart: `EINVAL` for the first. #[no_mangle] pub unsafe extern "C" fn readlink(path: *const u8, buf: *mut u8, size: usize) -> isize { - // No slice is longer than `isize::MAX`, and the answer is an `isize`. - if size == 0 || isize::try_from(size).is_err() { + let Some(len) = crate::linkreq::target_len(size) else { crate::errno::set(EINVAL); return -1; - } - let target = unsafe { core::slice::from_raw_parts_mut(buf, size) }; + }; + // SAFETY: C's caller hands `size` bytes at `buf`. + let target = unsafe { core::slice::from_raw_parts_mut(buf, len) }; match syscall::readlink(c_str_to_bytes(path), target) { Ok(n) => n as isize, Err(syscall::SyscallError::NotFound) => { @@ -390,20 +386,7 @@ pub unsafe extern "C" fn umask(mask: u32) -> u32 { old } -/// The descriptors marked close-on-exec, by number: kept for stage 3 of -/// `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`, -/// whose spawn reads them. A number is a slot at one generation, so a handle -/// made later in the slot is never taken for one marked here. -static CLOEXEC: Lock> = Lock::new(BTreeSet::new()); - -fn mark_cloexec(raw_fd: i32, cloexec: bool) { - let mut marked = CLOEXEC.lock(); - if cloexec { - marked.insert(raw_fd); - } else { - marked.remove(&raw_fd); - } -} +static CLOEXEC: Lock = Lock::new(CloseOnExec::new()); /// `arg` is the register C's variadic third argument arrives in (`fdreq`). #[no_mangle] @@ -421,9 +404,9 @@ pub unsafe extern "C" fn fcntl(raw_fd: i32, cmd: i32, arg: u64) -> i32 { Err(e) => set_errno(e), }; } - Command::GetFd => return i32::from(CLOEXEC.lock().contains(&raw_fd)), + Command::GetFd => return CLOEXEC.lock().flags(raw_fd), Command::SetFd(cloexec) => { - mark_cloexec(raw_fd, cloexec); + CLOEXEC.lock().set(raw_fd, cloexec); return 0; } Command::Invalid => EINVAL, diff --git a/userland/libc/src/sigmask.rs b/userland/libc/src/sigmask.rs index 4eca5646889..9c665db43a5 100644 --- a/userland/libc/src/sigmask.rs +++ b/userland/libc/src/sigmask.rs @@ -1,4 +1,5 @@ -//! A thread's signal mask as `pthread_sigmask` and `sigprocmask` change it: +//! A `sigset_t` as `sigemptyset`, `sigfillset` and `sigaddset` make one, and a +//! thread's signal mask as `pthread_sigmask` and `sigprocmask` change it: //! signal `n` is bit `n - 1` of a `sigset_t`, Linux's layout. It reads and //! sets nothing but what it is handed, so the host tests it //! (`toyos-libc-copies`). @@ -14,6 +15,17 @@ const SIGSTOP: u32 = 19; /// without an error. const UNBLOCKABLE: u64 = (1 << (SIGKILL - 1)) | (1 << (SIGSTOP - 1)); +/// What `sigfillset` answers: every signal a `sigset_t` has a bit for, since +/// no signal number is this library's own. +pub(crate) const FULL: u64 = u64::MAX; + +/// `set` with signal `signo` in it, or `None` for a number no bit stands for, +/// which POSIX's `sigaddset` refuses `EINVAL`. +pub(crate) fn with(set: u64, signo: i32) -> Option { + let n = u32::try_from(signo).ok().filter(|n| (1..=u64::BITS).contains(n))?; + Some(set | 1 << (n - 1)) +} + /// `mask` changed by `set` as `how` says, or `None` for a `how` that is none /// of the three, which POSIX refuses `EINVAL`. pub(crate) fn changed(mask: u64, how: i32, set: u64) -> Option { From 6c653d56158ced37fa5ca394628e4057081022f7 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 09:15:09 +0200 Subject: [PATCH 11/12] libc copies: one header reader; dup2's close-on-exec clear guest-tested The BLOCKER of the review at e14f7af23: signal_masks.rs read signal.h beside text_differential.rs, with a different filter, and descriptor_requests.rs read fcntl.h beside errno_codes.rs's reader of errno.h. header.rs is now the one reader of a libc header's integer #defines, decimal or 0x. errno_codes, descriptor_requests, text_differential and signal_masks call it, and signal_masks takes the signal list text_differential reads, header::signals(). The three copies are deleted. The NOTEs: - descriptor_requests opens with O_RDWR, not O_RDONLY, which is 0: under `self.set(fd, flags != 0)` the host now goes red. - fdreq's `cleared` was `set(fd, false)`; it is deleted, and close and dup2 call `set(fd, false)`. - dup2's clear was held by no test. 206 opens an O_CLOEXEC descriptor before anything is closed, so its number is its slot, dup2s onto it and reads F_GETFD: 0. libc's dup2 takes a number as a slot, so the open moves up; the kernel's install_at keeps the slot's generation, so dup2 answers the number it was given. - realloc(p, 0) is recorded, not changed. POSIX.1-2024's realloc answers size 0 with null and errno EINVAL, or with a pointer and p freed. Its application usage frees p only if errno changed, so setting EINVAL beside the free invites a double free. LLVM's safe_realloc (llvm/include/llvm/Support/MemAlloc.h) treats a null answer to size 0 as p freed and calls safe_malloc(1), so null with p kept leaks there. Either branch is a choice for both allocators, std's included, so the gap is recorded in the allocator issue's body and exit. - 207's endian: and names: lines are _Static_asserts now, so the corpus compile refuses a header that drifts. sysconf(_SC_PAGE_SIZE) is a call, so it stays a printed line. The REMOVEs: the false "a number no signal has is refused" goes from signal_masks.rs's doc and test name and from 208's header comment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...ators-set-no-errno-where-posix-has-them.md | 11 +++++- tests/testcases/tinycc/206_libc_refusals.c | 7 +++- .../testcases/tinycc/206_libc_refusals.expect | 1 + tests/testcases/tinycc/207_libc_names.c | 24 +++++++------ tests/testcases/tinycc/207_libc_names.expect | 4 +-- tests/testcases/tinycc/208_signal_mask.c | 3 +- toyos-libc-copies/src/descriptor_requests.rs | 28 +++++---------- toyos-libc-copies/src/errno_codes.rs | 13 ++----- toyos-libc-copies/src/header.rs | 34 +++++++++++++++++++ toyos-libc-copies/src/lib.rs | 2 ++ toyos-libc-copies/src/signal_masks.rs | 25 +++++--------- toyos-libc-copies/src/text_differential.rs | 13 ++----- userland/libc/src/fdreq.rs | 6 ---- userland/libc/src/posix_io.rs | 5 +-- 14 files changed, 92 insertions(+), 84 deletions(-) create mode 100644 toyos-libc-copies/src/header.rs diff --git a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md index 3a20fb2ccc2..0dcdbc92217 100644 --- a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md +++ b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -16,5 +16,14 @@ In a Rust program, C code allocates through std's C allocator and std names no libc symbol: setting it from std puts `__errno_location` in every Rust program's link. +`realloc(p, 0)` frees `p` and answers null with `errno` untouched, in libc +(`userland/libc/src/memory.rs`) and in std's. POSIX.1-2024 has it answer +either null with `errno` `EINVAL`, or a pointer with `p` freed; its +application usage frees `p` only if `errno` changed, so `EINVAL` beside the +free invites a double free. LLVM's `safe_realloc` +(`llvm/include/llvm/Support/MemAlloc.h`) takes a null answer to size 0 as `p` +freed and allocates afresh, so null with `p` kept leaks it there. + **Exit**: each of the four sets `errno` where POSIX has it in a Rust program, -and a C call there reads `ENOMEM` and `EINVAL` back. +and a C call there reads `ENOMEM` and `EINVAL` back; `realloc(p, 0)` gives +one of POSIX's two answers in both allocators. diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c index 09d3195c5d2..8ea2bebd395 100644 --- a/tests/testcases/tinycc/206_libc_refusals.c +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -50,6 +50,9 @@ int main(void) { printf("could not make " FILE_PATH "\n"); return 1; } + /* Opened before anything is closed, so its number is its slot, which is + what dup2 below takes. */ + int cloexec = open(FILE_PATH, O_RDONLY | O_CLOEXEC); char *page = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) { printf("could not map a page\n"); @@ -99,8 +102,10 @@ int main(void) { close(duplicate); set = fcntl(fd, F_SETFD, 0); printf("fcntl F_SETFD 0: %d; F_GETFD: %d\n", set, fcntl(fd, F_GETFD)); - int cloexec = open(FILE_PATH, O_RDONLY | O_CLOEXEC); printf("open O_CLOEXEC; F_GETFD: %d\n", fcntl(cloexec, F_GETFD)); + int onto = dup2(fd, cloexec); + printf("dup2 onto it: %s; F_GETFD: %d\n", onto == cloexec ? "answered it" : "answered another", + fcntl(onto, F_GETFD)); close(cloexec); said("fcntl F_DUPFD -1", fcntl(fd, F_DUPFD, -1)); said("fcntl F_DUPFD 4096", fcntl(fd, F_DUPFD, 4096)); diff --git a/tests/testcases/tinycc/206_libc_refusals.expect b/tests/testcases/tinycc/206_libc_refusals.expect index 9b2923e5553..5de407a79ad 100644 --- a/tests/testcases/tinycc/206_libc_refusals.expect +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -24,6 +24,7 @@ fcntl F_SETFD FD_CLOEXEC: 0; F_GETFD: 1 fcntl F_DUPFD 10: 10 or above; fstat of it: the file's one byte; F_GETFD of it: 0 fcntl F_SETFD 0: 0; F_GETFD: 0 open O_CLOEXEC; F_GETFD: 1 +dup2 onto it: answered it; F_GETFD: 0 fcntl F_DUPFD -1: -1, EINVAL fcntl F_DUPFD 4096: -1, EINVAL fcntl F_DUPFD_CLOEXEC: -1, ENOSYS diff --git a/tests/testcases/tinycc/207_libc_names.c b/tests/testcases/tinycc/207_libc_names.c index 00302a630b0..201f70b2716 100644 --- a/tests/testcases/tinycc/207_libc_names.c +++ b/tests/testcases/tinycc/207_libc_names.c @@ -29,6 +29,19 @@ /* The image's test library, which exports tls_get_label. */ #define LIB "/system/lib/libtls_lib.so" +/* The rest of what LLVM reads of these headers, by value and by size. */ +_Static_assert(BYTE_ORDER == LITTLE_ENDIAN, "BYTE_ORDER"); +_Static_assert(htobe32(0x01020304) == 0x04030201, "htobe32"); +_Static_assert(le16toh(0x0102) == 0x0102, "le16toh"); +_Static_assert(AF_UNIX == 1, "AF_UNIX"); +_Static_assert(sizeof(struct sockaddr_un) == 110, "sockaddr_un"); +_Static_assert(EX_IOERR == 74, "EX_IOERR"); +_Static_assert(_POSIX_ARG_MAX == 4096, "_POSIX_ARG_MAX"); +_Static_assert((SA_ONSTACK | SA_NODEFER | SA_RESETHAND) == 0xc8000000, "SA_ONSTACK|SA_NODEFER|SA_RESETHAND"); +_Static_assert(F_RDLCK == 0 && F_UNLCK == 2, "F_RDLCK, F_UNLCK"); +_Static_assert(sizeof(struct rusage) == 144, "rusage"); +_Static_assert(RLIM_INFINITY == ~0UL, "RLIM_INFINITY"); + int main(void); static const char *errno_name(int e) { @@ -153,15 +166,6 @@ int main(void) { errno = 0; double pole = logb(0.0); printf("logb(0): %g, %s\n", pole, errno == ERANGE ? "ERANGE" : errno_name(errno)); - printf("endian: %s 0x%x 0x%x\n", BYTE_ORDER == LITTLE_ENDIAN ? "little" : "big", - (unsigned)htobe32(0x01020304), (unsigned)le16toh(0x0102)); - - /* The rest of what LLVM reads of these headers, by value and by size. */ - struct sockaddr_un local = { AF_UNIX, "" }; - printf("names: AF_UNIX %d, sockaddr_un %zu, EX_IOERR %d, _POSIX_ARG_MAX %d, page %ld\n", local.sun_family, - sizeof local, EX_IOERR, _POSIX_ARG_MAX, sysconf(_SC_PAGE_SIZE)); - printf("names: SA_ONSTACK|SA_NODEFER|SA_RESETHAND 0x%lx, F_RDLCK %d, F_UNLCK %d, rusage %zu, RLIM_INFINITY %s\n", - (unsigned long)(SA_ONSTACK | SA_NODEFER | SA_RESETHAND), F_RDLCK, F_UNLCK, sizeof(struct rusage), - RLIM_INFINITY == ~0UL ? "every bit" : "not every bit"); + printf("sysconf _SC_PAGE_SIZE: %ld\n", sysconf(_SC_PAGE_SIZE)); return 0; } diff --git a/tests/testcases/tinycc/207_libc_names.expect b/tests/testcases/tinycc/207_libc_names.expect index 3f482d5dbde..a5091a68a7f 100644 --- a/tests/testcases/tinycc/207_libc_names.expect +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -16,6 +16,4 @@ strsignal: Segmentation fault; Unknown signal modf(-3.25): -3 and -0.25 logb: -4 10 logb(0): -inf, ERANGE -endian: little 0x4030201 0x102 -names: AF_UNIX 1, sockaddr_un 110, EX_IOERR 74, _POSIX_ARG_MAX 4096, page 4096 -names: SA_ONSTACK|SA_NODEFER|SA_RESETHAND 0xc8000000, F_RDLCK 0, F_UNLCK 2, rusage 144, RLIM_INFINITY every bit +sysconf _SC_PAGE_SIZE: 4096 diff --git a/tests/testcases/tinycc/208_signal_mask.c b/tests/testcases/tinycc/208_signal_mask.c index 5cba12f24fa..be00e6f1061 100644 --- a/tests/testcases/tinycc/208_signal_mask.c +++ b/tests/testcases/tinycc/208_signal_mask.c @@ -2,8 +2,7 @@ change read back by the next call, SIGKILL and SIGSTOP never in it, a how that is none of POSIX's three refused, and a new thread starting with its creator's mask; and a set as sigemptyset, sigaddset and sigfillset make - one, a number no signal has refused. Signal n is bit n - 1 of a - sigset_t. */ + one. Signal n is bit n - 1 of a sigset_t. */ #include #include #include diff --git a/toyos-libc-copies/src/descriptor_requests.rs b/toyos-libc-copies/src/descriptor_requests.rs index fd2840ccfcd..4d7ae5e11b9 100644 --- a/toyos-libc-copies/src/descriptor_requests.rs +++ b/toyos-libc-copies/src/descriptor_requests.rs @@ -3,6 +3,7 @@ //! (`kernel/src/object/handle.rs`), and each descriptor's close-on-exec mark. use crate::fdreq::{self, Command}; +use crate::header::{self, FCNTL_H}; /// What clang leaves in the register of an `int` argument of -1: `movl /// $0xffffffff, %edx`, which zeroes the upper half (measured on @@ -116,29 +117,16 @@ fn f_dupfd_meets_its_floor_holding_one_duplicate_at_most() { } } -/// `fcntl.h`'s value for `name`, which a C program passes. -fn fcntl_h(name: &str) -> i32 { - let line = include_str!("../../userland/libc/include/fcntl.h") - .lines() - .find_map(|line| line.strip_prefix("#define ")?.strip_prefix(name)?.strip_prefix(char::is_whitespace)) - .unwrap_or_else(|| panic!("fcntl.h defines no {name}")); - let value = line.trim(); - match value.strip_prefix("0x") { - Some(hex) => i32::from_str_radix(hex, 16), - None => value.parse(), - } - .unwrap_or_else(|_| panic!("fcntl.h: {name} {value}")) -} - /// A descriptor's close-on-exec mark through the calls that touch it: `open` /// with and without `O_CLOEXEC`, `F_SETFD` either way, `F_GETFD`, `close`, and /// `dup2`'s answer, which POSIX has not closed on `exec`. #[test] fn each_descriptor_keeps_its_own_close_on_exec_mark() { - let (cloexec, read_only, fd_cloexec) = (fcntl_h("O_CLOEXEC"), fcntl_h("O_RDONLY"), fcntl_h("FD_CLOEXEC")); + let (cloexec, read_write) = (header::int(FCNTL_H, "O_CLOEXEC"), header::int(FCNTL_H, "O_RDWR")); + let fd_cloexec = header::int(FCNTL_H, "FD_CLOEXEC"); let mut marks = fdreq::CloseOnExec::new(); - marks.opened(3, read_only | cloexec); - marks.opened(4, read_only); + marks.opened(3, read_write | cloexec); + marks.opened(4, read_write); assert_eq!((marks.flags(3), marks.flags(4)), (fd_cloexec, 0), "open"); marks.set(3, false); @@ -147,14 +135,14 @@ fn each_descriptor_keeps_its_own_close_on_exec_mark() { // `close`, and a later handle in slot 4 (one generation on), which is a // number of its own. - marks.cleared(4); + marks.set(4, false); assert_eq!(marks.flags(4), 0, "close"); marks.set(4, true); assert_eq!(marks.flags(4 | 1 << 12), 0, "the next generation of slot 4"); // `dup2`'s answer, and `open` without `O_CLOEXEC` of a number marked. - marks.cleared(4); + marks.set(4, false); marks.set(5, true); - marks.opened(5, read_only); + marks.opened(5, read_write); assert_eq!((marks.flags(4), marks.flags(5)), (0, 0), "dup2, and open"); } diff --git a/toyos-libc-copies/src/errno_codes.rs b/toyos-libc-copies/src/errno_codes.rs index a004fd7f013..7ab4510791e 100644 --- a/toyos-libc-copies/src/errno_codes.rs +++ b/toyos-libc-copies/src/errno_codes.rs @@ -2,19 +2,10 @@ //! numbers a C program compares them with: every code the library answers //! with is the header's, by name and value. -use std::collections::HashMap; +use crate::header::{self, ERRNO_H}; #[test] fn every_code_libc_answers_with_is_errno_h_s() { - let header = include_str!("../../userland/libc/include/errno.h"); - let defined: HashMap<&str, i32> = header - .lines() - .filter_map(|line| line.strip_prefix("#define ")) - .filter_map(|rest| { - let mut words = rest.split_whitespace(); - Some((words.next()?, words.next()?.parse().ok()?)) - }) - .collect(); let list = include_str!("../../userland/libc/src/errno.rs"); let codes: Vec<(&str, i32)> = list .lines() @@ -26,6 +17,6 @@ fn every_code_libc_answers_with_is_errno_h_s() { .collect(); assert!(codes.len() > 20, "errno.rs lists {} codes", codes.len()); for (name, value) in codes { - assert_eq!(defined.get(name), Some(&value), "{name}"); + assert_eq!(header::int(ERRNO_H, name), value, "{name}"); } } diff --git a/toyos-libc-copies/src/header.rs b/toyos-libc-copies/src/header.rs new file mode 100644 index 00000000000..ad4b5224c44 --- /dev/null +++ b/toyos-libc-copies/src/header.rs @@ -0,0 +1,34 @@ +//! A libc header's integer `#define`s as a C program reads them: a decimal or +//! `0x` value after the name. A `#define` of anything else is not one. + +pub(crate) const ERRNO_H: &str = include_str!("../../userland/libc/include/errno.h"); +pub(crate) const FCNTL_H: &str = include_str!("../../userland/libc/include/fcntl.h"); +const SIGNAL_H: &str = include_str!("../../userland/libc/include/signal.h"); + +/// Each integer `#define` in `header`, in its order. +fn defines(header: &str) -> impl Iterator { + header.lines().filter_map(|line| { + let mut words = line.strip_prefix("#define ")?.split_whitespace(); + let (name, value) = (words.next()?, words.next()?); + let value = match value.strip_prefix("0x") { + Some(hex) => i64::from_str_radix(hex, 16), + None => value.parse(), + }; + Some((name, value.ok()?)) + }) +} + +fn as_int((name, value): (&str, i64)) -> (&str, i32) { + (name, i32::try_from(value).unwrap_or_else(|_| panic!("{name} {value:#x} is no int"))) +} + +/// `name`'s value in `header`, as a C `int`. +pub(crate) fn int(header: &str, name: &str) -> i32 { + let found = defines(header).find(|&(n, _)| n == name); + as_int(found.unwrap_or_else(|| panic!("no integer #define {name}"))).1 +} + +/// Each signal `signal.h` numbers: `SIGHUP`, and not `SIG_BLOCK`. +pub(crate) fn signals() -> Vec<(&'static str, i32)> { + defines(SIGNAL_H).filter(|(name, _)| name.starts_with("SIG") && !name.contains('_')).map(as_int).collect() +} diff --git a/toyos-libc-copies/src/lib.rs b/toyos-libc-copies/src/lib.rs index 95265fd8f18..83ceec4f29b 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -54,6 +54,8 @@ mod errno_codes; #[cfg(test)] mod fparts_differential; #[cfg(test)] +mod header; +#[cfg(test)] mod link_requests; #[cfg(test)] mod listing_reader; diff --git a/toyos-libc-copies/src/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs index cf5dacdee75..2d24f734873 100644 --- a/toyos-libc-copies/src/signal_masks.rs +++ b/toyos-libc-copies/src/signal_masks.rs @@ -4,10 +4,11 @@ //! it; any other `how` refused. A set as `sigaddset` and `sigfillset` make one, //! against POSIX's words for each. +use crate::header; use crate::sigmask::{self, SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; /// Signal `n`'s bit. -fn bit(n: u32) -> u64 { +fn bit(n: i32) -> u64 { 1 << (n - 1) } @@ -27,13 +28,11 @@ fn each_how_changes_the_mask_as_posix_says() { } } -/// POSIX's `sigaddset`: a number no signal has is refused, `EINVAL` (`None`), -/// and any other is added alone, however often. #[test] -fn sigaddset_adds_its_signal_alone_and_refuses_a_number_no_signal_has() { +fn sigaddset_adds_its_signal_alone() { for n in 1..=64 { - assert_eq!(sigmask::with(0, n), Some(bit(n as u32)), "signal {n}"); - assert_eq!(sigmask::with(bit(n as u32), n), Some(bit(n as u32)), "signal {n} twice"); + assert_eq!(sigmask::with(0, n), Some(bit(n)), "signal {n}"); + assert_eq!(sigmask::with(bit(n), n), Some(bit(n)), "signal {n} twice"); } assert_eq!(sigmask::with(bit(1), 10), Some(bit(1) | bit(10))); for n in [0, -1, 65, i32::MIN, i32::MAX] { @@ -45,18 +44,10 @@ fn sigaddset_adds_its_signal_alone_and_refuses_a_number_no_signal_has() { /// and the full set is every signal `sigaddset` takes. #[test] fn the_full_set_holds_every_signal() { - let defined: Vec = include_str!("../../userland/libc/include/signal.h") - .lines() - .filter_map(|line| line.strip_prefix("#define SIG")) - .filter(|rest| !rest.starts_with('_')) - .map(|rest| { - let value = rest.split_whitespace().nth(1); - value.and_then(|n| n.parse().ok()).unwrap_or_else(|| panic!("signal.h: SIG{rest}")) - }) - .collect(); + let defined = header::signals(); assert!(defined.len() > 15, "signal.h defines {} signals", defined.len()); - for n in defined { - assert_ne!(sigmask::FULL & bit(n), 0, "signal {n}"); + for (name, n) in defined { + assert_ne!(sigmask::FULL & bit(n), 0, "{name}"); } assert_eq!((1..=64).fold(0, |set, n| sigmask::with(set, n).unwrap()), sigmask::FULL); } diff --git a/toyos-libc-copies/src/text_differential.rs b/toyos-libc-copies/src/text_differential.rs index 8f9c0e1ad06..12e86fa48ae 100644 --- a/toyos-libc-copies/src/text_differential.rs +++ b/toyos-libc-copies/src/text_differential.rs @@ -4,6 +4,7 @@ use std::collections::BTreeSet; +use crate::header; use crate::text; extern "C" { @@ -57,17 +58,7 @@ fn text_of(sig: i32) -> String { #[test] fn every_signal_signal_h_numbers_has_glibcs_text() { - let header = include_str!("../../userland/libc/include/signal.h"); - // `#define SIGHUP 1`, and not `SIG_DFL` or `SIG_BLOCK`. - let numbered: Vec<(&str, i32)> = header - .lines() - .filter_map(|line| line.strip_prefix("#define ")) - .filter_map(|rest| { - let mut words = rest.split_whitespace(); - let name = words.next().filter(|n| n.starts_with("SIG") && !n.contains('_'))?; - Some((name, words.next()?.parse().ok()?)) - }) - .collect(); + let numbered = header::signals(); assert_eq!(numbered.len(), GLIBC.len(), "signal.h numbers {numbered:?}"); let mut texts = BTreeSet::new(); for (name, sig) in numbered { diff --git a/userland/libc/src/fdreq.rs b/userland/libc/src/fdreq.rs index 50da5303729..45a76ccdb00 100644 --- a/userland/libc/src/fdreq.rs +++ b/userland/libc/src/fdreq.rs @@ -93,7 +93,6 @@ impl CloseOnExec { self.set(fd, flags & O_CLOEXEC != 0); } - /// `F_SETFD`'s. pub(crate) fn set(&mut self, fd: i32, cloexec: bool) { if cloexec { self.0.insert(fd); @@ -106,9 +105,4 @@ impl CloseOnExec { pub(crate) fn flags(&self, fd: i32) -> i32 { if self.0.contains(&fd) { FD_CLOEXEC } else { 0 } } - - /// `fd` closed, or answered by `dup2`, which POSIX has not closed on `exec`. - pub(crate) fn cleared(&mut self, fd: i32) { - self.0.remove(&fd); - } } diff --git a/userland/libc/src/posix_io.rs b/userland/libc/src/posix_io.rs index 8ed0d75b82a..5b9da34b9ec 100644 --- a/userland/libc/src/posix_io.rs +++ b/userland/libc/src/posix_io.rs @@ -104,7 +104,7 @@ pub unsafe extern "C" fn creat(path: *const u8, mode: u32) -> i32 { #[no_mangle] pub unsafe extern "C" fn close(raw_fd: i32) -> i32 { - CLOEXEC.lock().cleared(raw_fd); + CLOEXEC.lock().set(raw_fd, false); syscall::close(fd(raw_fd)); 0 } @@ -207,7 +207,8 @@ pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { }; match syscall::dup2(fd(old_fd), slot) { Ok(f) => { - CLOEXEC.lock().cleared(f.0 as i32); + // POSIX: the descriptor `dup2` answers is not closed on `exec`. + CLOEXEC.lock().set(f.0 as i32, false); // The slot holds something else now, so the stream is asked again. match slot { 1 => toyos::log::stdio::forget(toyos::log::stdio::Stream::Out), From 15625e0cb0d93490200ce971c66de9494d3edb28 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 09:51:42 +0200 Subject: [PATCH 12/12] libc: a C sysroot links a probe naming each library; fcntl and signal values read off their headers The fifth review's NOTEs, at round 4's 6c653d561. The libm issue this branch closes asked for a host link with -lc -lm -lpthread -ldl -lrt against the C sysroot with the toolchain's clang. That link happened only in the corpus build, which #660 moves to the metal loop, and the_c_sysroot_names_the_empty_libraries stayed green with build_c's call of empty_libraries deleted. `--ci host` carries no ToyOS toolchain (src/ci.rs's `host`), so the probe runs where the C sysroot is made: build_c links `int main(void) { return 0; }` with every one of EMPTY_LIBRARIES and refuses the sysroot if clang cannot. That runs in every build that makes a sysroot, the nightly's `build` job among them. RECIPE moves to 11, so no sysroot of this recipe went unprobed. descriptor_requests: every fcntl command and FD_CLOEXEC are read off fcntl.h through header::int, so fdreq.rs's numbers are held to the header on the host, not only by 206. The "close" and "dup2" steps were set(4, false), the F_SETFD step again, and held neither call: deleted, with the set(4, true) that only undid the first, and the doc's words for them. signal_masks: each `how`, and the five signals the test names, are read off signal.h through header::int, so sigmask.rs's SIG_* and its SIGKILL and SIGSTOP are held to the header on the host. Issues: the allocator issue's exit names POSIX's second answer for realloc(p, 0), a pointer with p freed, since the first leaks under LLVM's safe_realloc. The /tmp symlink issue's exit is the route the orchestrator ruled: symlink is the file server's; libc sends it to the directory's server, fsd makes the link and refuses an existing name, and the kernel's SYS_SYMLINK and symlink code go wherever nothing calls them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...ators-set-no-errno-where-posix-has-them.md | 2 +- ...mp-displaces-its-name-and-lists-nowhere.md | 16 +++-- src/libc.rs | 33 +++++++++- src/sysroot.rs | 5 +- toyos-libc-copies/src/descriptor_requests.rs | 62 +++++++++---------- toyos-libc-copies/src/header.rs | 2 +- toyos-libc-copies/src/signal_masks.rs | 21 ++++--- 7 files changed, 85 insertions(+), 56 deletions(-) diff --git a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md index 0dcdbc92217..91bdbd61603 100644 --- a/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md +++ b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -26,4 +26,4 @@ freed and allocates afresh, so null with `p` kept leaks it there. **Exit**: each of the four sets `errno` where POSIX has it in a Rust program, and a C call there reads `ENOMEM` and `EINVAL` back; `realloc(p, 0)` gives -one of POSIX's two answers in both allocators. +POSIX's second answer in both allocators: a pointer, with `p` freed. diff --git a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md index 770b54b37ae..a1d363de19e 100644 --- a/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md +++ b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -12,12 +12,16 @@ a name that exists `EEXIST`, so libc's `symlink` refuses `ENOSYS` (`userland/libc/src/refused.rs`). Its `list` answers files alone, so no `readdir` of a directory shows a link it holds. -**Exit**: `SYS_SYMLINK` over a name that exists is refused `AlreadyExists` -under the lock that makes the link, and a listing of `/tmp` names each link it -holds, each asserted by a guest case. libc's `symlink` calls it with nothing -asked first and answers that refusal `EEXIST`, which is how LLVM's -`LockFileManager` takes its lock (`create_link`, `::symlink` in -`llvm/lib/Support/Unix/Path.inc`). An LLVM build on ToyOS reaches it too: +**Exit**: `symlink` is the file server's, not the kernel's. libc's `symlink` +sends the request to the server of the directory that is to hold the link, as +std's does (`on_path` in `rust/library/std/src/sys/fs/toyos.rs`), with nothing +asked first, and answers its `AlreadyExists` `EEXIST`. fsd makes the link in +the one request that refuses a name that exists, and libc's `readdir` of that +directory names the link, each asserted by a test. The kernel's `SYS_SYMLINK` +and its symlink code (`kernel/src/vfs.rs`, `kernel/src/tmpfs.rs`) go wherever +nothing calls them. That `EEXIST` is how LLVM's `LockFileManager` takes its +lock (`create_link`, `::symlink` in `llvm/lib/Support/Unix/Path.inc`). An LLVM +build on ToyOS reaches `symlink` too: `LLVM_USE_SYMLINKS` is on for a UNIX host, so `add_llvm_tool_symlink` (`llvm/cmake/modules/AddLLVM.cmake`) makes each tool's aliases with CMake's `create_symlink`. diff --git a/src/libc.rs b/src/libc.rs index 53ee54e1f74..f0770d7ae37 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -110,18 +110,45 @@ pub fn build_c(root: &Path, toolchain: &Path, target_dir: &Path, arch: Arch) { "toyos-libc's staticlib for {target} did not build:\n{}", String::from_utf8_lossy(&output.stderr), ); - let c = crate::clang::CSysroot::of(toolchain, arch).dir; - let lib = c.join("lib"); + let c = crate::clang::CSysroot::of(toolchain, arch); + let lib = c.dir.join("lib"); fs::create_dir_all(&lib).unwrap_or_else(|e| panic!("create {}: {e}", lib.display())); let archive = target_dir.join(format!("{target}/release/libtoyos_libc.a")); fs::copy(&archive, lib.join("libtoyos_c.a")) .unwrap_or_else(|e| panic!("copy {} into {}: {e}", archive.display(), lib.display())); empty_libraries(&lib); - crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.join("include")); + crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.dir.join("include")); + links_naming_every_library(&c, target_dir); } const EMPTY_LIBRARIES: [&str; 5] = ["c", "m", "pthread", "dl", "rt"]; +/// Refuse the C sysroot `c` unless its clang links a C program against it +/// naming each of [`EMPTY_LIBRARIES`], as LLVM's configure links every probe +/// with `-lm` (`llvm/cmake/config-ix.cmake`). The program and its binary are +/// written in `scratch`. +fn links_naming_every_library(c: &crate::clang::CSysroot, scratch: &Path) { + let source = scratch.join(format!("probe-{}.c", c.target)); + fs::write(&source, "int main(void) { return 0; }\n") + .unwrap_or_else(|e| panic!("write {}: {e}", source.display())); + let names = EMPTY_LIBRARIES.map(|name| format!("-l{name}")); + let output = Command::new(&c.clang) + .args(c.args()) + .arg(&source) + .args(&names) + .arg("-o") + .arg(source.with_extension("")) + .output() + .unwrap_or_else(|e| panic!("run {}: {e}", c.clang.display())); + assert!( + output.status.success(), + "the C sysroot at {} links no program naming {}:\n{}", + c.dir.display(), + names.join(" "), + String::from_utf8_lossy(&output.stderr), + ); +} + /// Put each of [`EMPTY_LIBRARIES`] in `lib` as an archive of no members: a /// link that names one finds it and takes nothing from it. fn empty_libraries(lib: &Path) { diff --git a/src/sysroot.rs b/src/sysroot.rs index 316334a6087..d8810ac7740 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -67,8 +67,9 @@ const SOURCES: &str = "SOURCES"; const RECIPE: &str = "bootstrap stage-0 local rebuild, profile compiler, no LLVM or Ninja, \ libtoyos_c merged, libraries from the stamp, linked by rust-lld, \ a C sysroot of libc's staticlib, the empty libraries beside it, and headers \ - per target, and its C++ runtime built under n2 from the runtimes' sources \ - of the compiler's LLVM; 10"; + per target, refused unless a C program naming each library links against \ + it, and its C++ runtime built under n2 from the runtimes' sources of the \ + compiler's LLVM; 11"; /// Every sysroot on this host. pub fn sysroots_dir(rust_dir: &Path) -> PathBuf { diff --git a/toyos-libc-copies/src/descriptor_requests.rs b/toyos-libc-copies/src/descriptor_requests.rs index 4d7ae5e11b9..68901ad25d4 100644 --- a/toyos-libc-copies/src/descriptor_requests.rs +++ b/toyos-libc-copies/src/descriptor_requests.rs @@ -12,31 +12,33 @@ const MINUS_ONE_AS_CLANG_PASSES_IT: u64 = 0xffff_ffff; #[test] fn fcntl_reads_an_int_argument_from_the_lower_half_of_its_register() { + let set_cloexec = header::int(FCNTL_H, "FD_CLOEXEC") as u64; for (cmd, arg, want) in [ - (0, MINUS_ONE_AS_CLANG_PASSES_IT, Command::Invalid), - (0, u64::MAX, Command::Invalid), - (0, 0xdead_beef_0000_000a, Command::DupAtLeast(10)), - (0, 0, Command::DupAtLeast(0)), - (0, 4095, Command::DupAtLeast(4095)), + ("F_DUPFD", MINUS_ONE_AS_CLANG_PASSES_IT, Command::Invalid), + ("F_DUPFD", u64::MAX, Command::Invalid), + ("F_DUPFD", 0xdead_beef_0000_000a, Command::DupAtLeast(10)), + ("F_DUPFD", 0, Command::DupAtLeast(0)), + ("F_DUPFD", 4095, Command::DupAtLeast(4095)), // `{OPEN_MAX}`, the slots a table has. - (0, 4096, Command::Invalid), - (0, 0x7fff_ffff, Command::Invalid), - (1, u64::MAX, Command::GetFd), - (2, 1, Command::SetFd(true)), - (2, 0xffff_ffff_0000_0000, Command::SetFd(false)), - (2, 0, Command::SetFd(false)), - (3, 0, Command::Unsupported), - (4, 0, Command::Unsupported), - (5, 0, Command::Invalid), - (6, 0, Command::Invalid), - (7, 0, Command::Invalid), - (8, 0, Command::Unsupported), - (9, 0, Command::Unsupported), - (1030, 10, Command::Unsupported), - (12345, 0, Command::Invalid), - (-1, 0, Command::Invalid), + ("F_DUPFD", 4096, Command::Invalid), + ("F_DUPFD", 0x7fff_ffff, Command::Invalid), + ("F_GETFD", u64::MAX, Command::GetFd), + ("F_SETFD", set_cloexec, Command::SetFd(true)), + ("F_SETFD", 0xffff_ffff_0000_0000, Command::SetFd(false)), + ("F_SETFD", 0, Command::SetFd(false)), + ("F_GETFL", 0, Command::Unsupported), + ("F_SETFL", 0, Command::Unsupported), + ("F_GETLK", 0, Command::Invalid), + ("F_SETLK", 0, Command::Invalid), + ("F_SETLKW", 0, Command::Invalid), + ("F_SETOWN", 0, Command::Unsupported), + ("F_GETOWN", 0, Command::Unsupported), + ("F_DUPFD_CLOEXEC", 10, Command::Unsupported), ] { - assert_eq!(fdreq::command(cmd, arg), want, "fcntl(_, {cmd}, {arg:#x})"); + assert_eq!(fdreq::command(header::int(FCNTL_H, cmd), arg), want, "fcntl(_, {cmd}, {arg:#x})"); + } + for cmd in [12345, -1] { + assert_eq!(fdreq::command(cmd, 0), Command::Invalid, "fcntl(_, {cmd}, 0)"); } } @@ -117,9 +119,8 @@ fn f_dupfd_meets_its_floor_holding_one_duplicate_at_most() { } } -/// A descriptor's close-on-exec mark through the calls that touch it: `open` -/// with and without `O_CLOEXEC`, `F_SETFD` either way, `F_GETFD`, `close`, and -/// `dup2`'s answer, which POSIX has not closed on `exec`. +/// A descriptor's close-on-exec mark through `open` with and without +/// `O_CLOEXEC`, `F_SETFD` either way, and `F_GETFD`. #[test] fn each_descriptor_keeps_its_own_close_on_exec_mark() { let (cloexec, read_write) = (header::int(FCNTL_H, "O_CLOEXEC"), header::int(FCNTL_H, "O_RDWR")); @@ -133,16 +134,11 @@ fn each_descriptor_keeps_its_own_close_on_exec_mark() { marks.set(4, true); assert_eq!((marks.flags(3), marks.flags(4)), (0, fd_cloexec), "F_SETFD"); - // `close`, and a later handle in slot 4 (one generation on), which is a - // number of its own. - marks.set(4, false); - assert_eq!(marks.flags(4), 0, "close"); - marks.set(4, true); + // A later handle in slot 4 (one generation on), which is a number of its own. assert_eq!(marks.flags(4 | 1 << 12), 0, "the next generation of slot 4"); - // `dup2`'s answer, and `open` without `O_CLOEXEC` of a number marked. - marks.set(4, false); + // `open` without `O_CLOEXEC` of a number marked. marks.set(5, true); marks.opened(5, read_write); - assert_eq!((marks.flags(4), marks.flags(5)), (0, 0), "dup2, and open"); + assert_eq!(marks.flags(5), 0, "open over a mark"); } diff --git a/toyos-libc-copies/src/header.rs b/toyos-libc-copies/src/header.rs index ad4b5224c44..0b92751b7ac 100644 --- a/toyos-libc-copies/src/header.rs +++ b/toyos-libc-copies/src/header.rs @@ -3,7 +3,7 @@ pub(crate) const ERRNO_H: &str = include_str!("../../userland/libc/include/errno.h"); pub(crate) const FCNTL_H: &str = include_str!("../../userland/libc/include/fcntl.h"); -const SIGNAL_H: &str = include_str!("../../userland/libc/include/signal.h"); +pub(crate) const SIGNAL_H: &str = include_str!("../../userland/libc/include/signal.h"); /// Each integer `#define` in `header`, in its order. fn defines(header: &str) -> impl Iterator { diff --git a/toyos-libc-copies/src/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs index 2d24f734873..a3c6810a59d 100644 --- a/toyos-libc-copies/src/signal_masks.rs +++ b/toyos-libc-copies/src/signal_masks.rs @@ -4,8 +4,8 @@ //! it; any other `how` refused. A set as `sigaddset` and `sigfillset` make one, //! against POSIX's words for each. -use crate::header; -use crate::sigmask::{self, SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; +use crate::header::{self, SIGNAL_H}; +use crate::sigmask; /// Signal `n`'s bit. fn bit(n: i32) -> u64 { @@ -14,15 +14,16 @@ fn bit(n: i32) -> u64 { #[test] fn each_how_changes_the_mask_as_posix_says() { - let (hup, int, usr1) = (bit(1), bit(2), bit(10)); - let (kill, stop) = (bit(9), bit(19)); - assert_eq!(sigmask::changed(hup, SIG_BLOCK, usr1), Some(hup | usr1)); - assert_eq!(sigmask::changed(hup | usr1, SIG_UNBLOCK, usr1 | int), Some(hup)); - assert_eq!(sigmask::changed(hup, SIG_SETMASK, int), Some(int)); - assert_eq!(sigmask::changed(hup, SIG_SETMASK, 0), Some(0)); + let named = |name| header::int(SIGNAL_H, name); + let (block, unblock, set_mask) = (named("SIG_BLOCK"), named("SIG_UNBLOCK"), named("SIG_SETMASK")); + let [hup, int, usr1, kill, stop] = ["SIGHUP", "SIGINT", "SIGUSR1", "SIGKILL", "SIGSTOP"].map(|s| bit(named(s))); + assert_eq!(sigmask::changed(hup, block, usr1), Some(hup | usr1)); + assert_eq!(sigmask::changed(hup | usr1, unblock, usr1 | int), Some(hup)); + assert_eq!(sigmask::changed(hup, set_mask, int), Some(int)); + assert_eq!(sigmask::changed(hup, set_mask, 0), Some(0)); // Asked for, and dropped without an error. - assert_eq!(sigmask::changed(0, SIG_BLOCK, kill | stop | usr1), Some(usr1)); - assert_eq!(sigmask::changed(0, SIG_SETMASK, u64::MAX), Some(!(kill | stop))); + assert_eq!(sigmask::changed(0, block, kill | stop | usr1), Some(usr1)); + assert_eq!(sigmask::changed(0, set_mask, u64::MAX), Some(!(kill | stop))); for how in [-1, 3, 99] { assert_eq!(sigmask::changed(hup, how, usr1), None, "how {how}"); }