The set libwayland's event loop is made of, in kernel/sys/sys_event.c: four kinds of descriptor with no inode, FD_EVENT, that read() drains and poll() reports on. They were table entries returning ENOSYS that read as done. epoll registers (descriptor, file) pairs, so a closed descriptor leaves the set; level- and edge-triggered, EPOLLONESHOT. Waiting is poll()'s, a tick at a time. The old eventfd and signalfd numbers (284, 282) were aliased to the new calls and read flags from a garbage register; they are their own entries now, with epoll_create and epoll_pwait. cmd/evtest checks each, then all four in one set. The plans are brought up to date: everything Wayland was waiting on is built except setsockopt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
132 lines
6 KiB
Markdown
132 lines
6 KiB
Markdown
# Syscalls, and the Linux personality
|
|
|
|
Two things happen at this boundary. SchOS has its own syscall numbering, and it
|
|
also answers Linux's — which is why musl, toybox and mksh run unmodified.
|
|
|
|
---
|
|
|
|
## One table, everything generated
|
|
|
|
**Syscalls are declared in exactly one place:**
|
|
[`kernel/sys/table.inc`](../../kernel/sys/table.inc). One line each.
|
|
|
|
```c
|
|
SYSCALL(nr, linux_nr, name) // implemented as sys_<name>()
|
|
SYSCALL_ALIAS(linux_nr, name) // a second Linux number for the same handler
|
|
```
|
|
|
|
Three things are generated from it by macro expansion, and none is
|
|
hand-maintained:
|
|
|
|
| Generated | Where |
|
|
|---|---|
|
|
| The `SYS_*` constants | `include/core/syscall.h` |
|
|
| The dispatch table | `kernel/sys/syscall.c` |
|
|
| The Linux translation map | `kernel/sys/linux_abi.c` |
|
|
|
|
Adding a syscall is therefore two edits: a line here, and a body written with
|
|
`SYSCALL_DEFINE(<name>)` in one of the `kernel/sys/sys_*.c` files.
|
|
|
|
**This replaced a two-place scheme and the reason is worth keeping.** The Linux
|
|
number used to live in a second hand-written table. Adding a syscall meant three
|
|
edits across two files, and forgetting the second one left the call *unreachable
|
|
from every musl-linked program* while looking perfectly registered in the first.
|
|
A syscall can no longer be half-registered.
|
|
|
|
## How a Linux binary runs
|
|
|
|
The translation is a flat array indexed by Linux syscall number, whose value is
|
|
the native `SYS_*` number — or 0, meaning not implemented, which dispatches to
|
|
`-ENOSYS`.
|
|
|
|
It is enabled **per process**, and selected automatically by `exec()`:
|
|
|
|
```c
|
|
// musl binaries have entry >= 0x100000; SchOS-native ones are linked at 0x4000
|
|
if (elf.entry >= LINUX_ABI_ENTRY_THRESHOLD)
|
|
myproc()->syscall_map = linux_syscall_map;
|
|
```
|
|
|
|
A link address is a crude discriminator and it is honest about being one. It
|
|
works because the two classes of binary in this system are linked by two
|
|
different linker scripts at two very different addresses.
|
|
|
|
There is also `enable_linux_abi()` as an explicit call, which is why
|
|
`cmd/schinit/main.c` carries a warning not to use it: doing so mid-process would
|
|
renumber the syscalls the caller still has to make.
|
|
|
|
### Native vs. Linux numbering
|
|
|
|
Some calls have no Linux equivalent and are marked `NO_LINUX`: `sleep`,
|
|
`uptime`, `reboot`, `shutdown`, `getmouse`, `vga_sync`, `schfs_index`,
|
|
`schfs_query`. These are reachable only from first-party programs linked
|
|
against `lib/`.
|
|
|
|
They all land on the same scratch index in the translation array and overwrite
|
|
each other there, which is harmless — no real Linux syscall uses that number, so
|
|
the slot is never dispatched.
|
|
|
|
### Where the two disagree on a name
|
|
|
|
`shutdown` is the one collision. SchOS has a native `shutdown()` that powers the
|
|
machine off, and Linux number 48 is the socket call. Two syscalls cannot share a
|
|
`sys_` function name, so Linux 48 maps to `sockshutdown`.
|
|
|
|
## What is implemented
|
|
|
|
Roughly 190 native numbers covering:
|
|
|
|
| Area | |
|
|
|---|---|
|
|
| Process | fork, exec, exit, wait/waitpid/wait4/waitid, clone, kill, sessions, process groups |
|
|
| Identity | the full real/effective/saved set for both uid and gid, groups, umask |
|
|
| Memory | mmap, munmap, mprotect, brk/sbrk, madvise, System V shared memory |
|
|
| Filesystem | the ordinary set, the whole `*at()` family, statfs, xattr, mount/umount2 |
|
|
| I/O | read/write, readv/writev, pread/pwrite, sendfile, poll/select/ppoll/pselect6, epoll (create, create1, ctl, wait, pwait), ioctl, fcntl |
|
|
| Sockets | socket, bind, listen, accept, connect, sendto, recvfrom, socketpair, setsockopt, getsockopt |
|
|
| Signals | rt_sigaction, rt_sigreturn, rt_sigsuspend, rt_sigpending, rt_sigtimedwait, sigaltstack |
|
|
| Time | gettimeofday, clock_gettime, clock_getres, nanosleep, clock_nanosleep, alarm, timers |
|
|
| Notification | eventfd, eventfd2, timerfd_create/settime/gettime, signalfd, signalfd4 -- see `kernel/sys/sys_event.c` |
|
|
|
|
### Argument fetching
|
|
|
|
`fetchint`, `fetchaddr`, `fetchstr` and `argptr` validate a user pointer through
|
|
the page tables and nothing else. They previously rejected any address
|
|
`>= proc->sz`, on the assumption that every valid user pointer lives in the
|
|
heap — which is wrong for stack addresses and for every `mmap`'d buffer, i.e.
|
|
for most pointers real programs pass.
|
|
|
|
`uva2ka()` translates a user virtual address and **returns the in-page offset**,
|
|
which it did not originally do. Every caller that read or wrote through a
|
|
non-page-aligned pointer — `TIOCGPGRP`, `TCGETS`, `getcwd`, signal masks,
|
|
`times()`, `sysinfo()` — was touching the wrong address whenever the pointer was
|
|
not page-aligned, i.e. almost always.
|
|
|
|
**`uva2ka()` guarantees only the page containing that address.** A structure
|
|
copied in bulk through one translation runs off the end of the mapped page when
|
|
the pointer lands near a boundary — and `P2V` is a flat offset, so "the next
|
|
page" in kernel space has nothing to do with the user's next page. That bug
|
|
corrupted unrelated kernel memory and crashed every freshly `exec`'d shell. Copy
|
|
field by field, each through its own translation, or use `copyin`/`copyout`.
|
|
|
|
---
|
|
|
|
## What it does not do
|
|
|
|
- **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and nothing else.**
|
|
`SCM_CREDENTIALS` is refused rather than ignored. See
|
|
[networking.md](networking.md#descriptor-passing).
|
|
- **No `memfd_create`.**
|
|
- **No `getsockname` or `getpeername`**, and `accept()` does not report an
|
|
AF_INET peer's address.
|
|
- **`setsockopt` handles six options** and answers `ENOPROTOOPT` for everything
|
|
else. `SO_RCVTIMEO`/`SO_SNDTIMEO` are among the refused.
|
|
- **`clone` does not share an address space**, so it is `fork` with extra steps.
|
|
- **`sched_setaffinity` is accepted and ignored.**
|
|
- **`getrusage` does not distinguish `RUSAGE_CHILDREN`** from `RUSAGE_SELF`, and
|
|
fills in only the calling process's own tick count.
|
|
- **The ABI is chosen by link address.** It works, and it is a heuristic rather
|
|
than a fact about the binary.
|
|
- **`/proc/<pid>/exe` is a regular file, not a symlink**, so `readlink()` on it
|
|
fails and `ps -o comm` prints `[init]` rather than `init`. Default `ps` and
|
|
`top` output is unaffected.
|