diff --git a/Cargo.lock b/Cargo.lock index cc05a45c729..798e1d77d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1061,6 +1061,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/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..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 @@ -28,8 +28,10 @@ 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 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-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-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-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..1a5a26eda49 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` 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-has-no-pread-or-pwrite.md b/issues/build/libc-has-no-pread-or-pwrite.md new file mode 100644 index 00000000000..4e6a2810cd2 --- /dev/null +++ b/issues/build/libc-has-no-pread-or-pwrite.md @@ -0,0 +1,22 @@ +--- +status: open +kind: defect +opened: 2026-09-30 +--- + +# libc has no pread or pwrite + +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 +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-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-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 5f1e906ec4b..00000000000 --- a/issues/build/libc-pread-and-pwrite-move-the-offset-another-thread-shares.md +++ /dev/null @@ -1,26 +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` -(`issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md`), -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-printf-ignores-the-alternate-form-flag.md b/issues/build/libc-printf-ignores-the-alternate-form-flag.md new file mode 100644 index 00000000000..41bef0cc694 --- /dev/null +++ b/issues/build/libc-printf-ignores-the-alternate-form-flag.md @@ -0,0 +1,14 @@ +--- +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. + +**Exit**: `#` takes C's alternate form for every conversion that has one, and +a corpus case prints each. 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 006bd500da8..00000000000 --- a/issues/build/libc-readdir-calls-every-entry-a-regular-file.md +++ /dev/null @@ -1,19 +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 -(`issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md`); -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/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..aec0c62ef78 --- /dev/null +++ b/issues/build/libc-refuses-what-toyos-cannot-yet-answer.md @@ -0,0 +1,48 @@ +--- +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`), each asserted by +`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. +- `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. +- `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_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`). `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; +`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/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..91bdbd61603 --- /dev/null +++ b/issues/build/the-c-allocators-set-no-errno-where-posix-has-them.md @@ -0,0 +1,29 @@ +--- +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. + +`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; `realloc(p, 0)` gives +POSIX's second answer in both allocators: a pointer, with `p` freed. 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 ee6bd39730e..00000000000 --- a/issues/build/the-c-sysroot-has-no-libm-so-llvms-configure-fails.md +++ /dev/null @@ -1,34 +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 -(`issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md`), -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 4947b541ca8..ee911f995f3 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 - `issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md`, - 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 `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 6a6c5a92968..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 -`issues/build/libc-headers-are-written-by-hand-and-drift-from-its-definitions.md`'s. -`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/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..a1d363de19e --- /dev/null +++ b/issues/filesystem/a-symbolic-link-on-tmp-displaces-its-name-and-lists-nowhere.md @@ -0,0 +1,27 @@ +--- +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`, 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**: `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/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index 6cc56bf9fa8..7799a8ffc72 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -39,7 +39,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 @@ -294,32 +294,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 aca5f527fcb..3f6050fc829 160000 --- a/rust +++ b/rust @@ -1 +1 @@ -Subproject commit aca5f527fcb5dc3b694e33a0f0a7312d8d29aa60 +Subproject commit 3f6050fc8293153ce890879442735fdaeded9c63 diff --git a/src/libc.rs b/src/libc.rs index 758721fd2d6..f0770d7ae37 100644 --- a/src/libc.rs +++ b/src/libc.rs @@ -110,13 +110,52 @@ 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())); - crate::sysroot::clone_tree(&root.join(CRATE).join("include"), &c.join("include")); + empty_libraries(&lib); + 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) { + 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())); + } } /// Extract .o files from rlibs and merge them into a single GNU-format ar archive. @@ -275,3 +314,19 @@ fn extract_rlib_objects(data: &[u8], out: &mut Vec<(String, Vec)>) { } } } + +#[cfg(test)] +mod tests { + use super::*; + use toyos_tmpdir::TempDir; + + #[test] + 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"] { + 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 61ff79e1ec7..557a4c73cb4 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 @@ -58,13 +58,13 @@ use crate::toolchain::{self, host_triple, GuestTarget, Owner, Role, GUEST_TARGET use whole_toolchain::{whole, Whole}; /// 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"]; /// Of [`SYSROOT_MANIFESTS`], the ones std's lockfile resolves with the fork's /// own: what of a worktree can move a freestanding target's dependency versions. @@ -78,9 +78,10 @@ const SOURCES: &str = "SOURCES"; /// is none of them, nor std's configuration ([`std_config`]), which the /// freestanding key reads whole. Moving it moves every key. const RECIPE: &str = "bootstrap stage-0 local rebuild, libraries from the stamp, libtoyos_c merged, \ - a C sysroot of libc's staticlib and headers per target, and its C++ runtime \ - built under n2 from the runtimes' sources of the compiler's LLVM, the \ - freestanding libraries cloned from their key's; 10"; + a C sysroot of libc's staticlib, the empty libraries beside it, and headers \ + 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, the freestanding libraries cloned from their key's; 12"; /// Every sysroot on this host. pub fn sysroots_dir(rust_dir: &Path) -> PathBuf { @@ -965,7 +966,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/common/compile.rs b/tests/common/compile.rs index 8d018a3bcf0..b573d22212b 100644 --- a/tests/common/compile.rs +++ b/tests/common/compile.rs @@ -93,7 +93,10 @@ 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"])]; +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/LICENSE b/tests/testcases/LICENSE index 224dc5c6603..b4eab569b22 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/ 326 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 + 70 not upstream at all — 69 cases written here, plus `fred.txt`, which is output `40_stdio.c` writes when it runs and which was committed by accident. diff --git a/tests/testcases/tinycc/206_libc_refusals.c b/tests/testcases/tinycc/206_libc_refusals.c new file mode 100644 index 00000000000..8ea2bebd395 --- /dev/null +++ b/tests/testcases/tinycc/206_libc_refusals.c @@ -0,0 +1,177 @@ +/* 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, or memory it + cannot give. */ +#include +#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"; + case ENOMEM: return "ENOMEM"; + 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; + } + /* 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"); + 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("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("symlink", symlink(FILE_PATH, FILE_PATH ".symlink")); + 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)); + /* 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; 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)); + 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)); + 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)); + + /* 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, 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 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); + + /* 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)); + 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; 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 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; + 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 || 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); + 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..5de407a79ad --- /dev/null +++ b/tests/testcases/tinycc/206_libc_refusals.expect @@ -0,0 +1,57 @@ +execv: -1, ENOSYS +execve: -1, ENOSYS +setsid: -1, ENOSYS +getsid: -1, ENOSYS +gethostname: -1, ENOSYS +uname: -1, ENOSYS +link: -1, ENOSYS +symlink: -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, 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 +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 +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 +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 +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 +posix_madvise WILLNEED: 0 +posix_madvise 99: EINVAL +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 new file mode 100644 index 00000000000..201f70b2716 --- /dev/null +++ b/tests/testcases/tinycc/207_libc_names.c @@ -0,0 +1,171 @@ +/* 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 +#include +#include +#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 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" + +/* 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) { + switch (e) { + case EEXIST: return "EEXIST"; + case EINVAL: return "EINVAL"; + case ENOENT: return "ENOENT"; + default: return "another errno"; + } +} + +/* 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); +} + +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; + } + 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); + + 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)); +} + +static void listing(void) { + 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(LISTED); + if (!dir) { + refused("opendir", -1); + 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; + refused("opendir of nothing", opendir(LISTED "/none") == NULL ? -1 : 0); +} + +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\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("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 new file mode 100644 index 00000000000..a5091a68a7f --- /dev/null +++ b/tests/testcases/tinycc/207_libc_names.expect @@ -0,0 +1,19 @@ +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 +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 +logb(0): -inf, ERANGE +sysconf _SC_PAGE_SIZE: 4096 diff --git a/tests/testcases/tinycc/208_signal_mask.c b/tests/testcases/tinycc/208_signal_mask.c new file mode 100644 index 00000000000..be00e6f1061 --- /dev/null +++ b/tests/testcases/tinycc/208_signal_mask.c @@ -0,0 +1,87 @@ +/* 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; and a set as sigemptyset, sigaddset and sigfillset make + one. 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); + + 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 new file mode 100644 index 00000000000..31ed13d99b2 --- /dev/null +++ b/tests/testcases/tinycc/208_signal_mask.expect @@ -0,0 +1,15 @@ +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 +sigemptyset: 0, 0x0 +sigaddset USR1: 0, 0x200 +sigaddset 0: -1, EINVAL, 0x200 +sigaddset 65: -1, EINVAL, 0x200 +sigfillset: 0, 0xffffffffffffffff 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..ea39b59f952 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/c_aligned_alloc.rs @@ -0,0 +1,77 @@ +//! 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(); + // 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"); + 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)); + } + + 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/tests/toyos.rs b/tests/toyos.rs index 24ca74784cb..413a8dc820f 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -963,8 +963,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-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index ca034eb1541..0f00c9fc4aa 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1072,6 +1072,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)) @@ -2506,4 +2537,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 0487c33e712..439567e593f 100644 --- a/toyos-libc-copies/Cargo.toml +++ b/toyos-libc-copies/Cargo.toml @@ -1,18 +1,19 @@ [package] name = "toyos-libc-copies" -description = "A host differential test of userland libc's self-contained modules: its copies, fills and square roots against core's, its UTF-8 reader against core's, its number reader against the host C library and exact IEEE 754 rounding, and its long double widening against compiler-builtins." +description = "A host test of userland libc: its self-contained modules against core's, the host C library's, exact IEEE 754 rounding and compiler-builtins', and its headers against its definitions." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false -# `userland/libc/src/`'s architecture module, UTF-8 reader and number reader, -# compiled for the host this runs on and held against Rust's own and the host C -# library's. 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..68901ad25d4 --- /dev/null +++ b/toyos-libc-copies/src/descriptor_requests.rs @@ -0,0 +1,144 @@ +//! 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}; +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 +/// `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() { + let set_cloexec = header::int(FCNTL_H, "FD_CLOEXEC") as u64; + for (cmd, arg, want) in [ + ("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. + ("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(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)"); + } +} + +/// 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})"); + 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})"); + } + } +} + +/// 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")); + let fd_cloexec = header::int(FCNTL_H, "FD_CLOEXEC"); + let mut marks = fdreq::CloseOnExec::new(); + 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); + marks.set(4, true); + assert_eq!((marks.flags(3), marks.flags(4)), (0, fd_cloexec), "F_SETFD"); + + // 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"); + + // `open` without `O_CLOEXEC` of a number marked. + marks.set(5, true); + marks.opened(5, read_write); + assert_eq!(marks.flags(5), 0, "open over a mark"); +} diff --git a/toyos-libc-copies/src/dladdr_symbols.rs b/toyos-libc-copies/src/dladdr_symbols.rs new file mode 100644 index 00000000000..a75fedd8e2b --- /dev/null +++ b/toyos-libc-copies/src/dladdr_symbols.rs @@ -0,0 +1,118 @@ +//! `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; + +const SIZE: usize = 0x2000; +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_FUNC: u8 = (1 << 4) | 2; +const LOCAL_FUNC: u8 = 2; + +/// 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), +]; + +/// An image at the start of an 8-aligned buffer, every table addressed from +/// 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. + 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()); + put(PHDRS + 56 + 40, &0x100u64.to_le_bytes()); + let mut names = vec![0u8]; + 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() { + names.extend_from_slice(name.as_bytes()); + names.push(0); + } + put(at, &offset.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()); + } + 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()); + } + 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)); + } + 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 } +} + +#[test] +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))), + (0x1505, Some(("last", 0x1500))), + (0x1510, None), + (0x0800, None), + ] { + // 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(true); + 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/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/fparts_differential.rs b/toyos-libc-copies/src/fparts_differential.rs new file mode 100644 index 00000000000..43f513e8c6a --- /dev/null +++ b/toyos-libc-copies/src/fparts_differential.rs @@ -0,0 +1,78 @@ +//! `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_and_a_zero_its_pole() { + for x in inputs() { + // SAFETY: a pure function of its argument. + let host = unsafe { host_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/header.rs b/toyos-libc-copies/src/header.rs new file mode 100644 index 00000000000..0b92751b7ac --- /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"); +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 { + 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 543eef1b2cb..fd0393b3999 100644 --- a/toyos-libc-copies/src/lib.rs +++ b/toyos-libc-copies/src/lib.rs @@ -16,21 +16,65 @@ extern crate alloc; #[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/fdreq.rs"] +mod fdreq; +#[cfg(test)] +#[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)] +#[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)] +#[path = "../../userland/libc/src/text.rs"] +mod text; +#[cfg(test)] #[path = "../../userland/libc/src/utf8.rs"] mod utf8; +#[cfg(test)] +mod descriptor_requests; +#[cfg(test)] +mod dladdr_symbols; #[cfg(test)] mod errno_codes; #[cfg(test)] mod exact_hex; +#[cfg(test)] +mod fparts_differential; +#[cfg(test)] +mod header; +#[cfg(test)] +mod link_requests; +#[cfg(test)] +mod listing_reader; #[cfg(all(test, target_arch = "aarch64"))] mod long_double; #[cfg(test)] +mod memory_refusals; +#[cfg(test)] +mod prototypes; +#[cfg(test)] +mod signal_masks; +#[cfg(test)] mod strtonum_differential; #[cfg(test)] +mod text_differential; +#[cfg(test)] mod utf8_differential; #[cfg(test)] 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/listing_reader.rs b/toyos-libc-copies/src/listing_reader.rs new file mode 100644 index 00000000000..2554abababf --- /dev/null +++ b/toyos-libc-copies/src/listing_reader.rs @@ -0,0 +1,49 @@ +//! 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; + +/// 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) + } +} + +#[test] +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] +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 new file mode 100644 index 00000000000..4366343e090 --- /dev/null +++ b/toyos-libc-copies/src/memory_refusals.rs @@ -0,0 +1,55 @@ +//! What libc's memory calls refuse before the kernel or the allocator is +//! 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, MAP_PRIVATE, MAP_SHARED, PROT_EXEC, PROT_READ, PROT_WRITE}; + +#[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)), + // 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)), + ] { + 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 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/toyos-libc-copies/src/prototypes.rs b/toyos-libc-copies/src/prototypes.rs new file mode 100644 index 00000000000..7347a640ba7 --- /dev/null +++ b/toyos-libc-copies/src/prototypes.rs @@ -0,0 +1,781 @@ +//! 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"), + ("__cxa_atexit", "the C++ ABI's, which `cxxabi.h` declares"), + ("__cxa_thread_atexit_impl", "what libc++abi's `__cxa_thread_atexit` calls, which it declares"), + ("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/signal_masks.rs b/toyos-libc-copies/src/signal_masks.rs new file mode 100644 index 00000000000..a3c6810a59d --- /dev/null +++ b/toyos-libc-copies/src/signal_masks.rs @@ -0,0 +1,54 @@ +//! 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. A set as `sigaddset` and `sigfillset` make one, +//! against POSIX's words for each. + +use crate::header::{self, SIGNAL_H}; +use crate::sigmask; + +/// Signal `n`'s bit. +fn bit(n: i32) -> u64 { + 1 << (n - 1) +} + +#[test] +fn each_how_changes_the_mask_as_posix_says() { + 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, 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}"); + } +} + +#[test] +fn sigaddset_adds_its_signal_alone() { + for n in 1..=64 { + 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] { + 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 = header::signals(); + assert!(defined.len() > 15, "signal.h defines {} signals", defined.len()); + 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 new file mode 100644 index 00000000000..12e86fa48ae --- /dev/null +++ b/toyos-libc-copies/src/text_differential.rs @@ -0,0 +1,73 @@ +//! `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::header; +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 numbered = header::signals(); + 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/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/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 83383e88c64..d8bc572fdc9 100644 --- a/userland/libc/include/dlfcn.h +++ b/userland/libc/include/dlfcn.h @@ -11,8 +11,16 @@ extern "C" { #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/fcntl.h b/userland/libc/include/fcntl.h index 1d07513137c..e72e33fad25 100644 --- a/userland/libc/include/fcntl.h +++ b/userland/libc/include/fcntl.h @@ -22,11 +22,29 @@ extern "C" { #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 F_SETOWN 8 +#define F_GETOWN 9 +#define F_DUPFD_CLOEXEC 1030 #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); #ifdef __cplusplus } diff --git a/userland/libc/include/glob.h b/userland/libc/include/glob.h deleted file mode 100644 index 07508c33dee..00000000000 --- a/userland/libc/include/glob.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef _GLOB_H -#define _GLOB_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -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); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/userland/libc/include/limits.h b/userland/libc/include/limits.h index 7a882576299..54df895a2dd 100644 --- a/userland/libc/include/limits.h +++ b/userland/libc/include/limits.h @@ -24,6 +24,7 @@ #define MB_LEN_MAX 4 #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 4883ff8f1c9..a8b572551a6 100644 --- a/userland/libc/include/math.h +++ b/userland/libc/include/math.h @@ -62,6 +62,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/semaphore.h b/userland/libc/include/semaphore.h deleted file mode 100644 index c1a5041f268..00000000000 --- a/userland/libc/include/semaphore.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef _SEMAPHORE_H -#define _SEMAPHORE_H - -#ifdef __cplusplus -extern "C" { -#endif - -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); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/userland/libc/include/signal.h b/userland/libc/include/signal.h index 8f1e6100488..e430ed95528 100644 --- a/userland/libc/include/signal.h +++ b/userland/libc/include/signal.h @@ -14,7 +14,9 @@ extern "C" { #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 @@ -46,6 +48,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 { @@ -58,7 +63,11 @@ 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); #define SIG_BLOCK 0 diff --git a/userland/libc/include/stdlib.h b/userland/libc/include/stdlib.h index e0bebc26f82..f460dca89b1 100644 --- a/userland/libc/include/stdlib.h +++ b/userland/libc/include/stdlib.h @@ -52,6 +52,7 @@ long double strtold_l(const char *s, char **endptr, locale_t loc); 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 c909fa07de8..9833066c561 100644 --- a/userland/libc/include/string.h +++ b/userland/libc/include/string.h @@ -15,6 +15,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 edda25e8ff4..f0d7067e8bf 100644 --- a/userland/libc/include/sys/mman.h +++ b/userland/libc/include/sys/mman.h @@ -20,8 +20,27 @@ extern "C" { #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 + +#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/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 a6466c1744c..dfda2863f05 100644 --- a/userland/libc/include/sys/socket.h +++ b/userland/libc/include/sys/socket.h @@ -11,6 +11,7 @@ extern "C" { typedef unsigned int socklen_t; #define AF_UNSPEC 0 +#define AF_UNIX 1 #define AF_INET 2 #define SOCK_STREAM 1 @@ -32,13 +33,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 4a9f1c4793a..3dfdbe5106a 100644 --- a/userland/libc/include/sys/stat.h +++ b/userland/libc/include/sys/stat.h @@ -65,6 +65,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); #ifdef __cplusplus 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 06faf36813f..f60449eb1c3 100644 --- a/userland/libc/include/sys/wait.h +++ b/userland/libc/include/sys/wait.h @@ -15,7 +15,6 @@ extern "C" { #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); #ifdef __cplusplus 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 fca9c36896a..c93aa56e297 100644 --- a/userland/libc/include/unistd.h +++ b/userland/libc/include/unistd.h @@ -27,12 +27,23 @@ 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); +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); @@ -41,7 +52,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..a41ee60a54b --- /dev/null +++ b/userland/libc/src/elfsym.rs @@ -0,0 +1,80 @@ +//! What `dladdr` reads of a loaded image: whether an address lies in it, where +//! 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. + +use core::ffi::CStr; + +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)] +pub(crate) struct Image { + pub(crate) base: u64, + pub(crate) phdr: *const u8, + pub(crate) phnum: usize, +} + +impl Image { + /// # Safety + /// `phdr` points at `phnum` readable program headers. + 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`. + /// + /// # 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 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 [`SymTab::dladdr`] chooses it, from as many symbols as + /// [`Dynamic::sym_count`] counts. + /// + /// # Safety + /// 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 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 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)) + } +} diff --git a/userland/libc/src/errno.rs b/userland/libc/src/errno.rs index 250441f3cba..ce9cd41f9ce 100644 --- a/userland/libc/src/errno.rs +++ b/userland/libc/src/errno.rs @@ -28,12 +28,15 @@ pub(crate) const ENOMEM: i32 = 12; pub(crate) const EACCES: i32 = 13; pub(crate) const EBUSY: i32 = 16; pub(crate) const EEXIST: i32 = 17; +pub(crate) const ENODEV: i32 = 19; pub(crate) const EINVAL: i32 = 22; pub(crate) const EPIPE: i32 = 32; pub(crate) const ERANGE: i32 = 34; pub(crate) const EDEADLK: i32 = 35; pub(crate) const ENOSYS: i32 = 38; +pub(crate) const EOVERFLOW: i32 = 75; pub(crate) const EILSEQ: i32 = 84; +pub(crate) const EOPNOTSUPP: i32 = 95; pub(crate) const EAFNOSUPPORT: i32 = 97; pub(crate) const EADDRINUSE: i32 = 98; pub(crate) const ECONNRESET: i32 = 104; diff --git a/userland/libc/src/fdreq.rs b/userland/libc/src/fdreq.rs new file mode 100644 index 00000000000..45a76ccdb00 --- /dev/null +++ b/userland/libc/src/fdreq.rs @@ -0,0 +1,108 @@ +//! 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; +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); + } +} + +/// 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); + } + + 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 } + } +} diff --git a/userland/libc/src/fparts.rs b/userland/libc/src/fparts.rs new file mode 100644 index 00000000000..ff89184e8eb --- /dev/null +++ b/userland/libc/src/fparts.rs @@ -0,0 +1,63 @@ +//! 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` 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 an infinity, the NaN for a NaN. +pub(crate) fn logb(x: f64) -> Result { + if x.is_nan() { + return Ok(x); + } + if x.is_infinite() { + return Ok(f64::INFINITY); + } + if x == 0.0 { + return Err(Pole); + } + let e = exponent(x); + if e > -BIAS { + 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); + 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 c3943f7a935..8b184115473 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -6,19 +6,28 @@ extern crate alloc; mod arch; mod ctype; +mod elfsym; mod errno; +mod fdreq; +mod fparts; mod link; +mod linkreq; +mod listing; mod locale; mod math; mod memory; +mod memreq; mod misc; mod posix_io; mod printf; mod pthread; +mod refused; +mod sigmask; mod socket; mod stdio; mod string; mod strtonum; +mod text; mod time; mod utf8; mod wchar; diff --git a/userland/libc/src/link.rs b/userland/libc/src/link.rs index 7faffb24762..c417fc106d0 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,16 +51,84 @@ pub unsafe extern "C" fn dl_iterate_phdr(callback: Callback, data: *mut u8) -> i 0 } -/// One whole `SYS_QUERY_MODULES` answer: asked again while a `dlopen` -/// elsewhere grows it between the size and the read. -fn answer() -> Vec { - let mut buf = Vec::new(); +/// `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.as_ptr().cast()), + 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 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; + 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() }; } - buf.resize(need, 0); } } + +/// One whole `SYS_QUERY_MODULES` answer. +fn answer() -> Vec { + crate::listing::whole(syscall::query_modules).expect("SYS_QUERY_MODULES refused a buffer this process owns") +} 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/listing.rs b/userland/libc/src/listing.rs new file mode 100644 index 00000000000..40a4e476d20 --- /dev/null +++ b/userland/libc/src/listing.rs @@ -0,0 +1,35 @@ +//! 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`). + +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 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 537896ae224..3176755636b 100644 --- a/userland/libc/src/math.rs +++ b/userland/libc/src/math.rs @@ -241,6 +241,22 @@ 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 +} + +/// `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).unwrap_or_else(|crate::fparts::Pole| { + crate::errno::set(crate::errno::ERANGE); + f64::NEG_INFINITY + }) +} + // --- Utility functions --- #[no_mangle] 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/memreq.rs b/userland/libc/src/memreq.rs new file mode 100644 index 00000000000..fdfead9dbf0 --- /dev/null +++ b/userland/libc/src/memreq.rs @@ -0,0 +1,74 @@ +//! 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_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. +pub(crate) const PAGE: usize = 4096; + +/// Why `mmap` cannot give what it was asked. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum MapRefusal { + /// 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, + /// 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 { + 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 { + return Some(MapRefusal::File); + } + if prot & PROT_EXEC != 0 { + return Some(MapRefusal::Exec); + } + None +} + +/// 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/misc.rs b/userland/libc/src/misc.rs index 13ad9912ab7..ba2b635eeb6 100644 --- a/userland/libc/src/misc.rs +++ b/userland/libc/src/misc.rs @@ -211,13 +211,45 @@ pub unsafe extern "C" fn sigaction( 0 // success } +/// 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] -pub unsafe extern "C" fn sigprocmask( - _how: i32, _set: *const u64, _oldset: *mut u64, -) -> i32 { +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 @@ -230,17 +262,25 @@ 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; #[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, - _ => -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 3d1bfb27748..5b9da34b9ec 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::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::fdreq::CloseOnExec; +use crate::pthread::Lock; use crate::time::Timespec; // Constants (matching POSIX / Linux values) @@ -85,13 +89,22 @@ 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) => { + CLOEXEC.lock().opened(f.0 as i32, flags); + f.0 as i32 + } Err(e) => set_errno(e), } } +#[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 { + CLOEXEC.lock().set(raw_fd, false); syscall::close(fd(raw_fd)); 0 } @@ -194,6 +207,8 @@ pub unsafe extern "C" fn dup2(old_fd: i32, new_fd: i32) -> i32 { }; match syscall::dup2(fd(old_fd), slot) { Ok(f) => { + // 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), @@ -307,6 +322,29 @@ pub unsafe extern "C" fn lstat(path: *const u8, buf: *mut Stat) -> i32 { stat_impl(path, buf) } +/// `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 { + let Some(len) = crate::linkreq::target_len(size) else { + crate::errno::set(EINVAL); + return -1; + }; + // 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) => { + // `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 @@ -340,12 +378,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] @@ -355,117 +387,90 @@ 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 -} +static CLOEXEC: Lock = Lock::new(CloseOnExec::new()); -// pread/pwrite: emulate with seek + read/write + seek back +/// `arg` is the register C's variadic third argument arrives in (`fdreq`). #[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(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), + }; + } + Command::GetFd => return CLOEXEC.lock().flags(raw_fd), + Command::SetFd(cloexec) => { + CLOEXEC.lock().set(raw_fd, cloexec); + return 0; + } + Command::Invalid => EINVAL, + Command::Unsupported => crate::errno::ENOSYS, + }; + 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], } -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 { 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(_) => { super::memory::free(buf); 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, - _ => { super::memory::free(buf); 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; - 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 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; - (*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.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 } @@ -496,24 +501,60 @@ 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 } +} + +/// 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 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 new file mode 100644 index 00000000000..ea313350d88 --- /dev/null +++ b/userland/libc/src/refused.rs @@ -0,0 +1,145 @@ +//! What this C library refuses. 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 +} + +/// 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() +} + +/// `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 { + 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/sigmask.rs b/userland/libc/src/sigmask.rs new file mode 100644 index 00000000000..9c665db43a5 --- /dev/null +++ b/userland/libc/src/sigmask.rs @@ -0,0 +1,39 @@ +//! 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`). + +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)); + +/// 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 { + let next = match how { + SIG_BLOCK => mask | set, + SIG_UNBLOCK => mask & !set, + SIG_SETMASK => set, + _ => return None, + }; + Some(next & !UNBLOCKABLE) +} diff --git a/userland/libc/src/stdio.rs b/userland/libc/src/stdio.rs index 129467175d8..00e558458a8 100644 --- a/userland/libc/src/stdio.rs +++ b/userland/libc/src/stdio.rs @@ -648,7 +648,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 72a21972ab0..1cd8125bba9 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", + } +}