include: one folder per subsystem, and includes that name it

Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 15:05:12 +02:00
commit a1c965da1e
267 changed files with 1560 additions and 1440 deletions

View file

@ -0,0 +1,3 @@
static char *const rcinitcmd[] = { "/bin/sh", NULL };
static char *const rcrebootcmd[] = { "/bin/halt", "-rb", NULL };
static char *const rcpoweroffcmd[] = { "/bin/halt", "-po", NULL };

15
include/core/date.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
struct rtcdate {
uint second;
uint minute;
uint hour;
uint day;
uint month;
uint year;
};
struct timeval {
long tv_sec;
long tv_usec;
};

108
include/core/dbg.h Normal file
View file

@ -0,0 +1,108 @@
#pragma once
#include "core/types.h"
struct sbuf;
// Asking the kernel a question.
//
// docs/architecture/debugging.md milestone D2. The thing this exists to end is the
// ninety-second loop: add a cprintf, rebuild, boot, read the serial log, and
// learn the answer to exactly the one question you thought of before the
// rebuild.
//
// The interesting part is not the views. It is that a subsystem publishes its
// own state with **one line at the bottom of the file that owns the data**, and
// nothing central has to be edited to add one:
//
// dbg_register("dma", dma_dump, "the DMA arena's free extents");
//
// That is the difference between something that stays current and something
// that does not. /proc/sysrq is the version that does not -- a switch in one
// file with a letter per action, which is why after two years it holds three
// self-tests and no state at all.
//
// Views run in ordinary process context, from a write to /dev/dbg or from
// sysrq, so they may take locks. **They must not be called from panic().**
// The argument is whatever followed the view's name on the command line, or
// "" when there was none. Most views ignore it; the ones that can produce a
// hundred lines use it to narrow -- `vma 4` rather than `vma`. It is also the
// shape milestone D3 needs, since `whose <addr>` is nothing but an argument.
typedef void (*dbg_view_fn)(struct sbuf *out, const char *arg);
// Publish a view. `what` is one line, shown by `help`.
//
// Safe to call from any subsystem's init, in any order: the table is static
// and needs no initialisation of its own.
void dbg_register(const char *name, dbg_view_fn fn, const char *what);
// The same, for a view that **changes something**.
//
// Nearly every view only reads, and the few that do not are worth separating
// because of where this can now be reached from. `watch 0xffff...` arms a debug
// register; that is a legitimate thing for a debugger to do and an illegitimate
// thing for a stranger on a TCP socket to do, and milestone D5 put a TCP socket
// in front of it.
//
// The flag lives on the registration and not in a list somewhere, for the same
// reason the registration exists at all: a central list of which views are
// dangerous is a list that goes stale, and it goes stale in the direction where
// a new view is quietly treated as safe.
void dbg_register_rw(const char *name, dbg_view_fn fn, const char *what);
// Answer only the views that read.
//
// `/dev/dbgro` is the same device on minor 1. The minor is the one piece of
// per-open state a character device already sees, so *which node you opened* is
// the permission -- there is nothing to latch, nothing to remember between the
// write and the read, and no way to escalate from one descriptor to the other.
//
// `schdbgd` opens that one and never the other, so nothing reachable over the
// network can arm a watchpoint. See cmd/schdbgd.
int dbg_command_ro(const char *line, struct sbuf *out, int ro);
// --- whose <addr> -----------------------------------------------------------
//
// The query the views cannot answer between them: *who owns this page?*
//
// Each view speaks for one subsystem, which is what keeps them cheap and
// current -- and it is exactly why none of them can see the bug that matters.
// A page on the DMA arena's free list looks fine from dma.c. The same page on
// kalloc's free list looks fine from kalloc.c. That it is on both is a fact no
// single subsystem holds, and it is the bug that cost this project a day.
//
// So `whose` asks every subsystem about one page and counts the answers. The
// contributors are registered the same way views are, for the same reason:
// adding one is a line in the file that owns the data.
struct whose {
uint64 phys; // the page being asked about, page-aligned
uint64 off; // how far into it the original address was
int owners; // allocators and reserved regions claiming it
int free_owners; // of those, how many believe it is unused
int mappings; // user page tables mapping it
};
typedef void (*dbg_whose_fn)(struct sbuf *out, struct whose *w);
// Publish a claimant. `name` is the column it prints under.
void dbg_register_whose(const char *name, dbg_whose_fn fn);
// Run one command line and render the answer. Returns the number of bytes
// written into `out`.
//
// The protocol is lines of text -- see docs/architecture/debugging.md for why that and
// not a typed binary format. Commands:
//
// help what this kernel can be asked
// all every view, in registration order
// <name> [arg] that view, optionally narrowed
// whose <addr> every subsystem's claim on one page
int dbg_command(const char *line, struct sbuf *out);
// Every view, straight to the console. This is what sysrq 'g' runs, and it is
// the level-0 path from the plan: no process, no filesystem, no client.
void dbg_dump_all(void);
#define DBG_MAJOR 10

740
include/core/defs.h Normal file
View file

@ -0,0 +1,740 @@
#pragma once
struct buf;
struct context;
struct file;
struct inode;
struct pipe;
struct proc;
struct rtcdate;
struct spinlock;
struct sleeplock;
struct stat;
struct superblock;
struct trapframe;
struct dirent;
struct device;
// entryother.S / syscall.S
void syscall_entry(void);
void ignore_sysret(void);
// bio.c
void binit(void);
struct buf* bread(uint, uint64);
void brelse(struct buf*);
void bwrite(struct buf*);
// Modify without writing. The buffer stays dirty in the cache until an
// eviction or a bflush() takes it out; the caller is promising to ask for the
// ordering it needs explicitly rather than relying on write order.
void bdirty(struct buf*);
// Write out every dirty buffer for `dev`, then make the device commit them.
// Returns 0, or -1 if the device could not confirm durability.
int bflush(uint dev);
// console.c
struct termios;
void consoleinit(void);
// The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the
// only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG,
// ICRNL, INLCR, IGNCR, ISTRIP and VMIN.
void cons_get_termios(int minor, struct termios *);
void cons_set_termios(int minor, const struct termios *);
void cons_flush_input(int minor);
int cons_fg_pgrp(int minor);
void cons_set_fg_pgrp(int minor, int pg);
int console_vt_hotkey(int key);
extern uint vt_chord_mods; // kernel/dev/kbd.c -- the switch chord
extern uint vt_chord_base;
struct vt *vt_for_minor(int minor);
extern uint lapic_timer_hz; // measured, kernel/arch/x86_64/lapic.c
extern uint64 tsc_hz;
extern uint64 boot_tsc;
void monotonic(uint64 *sec, uint64 *usec);
int proc_write_schinit(int user_src, uint64 src, uint n);
int proc_wedge(const char *name);
void pciuser_init(void);
int pciuser_ioctl(uint64 request, uint64 argp);
int pciuser_read(struct inode*, uint64, char*, int);
int pciuser_write(struct inode*, uint64, char*, int);
int pciuser_mmio_ok(uint64 phys, uint64 len);
void pciuser_release_all(struct proc*);
int pciuser_owner_pid(int idx);
int proc_reincarnate(int status);
void proc_kill_init(void);
extern uint init_generation;
void cprintf(char*, ...);
// Boot chatter: always in the klog, on the screen only when boot_verbose.
// See the note above kinfo() in kernel/core/console.c for which is which.
void kinfo(char*, ...);
extern int boot_verbose;
extern int boot_debug;
// A log line: the serial port only, never the display.
//
// console_takeover() hands the framebuffer to userspace on the first exec, and
// after that the screen belongs to whoever is using the machine. A kernel
// message arriving then is not information they asked for -- it lands in the
// middle of their shell, or their editor, or a compositor's output. The DHCP
// lease is what made this necessary: it turns up seconds after the prompt does,
// so the first thing anyone sees after booting is the kernel talking over them.
//
// The same rule the boot path already follows, and the reason cons_serial_only
// starts set: the display is the product, the serial line is the diagnostics.
// klog is that rule for messages that cannot be timed to happen before
// userspace starts.
//
// A macro rather than a function because cprintf's varargs cannot be forwarded
// -- there is no vcprintf, and adding one to duplicate the formatter would cost
// more than it buys. cprintf holds cons.lock for the whole message, so the flag
// cannot be seen half-changed by another CPU printing at the same time.
//
// Nothing is lost: serial is where bootcheck reads from and where you want
// these when something is wrong, and anything a *user* should see belongs in a
// file they can read -- /proc/netif, for the example above.
extern int cons_serial_only;
#define klog(...) do { \
int _klog_was = cons_serial_only; \
cons_serial_only = 1; \
cprintf(__VA_ARGS__); \
cons_serial_only = _klog_was; \
} while (0)
void consoleintr(int(*)(void));
void panic(char*) __attribute__((noreturn));
// exec.c
int exec(char*, char**, char**);
// file.c
struct file* filealloc(void);
int fdalloc(struct file*);
void fileclose(struct file*);
struct file* filedup(struct file*);
void fileinit(void);
int fileread(struct file*, char*, int n);
int file_poll(struct file*, int events);
int filestat(struct file*, struct stat*);
int filewrite(struct file*, char*, int n);
// fs.c
void fsinit(int);
int dirlink(struct inode*, char*, uint64);
struct inode* dirlookup(struct inode*, char*, uint*);
// Remove the entry `name` at the cookie dirlookup returned in *poff.
int dirunlink(struct inode*, char*, uint);
// 1 if the directory holds no real entries, 0 if it does, -1 on error.
int dirempty(struct inode*);
// Fill `out` with getdents64 records starting at *poff, which is an opaque
// position the filesystem advances. Returns bytes written, 0 at end of
// directory. The inode must be locked.
int getdents(struct inode*, char *out, int cap, uint64 *poff);
// Mount table: a substitution applied during path resolution. Nothing on disk
// records a mount, so unmounting is forgetting.
void mountinit(void);
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
// Ask before mutating, not after: the create path panics on a failed dirlink.
int fs_readonly(uint dev);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type);
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
// bytes, `type` 16. Returns 0 for a live entry, -1 for an empty slot.
int mount_get(int i, char *path, char *source, char *type,
uint *fsdev);
int mount_del_by_root(uint fsdev, uint64 root);
struct inode* ialloc(uint, short);
struct inode* iget(uint, uint64);
struct inode* idup(struct inode*);
void iinit(void);
void ilock(struct inode*);
void iput(struct inode*);
void iunlock(struct inode*);
void iunlockput(struct inode*);
void iupdate(struct inode*);
int namecmp(const char*, const char*);
struct inode* namei(char*);
struct inode* nameiparent(char*, char*);
struct inode* nameifrom(struct inode*, char*);
struct inode* nameiparentfrom(struct inode*, char*, char*);
int readi(struct inode*, int, uint64, uint64, uint);
void stati(struct inode*, struct stat*);
int writei(struct inode*, int, uint64, uint64, uint);
void itrunc(struct inode*);
void itrunc_to(struct inode*, uint64);
unsigned int BKDRHash(char * str);
uint tree_expand(struct inode *ip, uint off);
void tree_search(struct inode* dp, char* name, uint* poff, struct dirent* de,uint add);
void build_dir_tree(struct inode* dp,uint64 pinum);
struct inode* divesymlink(struct inode *);
// A socket node for AF_UNIX bind(2), and unlink by path for the failure that
// follows it. See the notes on each in kernel/sys/sys_fs.c.
struct inode* create_sock(char *path);
int unlink_path(char *path);
void readsb(int dev, struct superblock *sb);
// pci.c -- bus enumeration. Drivers that need to *find* their hardware use
// these; drivers for things that have lived at a fixed address since 1984
// (the PIC, the PIT, the legacy IDE ports) do not.
struct pci_dev;
struct pci_driver;
void pci_init(void);
uint32 pci_read32(uint8, uint8, uint8, uint8);
uint16 pci_read16(uint8, uint8, uint8, uint8);
uint8 pci_read8(uint8, uint8, uint8, uint8);
void pci_write32(uint8, uint8, uint8, uint8, uint32);
void pci_enable(struct pci_dev *, uint16 bits);
void pci_disable(struct pci_dev *, uint16 bits);
uint8 pci_find_cap(struct pci_dev *, uint8 id);
int pci_msi_enable(struct pci_dev *, int vector, int cpu);
int pci_count(void);
struct pci_dev *pci_get(int);
struct pci_dev *pci_find_class(uint8 class, uint8 subclass);
struct pci_dev *pci_find_id(uint16 vendor, uint16 device);
void pci_bind(const struct pci_driver *);
int pci_info(char *buf, int cap);
// drivers/registry.c -- the one list of every driver in the kernel.
void pci_bind_all(void);
// xhci.c — `echo u > /proc/sysrq`
void xhci_dump(void);
// ide.c
void ideinit(void);
void ideintr(void);
void iderw(struct buf*);
// For probes, not for reads: iderw() panics on an absent drive, so anything
// that is guessing whether a disk exists has to ask here first.
int ide_disk_present(int dev);
// ATA FLUSH CACHE: block until the drive has committed its write cache.
int ideflush(int dev);
// fs/schfs/kmount.c -- SchFS.
//
// Objects are named by 64-bit object id, not by a path: resolution is the
// caller's business, so these compose with a VFS rather than assuming one.
// Every schfs_k* that changes anything is one transaction and is durable by the
// time it returns.
struct schfs_inode;
// Mounting. Slot 0 is the root filesystem, mounted by proc.c before pid 1 has a
// working directory; schfs_kmount_dev takes any other SchFS disk and returns
// the filesystem device number it landed on, or a negative errno.
void schfs_kmount(uint bdev);
int schfs_kmount_dev(uint bdev);
int schfs_kumount(uint dev);
int schfs_kmounted(void);
// Read side. `dev` is a filesystem device number -- a live volume or a mounted
// snapshot -- not the block device underneath it.
uint64 schfs_klookup(uint dev, const char *path);
long schfs_kread_dev(uint dev, uint64 ino, uint64 off, void *buf,
unsigned n);
int schfs_kstat_dev(uint dev, uint64 ino, struct schfs_inode *out);
uint64 schfs_klookup_at_dev(uint dev, uint64 dir, const char *name,
unsigned len);
int schfs_kreaddir_dev(uint dev, uint64 dir,
int (*fn)(uint64, unsigned short, const char *,
unsigned, void *),
void *arg);
// Resume a directory walk at a tree key. The callback gets each entry's hash,
// which is what getdents hands back to userland as its position.
int schfs_kreaddir_from_dev(uint dev, uint64 dir, uint64 start_hash,
int (*fn)(uint64, unsigned short, const char *,
unsigned, uint64, void *),
void *arg);
uint64 schfs_kroot(uint dev);
void schfs_lockinfo(int slot, int *locked, int *owner_pid, int *depth);
int schfs_kinfo(char *buf, int cap);
// Write side. Each is one transaction and is durable by the time it returns,
// unless the caller has a bracket open -- see schfs_ktx_open.
int schfs_kcreate(uint dev, uint64 dir, const char *name,
unsigned namelen, unsigned short type, uint mode,
unsigned short major, unsigned short minor, uint64 *out);
long schfs_kwrite(uint dev, uint64 ino, uint64 off, const void *buf,
unsigned n);
int schfs_ktruncate(uint dev, uint64 ino, uint64 size);
int schfs_kunlink(uint dev, uint64 dir, const char *name,
unsigned namelen);
int schfs_klink(uint dev, uint64 dir, const char *name,
unsigned namelen, uint64 ino);
int schfs_krename(uint dev, uint64 olddir, const char *oldname,
unsigned oldlen, uint64 newdir, const char *newname,
unsigned newlen);
int schfs_ksetattr(uint dev, uint64 ino,
const struct schfs_inode *in);
int schfs_ksync(uint dev);
int schfs_ksync_all(void);
// Share src's data blocks with dst without copying. Both stay writable; the
// first write to either copies only the run it touches. Behind FICLONE.
int schfs_kreflink(uint dev, uint64 src, uint64 dst);
// Attributes, and the index over them. See kernel/fs/schfs/attr.c.
int schfs_kattr_set(uint dev, uint64 ino, const char *name,
unsigned namelen, const void *val, unsigned vallen,
unsigned short type);
int schfs_kattr_get(uint dev, uint64 ino, const char *name,
unsigned namelen, void *out, unsigned cap,
unsigned *vallen);
int schfs_kattr_del(uint dev, uint64 ino, const char *name,
unsigned namelen);
int schfs_kindex(uint dev, const char *name, unsigned namelen);
int schfs_kquery(uint dev, const char *name, unsigned namelen,
const void *val, unsigned vallen, uint64 *out, int cap);
// Bracket a run of changes into one transaction group. Every schfs_k* call made
// between open and close joins it and commits once, at the close; the bracket
// nests, and holds the volume lock for its whole extent. Costly to leave open —
// nothing else touches that volume meanwhile — so bracket a bounded piece of
// work such as one write(2), not a whole syscall's worth of unrelated steps.
// schfs_ktx_close returns the commit's status, or -1 if the caller had no
// bracket open.
void schfs_ktx_open(uint dev);
int schfs_ktx_close(uint dev);
// Volume counts for statfs. 0 if `dev` is a mounted SchFS volume (live or
// snapshot) and the counts are filled in, -1 if it is not.
int schfs_kstatfs(uint dev, uint64 *total, uint64 *freeblocks,
uint64 *objects);
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
// object it names are separate operations. The VFS keeps the link count.
int schfs_kdirempty(uint dev, uint64 dir);
int schfs_kalloc_object(uint dev, unsigned short type, uint mode,
uint64 *out);
int schfs_kdiradd(uint dev, uint64 dir, const char *name,
unsigned len, uint64 ino, unsigned short type);
int schfs_kdirdel(uint dev, uint64 dir, const char *name,
unsigned len);
int schfs_kfree(uint dev, uint64 ino);
// A snapshot mounted read-only: the same block device with an older tree root.
// The read path takes a device number so the VFS can address either view; there
// is no write path, because a snapshot that could be written to would not be
// one. See kernel/fs/schfs/kmount.c.
// Returns the device number of the slot the snapshot landed in, or negative.
int schfs_ksnap_mount(uint dev, const char *name, unsigned namelen);
void schfs_ksnap_unmount(uint dev);
uint64 schfs_ksnap_root(uint dev);
// ramdisk.c
uint uptime(void);
// procfs.c
int procread(struct inode*, uint64, char*, int);
int procwrite(struct inode*, uint64, char*, int);
// Shared with the /proc filesystem (kernel/fs/procfs.c): one owns the
// namespace, the other the content.
int proc_gen_global(int id, char *buf, int cap);
int proc_write_sysrq(int user_src, uint64 src, uint n);
void procfs_mount(void);
void devfs_mount(void);
// input.c -- raw evdev event devices
void inputinit(void);
int inputread(struct inode*, uint64, char*, int);
int inputwrite(struct inode*, uint64, char*, int);
int inputready(struct inode*);
void inputclose(struct inode*);
void inputopen(struct inode*);
int input_ioctl(struct inode*, int request, addr_t arg);
int input_grab(int dev, int on);
int input_is_grabbed(int dev);
void input_report(int dev, int type, int code, int value);
// An absolute pointing device (usb-tablet). Coordinates are already in screen
// pixels; see input_mouse_abs in kernel/dev/input.c for why this is separate
// from the relative path.
void input_mouse_abs(int x, int y, int buttons);
void mouse_set_abs(int x, int y, int buttons);
void input_sync(int dev);
int input_ready(int dev);
void input_ps2_scancode(int raw, int e0);
void input_key(int hid_usage, int down);
int input_hid_keycode(int hid_usage);
void input_mouse(int dx, int dy, int buttons);
void ramdiskinit(void);
void ramdiskrw(struct buf*);
int ramdisk_present(void);
// usb_storage.c
void usb_storage_rw(struct buf*);
int usb_storage_present(void);
// SCSI SYNCHRONIZE CACHE: the same barrier as ideflush, over bulk transport.
int usb_storage_flush(void);
void usb_storage_dump(void); // `echo s > /proc/sysrq`
// ioapic.c
void ioapicenable(int irq, int cpu);
void ioapicroute(int irq, int cpu, int level_low);
void ioapicmask(int irq);
extern uchar ioapicid;
void ioapicinit(void);
// dma.c — see include/core/dma.h for the full contract
void dma_init(void*, void*);
// watch.c -- hardware watchpoints on DR0-DR3. See docs/architecture/debugging.md, D4.
struct trapframe;
void watch_init(void);
void watch_sync(void);
int watch_trap(struct trapframe *tf);
// netuser.c -- /dev/netdev, an interface backed by a process. See netuser.h.
void netuser_init(void);
int netuser_read(struct inode*, uint64, char*, int);
int netuser_write(struct inode*, uint64, char*, int);
int netuser_ready(struct inode*);
int netuser_ioctl(uint64 request, uint64 argp);
void netuser_release_all(struct proc*);
// dbg.c -- /dev/dbg, the introspection registry. See include/core/dbg.h.
int dbg_read(struct inode*, uint64, char*, int);
void dbg_request_dump(void);
void dbg_thread_start(void);
int dbg_write(struct inode*, uint64, char*, int);
// klog.c -- the kernel log that outlives the kernel. See include/core/klog.h.
void klog_init(void *base, uint64 bytes);
void klog_putc(int c);
int klog_read(uint64 off, char *dst, int n);
uint64 klog_len(void);
uint64 klog_prev_bytes(void);
int klog_recovered(void);
int klog_render_syslog(char *dst, int cap);
// ksym.c -- naming a kernel address. See docs/architecture/debugging.md, D0.
int ksym_ready(void);
const char* ksym_lookup(uint64 addr, uint64 *off);
int ksym_fmt(char *buf, int cap, uint64 addr);
void *dma_alloc(uint64 size, uint64 align);
void dma_free(void*, uint64 size);
addr_t dma_phys(const void*);
// /dev/random and /dev/urandom. Seeded once at boot; see kernel/dev/cdev.c.
void random_init(void);
uint64 dma_free_bytes(void);
uint64 dma_total_bytes(void);
// kalloc.c
char* kalloc(void);
uint64 kalloc_free_bytes(void);
void kfree(char*);
void kinit1(void*, void*);
void kinit_high(void);
void kinit2(void);
// kbd.c
void kbdintr(void);
// mouse.c
void mouseinit(void);
void mouseintr(void);
void mouse_inject(int dx, int dy, int buttons);
int get_mouse_x(void);
int get_mouse_y(void);
uint8 get_mouse_buttons(void);
// lapic.c
void cmostime(struct rtcdate *r);
// time.c -- wall-clock time, from the RTC read once at boot plus uptime.
void time_init(void);
void wallclock(uint64 *sec, uint64 *usec);
extern uint64 boot_epoch;
int cpunum(void);
extern volatile uint* lapic;
void lapiceoi(void);
void lapicinit(void);
void lapicstartap(uchar, uint);
void microdelay(int);
// log.c
void initlog(int dev);
void log_write(struct buf*);
void begin_op();
void end_op();
// mp.c
extern int ismp;
void mpinit(void);
// pipe.c
int pipealloc(struct file**, struct file**);
void pipeclose(struct pipe*, int);
int piperead(struct pipe*, char*, int);
int pipewrite(struct pipe*, char*, int);
// net/unix.c
struct socket;
void sockinit(void);
struct socket* socknew(int domain, int type, int proto);
void sockclose(struct socket*);
void sockdup(struct socket*);
int sockread(struct socket*, char*, int);
int sockwrite(struct socket*, char*, int);
// The same two, carrying descriptors. See kernel/net/unix.c.
int sockread_fds(struct socket*, char*, int, struct file**, int*);
int sockwrite_fds(struct socket*, char*, int, struct file**, int);
int sockbind(struct socket*, const char *path, uint dev, uint64 ino);
int socklisten(struct socket*, int backlog);
struct socket* sockaccept(struct socket*);
int sockconnect(struct socket*, uint dev, uint64 ino);
int sockreadable(struct socket*);
int sock_hup(struct socket*);
int sock_shutdown(struct socket*, int how);
int sockpair(struct socket **a, struct socket **b);
const char* sockname(struct socket*);
// AF_INET addressing. Separate from sockbind/sockconnect because the families
// do not name the same kind of thing; `addr` and `port` are network order.
int sockrecvfrom(struct socket*, char *buf, int n,
uint *addr, ushort *port);
int socksendto(struct socket*, char *buf, int n,
uint addr, ushort port);
int sockbind_in(struct socket*, uint addr, ushort port);
int sockconnect_in(struct socket*, uint addr, ushort port);
int sock_domain(struct socket*);
// Socket options. The syscall layer does the user-memory marshalling and these
// do the work, because struct socket's fields live in kernel/net/unix.c and the
// syscall layer deliberately cannot see them.
int sockopt_set(struct socket*, int level, int name, int val);
int sockopt_get(struct socket*, int level, int name, int *out);
// net/inet.c -- AF_INET on lwIP's raw API. Called only from net/unix.c, which
// is where the per-domain dispatch lives.
void inet_init(void);
void* inet_new(int type, int proto);
int inet_bind(void *h, uint addr, ushort port);
int inet_listen(void *h, int backlog);
int inet_setopt(void *h, int level, int name, int val);
void* inet_accept(void *h);
int inet_connect(void *h, uint addr, ushort port);
int inet_read(void *h, char *buf, int n);
int inet_write(void *h, char *buf, int n);
int inet_recvfrom(void *h, char *buf, int n,
uint *addr, ushort *port);
int inet_sendto(void *h, char *buf, int n,
uint addr, ushort port);
int inet_readable(void *h);
int inet_hup(void *h);
void inet_close(void *h);
// proc.c
void exit(int status);
// Start a kernel thread: a process that runs `fn` in kernel mode forever. It
// has no files, no cwd and no parent, and must never return. Returns the pid.
int kthread_create(void (*fn)(void), const char *name);
int fork(void);
int growproc(int64);
int kill(int);
int sigsend_pgrp(int pgid, int sig);
int proc_pgid(int pid);
int proc_setpgid(int pid, int pgid);
int proc_setsid(void);
int sigsend(int pid, int sig);
int proc_exists(int);
int proc_num(void);
struct procinfo;
int proc_snapshot(int idx, struct procinfo *out);
int proc_snapshot_pid(int pid, struct procinfo *out);
void pinit(void);
void procdump(void);
void scheduler(void) __attribute__((noreturn));
void sched(void);
void sleep(void*, struct spinlock*);
int wait(void);
int waitpid(int, int*, int, uint64*, uint64*);
void wakeup(void*);
void yield(void);
int sched_slice_expired(void);
void sched_init_proc(struct proc*, int);
void sched_set_nice(struct proc*, int);
int proc_setnice(int, int);
int proc_getnice(int, int*);
void sinit(void);
void kstack_check_canary(const char *where);
// swtch.S
void swtch(struct context**, struct context*);
// spinlock.c
void acquire(struct spinlock*);
void getcallerpcs(void*, addr_t*);
void getstackpcs(addr_t*, addr_t*);
int holding(struct spinlock*);
void initlock(struct spinlock*, char*);
void release(struct spinlock*);
void pushcli(void);
void popcli(void);
// sleeplock.c
void acquiresleep(struct sleeplock*);
void releasesleep(struct sleeplock*);
int holdingsleep(struct sleeplock*);
void initsleeplock(struct sleeplock*, char*);
// string.c
int memcmp(const void*, const void*, uint64);
void* memmove(void*, const void*, uint64);
void* memset(void*, int, uint64);
char* safestrcpy(char*, const char*, int);
int strlen(const char*);
int strnlen(const char*, int);
int strcmp(const char*, const char*);
int strncmp(const char*, const char*, uint64);
char* strncpy(char*, const char*, int);
// syscall.c
void syscall(struct trapframe *);
void syscallinit(void);
void deliver_signals(struct trapframe *);
int argint(int, int*);
int argptr(int, char**, int);
int argstr(int, char**);
int argaddr(int, addr_t*);
int fetchaddr(addr_t, addr_t*);
int fetchstr(addr_t, char**);
int fetchint(addr_t, int*);
// trap.c
void idtinit(void);
extern uint ticks;
void tvinit(void);
extern struct spinlock tickslock;
// uart.c
void uartearlyinit(void);
void uartinit(void);
void uartintr(void);
void uartputc(int);
// Drawing, the text console and the framebuffer geometry are NOT declared
// here: they live in include/gfx/gfx.h, and the display drivers that back them in
// include/gfx/display.h. Declaring vga_fill_rect() here is what let the crash
// screen draw on one named driver.
// fb.c -- /dev/fb0, major 2.
void fbclose(struct inode*);
// Called from sys_mmap: a mapping of /dev/fb0 is a licence to draw with no
// further syscalls, so it is the last the kernel hears about it.
void fb_mapped(void);
// AudioEngine and /dev/dsp. kernel/dev/audio.c, include/dev/audio.h.
int dspread(struct inode*, uint64, char*, int);
int dspwrite(struct inode*, uint64, char*, int);
void dspclose(struct inode*);
int dsp_ioctl(int request, uint64 argp);
void audio_wait(struct device *);
// The crash screen, on displays other than the console's. kernel/dev/device.c.
void display_panic_present(void);
// Push the console's newly-drawn rows to a second display, if the kernel
// drives one. kernel/dev/device.c.
void display_mirror_console(void);
uint64 display_scanout_phys(void);
// Where a userspace display driver said it is scanning out, and which kernel
// driver it took the device from. kernel/dev/pciuser.c.
int pciuser_panic_surface(int idx, uint64 *paddr, int *w, int *h,
uint32 *stride,
const struct pci_driver **drv);
int fbread(struct inode*, uint64, char*, int);
int fbwrite(struct inode*, uint64, char*, int);
int fb_ioctl(int request, addr_t arg);
// dev/disk.c -- /dev/hdN, raw seekable access to a whole disk. The minor is the
// IDE device number. Writes bypass every filesystem above them.
int diskread(struct inode*, uint64, char*, int);
int diskwrite(struct inode*, uint64, char*, int);
// Network *drivers* are not declared here at all. They are reached through the
// netdev class in include/dev/netdev.h -- a driver publishes a vtable and nothing
// above it ever learns its name. Declaring e1000_transmit() here was what let
// the protocol stack call it directly.
// net/net.c -- the TCP/IP stack (lwIP). netinit starts the network thread;
// net_is_up reports whether an address has been obtained.
void netinit(void);
int net_is_up(void);
int net_is_ready(void);
void net_wake_counts(uint64 *rx, uint64 *tick);
int net_info(char *buf, int cap);
// net/lwip/ -- the port layer and the Ethernet interfaces, called only from
// net.c. One netif per bound network device; the index is that interface's.
void lwip_sys_init(void);
void lwip_sys_lock(void);
void lwip_sys_unlock(void);
int net_resolv(char *, int);
int net_eth_attach(struct device *);
int net_eth_detach(struct device *);
int net_detach(struct device *);
int net_attach_late(struct device *);
int net_eth_poll(void);
int net_eth_count(void);
void net_eth_counts(int i, uint64 *rx, uint64 *tx);
struct netif *net_eth_netif(int i);
uint netif_addr4(struct netif *);
uint netif_mask4(struct netif *);
uint ntohl_u32(uint);
uint htonl_u32(uint);
// rtnetlink: turn one request into a whole dump. See kernel/net/netlink.c.
int netlink_request(const char *req, uint reqlen,
char *out, uint outcap);
struct device *net_eth_device(int i);
// registry.c
// Platform devices, and the boot framebuffer. Takes the framebuffer address
// the loader handed over, because bringing that up is the first thing it does.
void platform_register_devices(uint64 fb_addr);
// drivers/gpu/vga -- the VBE framebuffer the loader programmed. Called only
// from registry.c; everything else draws through include/gfx/gfx.h.
void vga_fb_init(uint64 phys_addr);
uint64 vga_fb_phys(void);
int vga_fb_restore(void); // 0 = there is no fallback
uint64 vga_fb_size(void);
void e820_init(void);
int acpi_init(void);
// console.c / panic.c
void cons_panic_mode(void);
extern int cons_serial_only;
void console_takeover(void);
// vm.c
void seginit(void);
void kvmalloc(void);
// Map device registers. Returns a kernel address for `size` bytes of physical
// MMIO at `pa`, or 0 if the window is exhausted. Use this rather than P2V for
// anything that came out of a BAR: firmware may place a 64-bit BAR far above
// the 4 GB the direct map covers.
void* ioremap(uint64 pa, uint64 size);
pml4e_t* setupkvm(void);
char* uva2ka(pml4e_t*, char*);
addr_t allocuvm(pml4e_t*, uint64, uint64);
addr_t deallocuvm(pml4e_t*, uint64, uint64);
void freevm(pml4e_t*);
void inituvm(pml4e_t*, char*, uint64);
int loaduvm(pml4e_t*, char*, struct inode*, uint64, uint64);
pml4e_t* copyuvm(pml4e_t*, uint64);
void switchuvm(struct proc*);
void switchkvm(void);
int pipe_poll(struct pipe*, int readable, int writable);
int console_input_ready(int minor);
int copyin(pml4e_t*, addr_t, void*, uint64);
int copyout(pml4e_t*, addr_t, void*, uint64);
void vm_unmap_range(pml4e_t*, uint64, uint64);
// Unmap without freeing: for pages this process does not own — device memory
// and shared segments. vm_unmap_range() would hand them to kfree.
void vm_unmap_norelease(pml4e_t*, uint64, uint64);
void vm_walk_user(pml4e_t*, void (*)(void*, uint64, uint64, uint64), void*);
// Shared memory (sys_mem.c). shm_ref/shm_detach maintain the attachment
// count that decides when a segment's pages go back.
void shm_ref(int id);
char* shm_page(int id, int pg);
uint64 memfd_size(int id);
int memfd_truncate(int id, uint64 len);
int memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing);
void shm_detach(int id);
void proc_release_shared(struct proc *);
int copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int is_fb);
void clearpteu(pml4e_t *pgdir, char *uva);
#define NELEM(x) (sizeof(x)/sizeof((x)[0]))

73
include/core/dma.h Normal file
View file

@ -0,0 +1,73 @@
#pragma once
// Physically contiguous memory for devices that read and write it themselves.
//
// kalloc() hands out 4 KB pages from a free list, in whatever order they were
// released. That is exactly what a page table wants and exactly what a device
// cannot use: an xHCI command ring, a NIC's descriptor array or a virtio
// queue is a single physically contiguous object that the hardware walks
// without any idea that paging exists. Two kalloc'd pages are adjacent in the
// kernel's address space only by luck.
//
// So DMA memory comes from an arena carved out of the top of physical memory
// before kalloc ever sees it, and is handed out contiguous and aligned.
//
// On x86 this memory needs no special cacheability. The PCI bus is cache
// coherent: a device's write invalidates the corresponding cache line, and a
// CPU write is snooped out of the store buffer before the device sees stale
// data. Architectures without that guarantee need explicit cache maintenance
// around every descriptor access, and porting this file is where that would
// go. What is *not* free even here is ordering -- the compiler and the CPU
// will happily reorder stores to a ring, so a driver still needs a barrier
// between filling a descriptor and telling the device about it.
// 64 MB, and the arithmetic is worth writing down because it has been got
// wrong twice.
//
// 4 MB was one framebuffer at 1280x800, which was fine while exactly one driver
// wanted one. It stopped being fine the day a display driver could be handed to
// userspace (docs/plan/hybrid.md): either side of that handover **both drivers
// exist**, and the incoming one has to allocate its framebuffer before the
// outgoing one lets go of its own. So the real requirement is two framebuffers
// at once, not one.
//
// 16 MB was chosen for two at 1280x800 (4 MB each) and that is what it holds.
// It does not hold two of anything larger, and virtio-gpu's size is **the size
// of the host's window**, which the user resizes. A 1920x982 window is 7365 KB
// a copy, so the handover needs 14.4 MB of a 16 MB arena and then fails on the
// rings and the alignment:
//
// vgpud: wanted 7365 KB of DMA memory for a 1920x982 framebuffer
// vgpud: not enough contiguous DMA memory
// schinitd: restarting os.schos.vgpud (and again, and again)
//
// 64 MB is two framebuffers at 2560x1440 with room to spare, which covers any
// window somebody is likely to drag out on a desktop. It is carved off the top
// of physical memory at boot and never grows, so it is 64 MB this kernel cannot
// use for anything else -- 6% of the 1 GB this is usually run with, against a
// display driver that otherwise cannot start at all.
#define DMA_ARENA_BYTES (64*1024*1024)
void dma_init(void *vstart, void *vend);
// Zeroed, physically contiguous, at least `align`-aligned (align must be a
// power of two). Returns a kernel virtual address, or 0.
void *dma_alloc(uint64 size, uint64 align);
// `size` must be the size the allocation was made with, as in Linux's
// dma_free_coherent(). Tracking it here would mean a second table whose only
// job is to remember something the caller already knows.
void dma_free(void *p, uint64 size);
// The address to give the device. Kernel memory is direct-mapped, so this is
// just V2P -- but drivers should say what they mean, because on a machine
// with an IOMMU these stop being the same number.
addr_t dma_phys(const void *p);
uint64 dma_free_bytes(void);
// The biggest single block still available. When an allocation fails, the
// difference between this and dma_free_bytes() is the difference between a
// full arena and a fragmented one -- see the message in dma_alloc().
uint64 dma_largest_bytes(void);
uint64 dma_total_bytes(void);

35
include/core/elf.h Normal file
View file

@ -0,0 +1,35 @@
#define ELF_MAGIC 0x464C457FU
struct elfhdr {
uint32 magic;
uchar elf[12];
uint16 type;
uint16 machine;
uint32 version;
uint64 entry;
uint64 phoff;
uint64 shoff;
uint32 flags;
uint16 ehsize;
uint16 phentsize;
uint16 phnum;
uint16 shentsize;
uint16 shnum;
uint16 shstrndx;
};
struct proghdr {
uint32 type;
uint32 flags;
uint64 off;
uint64 vaddr;
uint64 paddr;
uint64 filesz;
uint64 memsz;
uint64 align;
};
#define ELF_PROG_LOAD 1
#define ELF_PROG_FLAG_EXEC 1
#define ELF_PROG_FLAG_WRITE 2
#define ELF_PROG_FLAG_READ 4

63
include/core/hwinfo.h Normal file
View file

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// What the firmware and the buses said about this machine, kept so /sys can
// say it again. kernel/fs/sysfs.c is the reader; each producer is named
// beside its part.
//
// Read-only views, copied out under whatever lock the producer has, so a
// caller never holds a pointer into a table that may change under it.
// --- the memory map, as the loader handed it over (kernel/arch/x86_64/e820.c)
struct e820_rec {
uint64 base, len;
uint32 type; // E820: 1 RAM, 2 reserved, 3 ACPI, 4 NVS, 5 bad
};
int e820_nth(int i, struct e820_rec *out); // 0, or -1 past the end
// The RAM the page allocator was given, in bytes: MemTotal and sysinfo's
// totalram. Not PHYSTOP, which is an address and counts the PCI hole.
uint64 ram_total_bytes(void);
// --- ACPI tables (kernel/arch/x86_64/acpi.c)
//
// Every table the RSDT or XSDT lists, then the DSDT and FACS, which only the
// FADT points at. `sig` gets four characters, no terminator. 0 for a table,
// 1 for an empty slot (keep counting), -1 past the end.
int acpi_table_nth(int i, uint64 *pa, uint32 *len, char sig[4]);
// --- SMBIOS (kernel/dev/dmi.c)
void dmi_init(void);
// The entry point structure and the table it describes, both physical.
int dmi_entry(uint64 *pa, uint32 *len);
int dmi_table(uint64 *pa, uint32 *len);
// Bytes of physical memory, through the direct map or ioremap. 0 on failure.
void *phys_view(uint64 pa, uint64 len);
// The strings Linux publishes in /sys/class/dmi/id, under its names. Serial
// numbers and the UUID are left out on purpose: nothing here needs them, and a
// hardware report is meant to be handed to somebody else.
enum {
DMI_BIOS_VENDOR, DMI_BIOS_VERSION, DMI_BIOS_DATE, DMI_BIOS_RELEASE,
DMI_SYS_VENDOR, DMI_PRODUCT_NAME, DMI_PRODUCT_VERSION, DMI_PRODUCT_SKU,
DMI_PRODUCT_FAMILY,
DMI_BOARD_VENDOR, DMI_BOARD_NAME, DMI_BOARD_VERSION,
DMI_CHASSIS_VENDOR, DMI_CHASSIS_TYPE, DMI_CHASSIS_VERSION,
DMI_NFIELDS
};
extern const char *const dmi_field_names[DMI_NFIELDS];
// Length written (no terminator counted), or -1 if the machine does not say.
int dmi_field(int f, char *out, int cap);
// 1 if the UEFI loader booted this kernel.
int boot_is_efi(void);
// --- USB devices (kernel/drivers/usb/core/usb.c), one per addressed device
struct usb_info {
int bus, devnum, speed; // speed: USB_SPEED_*
uint16 vid, pid, bcd;
uint8 cls, subcls, proto; // the device descriptor's
uint8 nconf;
char path[24]; // Linux's name: "1-3", "2-1.4"
char driver[16]; // the first interface's, or empty
};
int usb_info_nth(int i, struct usb_info *out);

70
include/core/irq.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
// Hardware interrupt registration.
//
// Until this existed, trap() was a switch statement listing every interrupt
// the kernel knew about, and adding a device meant editing the trap handler.
// That is fine while the devices are the four the PC has always had at fixed
// pins; it stops being fine the moment a device's IRQ is whatever the BIOS
// decided to put in its PCI config space this boot. A driver cannot write
// `case T_IRQ0 + IRQ_XHCI:` because there is no such constant -- it only
// learns its line at probe time.
//
// So: a line has a chain of handlers, a driver adds itself to one at probe
// time, and enabling the line is a consequence of registering rather than a
// separate call somebody can forget.
//
// Chains matter more than they look. PCI interrupt pins are *shared*: four
// INTx lines are routed to a handful of IOAPIC inputs, so two unrelated
// devices routinely land on the same one. A handler must therefore check
// whether its own device actually raised the interrupt and do nothing if it
// did not.
// Lines 0-23 are the IOAPIC's pins. 24-29 are MSI vectors, which are not pins
// at all -- a device writes the vector number to the local APIC directly --
// and are dispatched through the same table so that a handler cannot tell
// which kind it is on. 30 and 31 are the LAPIC's error and spurious vectors.
// Everything stops below vector 64, which is T_SYSCALL: a line there would
// let a user-mode `int $0x40` dispatch an interrupt and EOI one that is not.
#define NIRQ_IOAPIC 24
#define IRQ_MSI_FIRST 24
#define IRQ_MSI_LAST 29
#define NIRQ 31
#define NCHAIN 4 // handlers that may share one line
typedef void (*irq_handler_t)(void *arg);
// Returns 0, or -1 if the line is out of range, reserved, or full.
// Registering enables the line; the last unregister masks it again.
//
// `level_low` selects PCI's signalling (level-triggered, active low) rather
// than ISA's (edge-triggered, active high). Getting this wrong does not fail
// cleanly: an edge-configured level interrupt fires once and then wedges,
// because the device keeps the line asserted and no further edge ever comes.
//
// It is a mode, not a flag. 0 and 1 are the two above, which is all a PCI or
// ISA device needs; the other two are what ACPI can say about a line -- q35's
// PCI links and most machines' SCI are level-triggered *active high*, and
// programming one active low leaves the pin reading as permanently asserted.
#define IRQ_EDGE_HIGH 0 // ISA
#define IRQ_LEVEL_LOW 1 // PCI INTx
#define IRQ_LEVEL_HIGH 2
#define IRQ_EDGE_LOW 3
#define IRQ_IS_LEVEL(m) ((m) == IRQ_LEVEL_LOW || (m) == IRQ_LEVEL_HIGH)
int irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
int level_low);
void irq_unregister(int irq, irq_handler_t fn, void *arg);
int irq_handler_count(int irq);
// Called from trap() with the IOAPIC pin number, not the vector.
void irq_dispatch(int irq);
void irq_init(void);
// An MSI vector for pci_irq_attach(). -1 when all six are in use.
int irq_alloc_msi(void);
void irq_free_msi(int irq);
// The thread that polls PCI lines which never interrupt. After sinit().
void irq_poll_start(void);
int irq_info(char *buf, int cap); // /proc/interrupts

44
include/core/klog.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include "core/types.h"
// The kernel log that outlives the kernel.
//
// docs/architecture/debugging.md milestone D1. Until this existed, `cprintf` wrote to a
// serial line and a screen and was then gone: a panic's report could be read
// only by whoever was watching at the time, and a triple fault left nothing at
// all. panic.c said so in its own header comment -- *"there is no core dump and
// no log to go back to"* -- and that sentence is what this deletes.
//
// The buffer is a byte ring in a region carved off physical memory *before*
// kalloc or the DMA arena are given anything, at a fixed distance below
// PHYSTOP, so its address is a property of the machine rather than of this
// boot. Nothing allocates it, nothing frees it, and a reboot that leaves DRAM
// alone leaves it intact -- which is what makes the last words of a crashed
// kernel readable from the one that comes after.
#define KLOG_BYTES (128 * 1024) // ring plus header
#define KLOG_MAGIC 0x676f4c6bu // "kLog"
#define KLOG_VERSION 1
// Kept at the head of the region. Everything in it has to survive a reboot, so
// it is all fixed-width and there are no pointers: the only thing that says
// where the data is, is that it follows.
struct klog_hdr {
uint32 magic;
uint32 version;
uint32 size; // bytes of data[], = KLOG_BYTES - sizeof(struct klog_hdr)
uint32 boot; // how many kernels have written here
uint64 head; // total bytes ever written; the ring is head mod size
uint64 mark; // head as it stood when this boot started
uint32 sum; // over the fields above -- see klog.c
uint32 pad;
char data[];
};
void klog_init(void *base, uint64 bytes);
void klog_putc(int c);
int klog_read(uint64 off, char *dst, int n);
uint64 klog_len(void);
uint64 klog_prev_bytes(void);
int klog_recovered(void);

58
include/core/kmem.h Normal file
View file

@ -0,0 +1,58 @@
// Small-object allocation.
//
// `kalloc()` hands out one 4096-byte page and nothing smaller, so every
// subsystem written on top of it has had to invent a fixed-size table to avoid
// asking: NPROC, NSOCK, NDEVICE, NFILE, NINODE, NBUF. Each one is a compile-
// time ceiling that fails as a refusal ("no more sockets") rather than as
// memory pressure, each one is sized by guess, and each one costs its full
// size in BSS whether the machine ever uses it or not.
//
// This is the thing they were all working around. Two interfaces over it:
//
// kmem_cache_create() a named cache of one object size, for a subsystem
// with one kind of thing. Shows up in /proc/slab under
// its own name, which is most of why it is worth
// naming.
//
// kmalloc()/kmfree() the general case, for code that does not have a
// type to build a cache around. Backed by power-of-two
// caches; kmfree() finds the right one from the
// object's own page.
//
// **Everything is backed by kalloc().** A slab is exactly one page with a
// header at the front, so freeing needs no per-object header and no search:
// the page's base address holds the slab, and the slab knows its cache. That
// is the whole trick, and it is why an object may not be larger than a page.
#ifndef KMEM_H
#define KMEM_H
// The host harness (tools/kmemtest.c) defines its own uint/uint64 before it
// gets here, and types.h collides with glibc's -- same reason kernel/dev/ttf.c
// guards its includes.
#ifndef KMEM_HOST
#include "core/types.h"
#endif
struct kmem_cache;
struct sbuf;
// The largest object a cache can hold: a page, less the slab header. Asking
// for more returns 0 rather than silently falling back to kalloc(), because a
// caller that wants whole pages should say so.
uint kmem_max_obj(void);
// A named cache. `size` is rounded up to the alignment; the name is kept by
// pointer and must outlive the cache, which in practice means a literal.
struct kmem_cache *kmem_cache_create(const char *name, uint size);
void *kmem_cache_alloc(struct kmem_cache *c);
void kmem_cache_free(struct kmem_cache *c, void *obj);
// The general case. kmalloc(0) is a null return, not a zero-size object.
void *kmalloc(uint n);
void kmfree(void *p);
// /proc/slab.
void kmem_stats(struct sbuf *out);
#endif /* KMEM_H */

72
include/core/param.h Normal file
View file

@ -0,0 +1,72 @@
#pragma once
// The timer interrupt rate the kernel is built around. Everything that turns
// ticks into a duration divides by this. It used to be the literal 100 written
// out at each site, which made it look like a fact about the hardware; it is
// not. It is a target, and kernel/arch/x86_64/lapic.c calibrates the LAPIC
// timer against the PIT to actually hit it.
#define HZ 100 // timer interrupts per second
// The tick rate the *user-space ABI* is defined in, which is a different thing
// from HZ and must not follow it. glibc and musl both answer sysconf(_SC_CLK_TCK)
// with a hardcoded 100, so times() and /proc/stat are read as hundredths of a
// second no matter what the kernel's internal rate is. Linux calls this USER_HZ
// and scales at the syscall boundary; so does SchOS, in the two places below.
#define USER_HZ 100
// Converting a POSIX timespec/timeval into ticks. These used to appear as the
// bare literals 10000000 and 10000 at each call site, where they looked like
// magic numbers rather than what they are: the length of one tick, expressed
// in the units the caller happens to be using.
#define NSEC_PER_TICK (1000000000 / HZ)
#define USEC_PER_TICK (1000000 / HZ)
#define MSEC_PER_TICK (1000 / HZ)
#define NPROC 128 // maximum number of processes
#define KSTACKSIZE 8192 // per-process kernel stack; see kstacks[] in proc.c
#define KSTACK_CANARY 0xDEADC0DEDEADC0DEULL // sentinel at kstack[0], see kstack_check_canary()
#define NCPU 64 // maximum number of CPUs
#define NOFILE 128 // open files per process
#define NFILE 512 // open files per system
#define NINODE 200 // maximum number of active i-nodes
#define NDEV 13 // maximum major device number
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
// Device the root filesystem is mounted from. Building with RAMROOT=1 defines
// RAMDISK_ROOT and boots from the SCHFS image linked into the kernel instead
// of the IDE disk, leaving the physical disk free to be reformatted from a
// running system. See kernel/dev/ramdisk.c.
#ifdef RAMDISK_ROOT
#define ROOTDEV RAMDISK_DEV
#else
#define ROOTDEV 1 // device number of file system root disk
#endif
// exec's limits, Linux's: ARG_MAX bytes of argument and environment strings
// and their pointers together, and no one string over ARG_STRLEN_MAX (Linux's
// MAX_ARG_STRLEN). There is no limit on the count beyond that.
#define ARG_MAX (2 * 1024 * 1024)
#define ARG_STRLEN_MAX (128 * 1024)
#define MAXPATH 128 // max path
#define MAXOPBLOCKS 30 // max # of blocks any FS op writes
#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
// Disk block cache, in blocks. 256 * BSIZE = 1 MB.
//
// Was MAXOPBLOCKS*3 = 90 blocks, and at the old 1 KB BSIZE that is a 90 KB
// cache -- smaller than the toybox binary that every single command execs. So
// nothing was ever still resident on the next command and each one paid the
// full read again. 1 MB holds toybox with room to spare, which is what makes
// the *second* command fast rather than just the first.
//
// Sized rather than maximised: these are statically allocated in bio.c, so
// this is memory the system always spends. 1 MB is affordable on a machine
// with tens of megabytes; 8 MB would not be.
#define NBUF 256
#define FSSIZE 150000 // size of file system in blocks
// Network interfaces the stack will attach. Two is already an ordinary machine
// -- a laptop with Ethernet plus a virtual adapter -- and each one costs a
// struct netif, so the limit is about keeping a fixed array honest rather than
// about memory. Eight, since a dock's Ethernet and a phone tethered over USB
// sit beside the built-in ones.
#define NETIF_MAX 8

328
include/core/proc.h Normal file
View file

@ -0,0 +1,328 @@
#pragma once
#include "core/types.h"
#include "arch/traps.h"
#include "arch/ioperm.h"
// Simultaneous mmap() regions per process.
//
// 64 was enough for every first-party program and for toybox, and not enough
// for zsh: musl's allocator takes a fresh mapping per size class and does not
// release them eagerly, so compinit -- which reads twelve hundred completion
// functions -- ran out of slots and reported "fatal error: out of memory" on
// a machine with a gigabyte free. The slots were the limit, not the memory.
//
// The table is inline in struct proc, so this is NPROC * NVMA * sizeof(struct
// vma) of kernel memory, always resident. At 256 that is about 1 MB total,
// which is worth it to stop a shell dying on a machine that has 1000 MB spare.
#define NVMA 256
struct vma {
uint64 addr;
uint64 length;
int used;
// The pages under this mapping are not ours to free on unmap: either they
// are device memory (the framebuffer) or they belong to something with a
// longer life than this process (a shared memory segment). This was
// `is_fb`, which was the only such case until shared memory existed --
// and freeing another process's shared pages on exit is exactly the kind
// of bug that shows up somewhere else entirely, hours later.
int no_free;
int shmid; // shared segment this came from, or -1
};
// Per-CPU state
struct cpu {
uchar id;
uchar apicid; // Local APIC ID
struct context *scheduler; // swtch() here to enter scheduler
volatile uint64 started; // Has the CPU started?
int ncli; // Depth of pushcli nesting.
int intena; // Were interrupts enabled before pushcli?
void *local; // CPU-local storage (GS base)
};
extern struct cpu cpus[NCPU];
extern int ncpu; // processors present
extern int ncpu_online; // processors actually running
// --- per-CPU variables ------------------------------------------------------
//
// These were `__thread struct cpu *cpu` and `__thread struct proc *proc`, and
// that was wrong in a way that only appears once a second processor runs.
//
// `__thread` means *per thread*, and the compiler is entitled to assume a
// thread's pointer never changes: it reads %fs:0 once and keeps the value in a
// register or spilled on the stack across calls. clang did exactly that in
// sleep():
//
// movq %fs:0x0, %rax # read the thread pointer
// movq %rax, -0x18(%rbp) # spill it
// callq sched # <-- the CPU can change here
// movq -0x18(%rbp), %rax # reload the OLD pointer
// movq (%rax,%rcx), %rax # proc, from the previous CPU's block
// movq $0x0, 0x40(%rax) # NULL, because that CPU set proc = 0
//
// But %fs here is *per CPU*, not per thread, and a kernel thread changes CPU at
// every sched(). So the assumption is false for exactly the variables that
// most need to be right, and the failure is a null dereference deep in the
// scheduler on a machine with more than one core.
//
// The fix is to read the value at each use, with no cached base: an explicit
// %gs-relative access that the compiler cannot hoist across a call. `volatile`
// is what forbids that, and the memory clobber on the setters keeps stores in
// order with the rest of the scheduler's bookkeeping.
//
// Layout of the per-CPU block, whose base is in both FS_BASE and GS_BASE, set
// by seginit() in kernel/arch/x86_64/vm.c:
//
// +0 self-pointer required by the x86-64 TLS ABI (%fs:0)
// +8 syscall scratch trapasm.S parks %rsp here across the stack switch
// +16 struct cpu *
// +24 struct proc *
// +32 syscall rax the three registers wrmsr destroys, parked across
// +40 syscall rcx the FS_BASE swap at the top of syscall_entry
// +48 syscall rdx
//
// Offset 8 is NOT free. syscall_entry uses %fs:(8) as its scratch slot, and
// because seginit() sets GS base and FS base to the same address, %fs:8 and
// %gs:8 are the same memory. Putting `cpu` there made every syscall overwrite
// it with %rsp -- a #GP in syscall_entry itself, several layers below anything
// that could report it.
//
// **Offsets 32..48 were three globals**, `_sc_rax`/`_sc_rcx`/`_sc_rdx` in
// vm.c, under a comment saying they were safe because SYSCALL clears IF. IF
// answers for interrupts on *this* CPU and says nothing about another CPU
// executing the same three stores. Two CPUs entering a syscall together
// overwrote each other's parked registers, and %rcx is the user return address
// that sysretq uses -- so one process returned into another's instruction
// stream. See the note on syscall_entry in trapasm.S.
//
// trapasm.S reads all of these, so the offsets are shared with assembly and
// must not be reordered.
#define PERCPU_SELF 0
#define PERCPU_SCRATCH 8
#define PERCPU_CPU 16
#define PERCPU_PROC 24
#define PERCPU_SC_RAX 32
#define PERCPU_SC_RCX 40
#define PERCPU_SC_RDX 48
static inline struct cpu *
cpu_get(void)
{
struct cpu *c;
asm volatile("movq %%gs:16, %0" : "=r" (c));
return c;
}
static inline void
cpu_set(struct cpu *c)
{
asm volatile("movq %0, %%gs:16" :: "r" (c) : "memory");
}
static inline struct proc *
proc_get(void)
{
struct proc *p;
asm volatile("movq %%gs:24, %0" : "=r" (p));
return p;
}
static inline void
proc_set(struct proc *p)
{
asm volatile("movq %0, %%gs:24" :: "r" (p) : "memory");
}
// Named mycpu()/myproc(), as xv6-riscv does, and *not* spelled `cpu`/`proc`.
// A macro by those names would also rewrite every `struct cpu` and
// `struct proc`, because macros have no namespace and tags do -- which is
// exactly why the variables could carry those names in the first place.
#define mycpu() cpu_get()
#define myproc() proc_get()
// Saved registers for kernel context switches (x86_64 ABI)
struct context {
uint64 r15;
uint64 r14;
uint64 r13;
uint64 r12;
uint64 rbx;
uint64 rbp;
uint64 rip;
};
enum procstate { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
// A copy of the interesting fields of one process, taken under ptable.lock.
// procfs reads these rather than struct proc, so nothing outside proc.c needs
// the lock or the layout.
struct procinfo {
int pid, ppid, pgid, sid;
uint uid;
int state; // enum procstate
uint64 sz;
int killed;
int ctty; // controlling terminal, 1-based; 0 = none
uint64 utime, stime; // CPU time in ticks, this process
uint64 cutime, cstime; // and its reaped children
int nice; // scheduling priority, -20..19
uint64 vruntime; // weighted CPU nanoseconds, for `top` and debugging
uint64 nvcsw, nivcsw; // voluntary / involuntary context switches
char name[16];
char exe[64]; // full path, for /proc/<pid>/exe
};
struct proc {
uint64 sz; // Size of process memory (bytes) [+0x00]
pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08]
char *kstack; // Bottom of kernel stack [+0x10]
uint64 fs_base; // User TLS base (IA32_FS_BASE MSR) [+0x18] ← trapasm.S depends on this offset
enum procstate state; // Process state [+0x20]
int pid; // [+0x24]
struct proc *parent; // [+0x28]
struct trapframe *tf; // [+0x30]
struct context *context; // [+0x38]
void *chan; // [+0x40]
int killed; // [+0x48]
int _pad; // [+0x4c]
struct file *ofile[NOFILE]; // Open files [+0x50]
struct inode *cwd; // follows
char name[16]; //
// The path exec() was given. `name` is only the last component and only 15
// characters of it, which is what /proc/<pid>/stat wants; this is the whole
// path, reported by /proc/<pid>/exe.
//
// Note it is exposed as a regular *file*, not the symlink Linux uses, so
// `cat /proc/1/exe` works but readlink() does not. That is why `ps -o comm`
// still prints "[init]": toybox readlink()s exe and brackets the name when
// that fails. Default ps and top output is unaffected. Fixing it properly
// means symlink support in procfs, which is worth doing with the rest of the
// VFS work rather than on its own.
char exe[64];
// A kernel thread's entry point, or 0 for an ordinary process.
//
// Set by kthread_create and dispatched by forkret. A kernel thread has a
// page directory with only the kernel mapped and never returns to user mode,
// so it has no trapframe worth speaking of and no user stack -- it simply
// runs `kfn` forever inside the scheduler's ordinary rotation, which is what
// makes it able to sleep and to be preempted like anything else.
void (*kfn)(void);
// Ports this process may use directly, from the I/O BARs of the PCI devices
// it has claimed. Empty for everything that is not hosting a driver, which is
// everything. See include/arch/ioperm.h.
struct ioport_range ioport[NIOPORT];
int nioport;
// CPU time, in timer ticks, charged by trap() at each tick to whoever was
// running. utime/stime split on the interrupted privilege level -- cs & 3 --
// which is the only information the tick carries about what it interrupted.
//
// cutime/cstime accumulate *reaped* children, and only reaped ones: that is
// what RUSAGE_CHILDREN means, and it is why they are rolled up in wait()
// rather than in exit(). A child still running has not contributed yet.
uint64 utime, stime;
uint64 cutime, cstime;
uint64 signal_mask;
uint64 pending_signals;
int alarm_ticks;
char ofile_flags[NOFILE];
// Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's
// deliver_signals() and kernel/proc/proc.c's exit()). sig_handler[sig]/
// sig_restorer[sig] come straight from the user's struct sigaction
// (sys_rt_sigaction); 0 means no handler installed. saved_sigtf stashes
// the pre-signal trapframe so sys_rt_sigreturn() can restore it; in_signal
// prevents re-entering a handler while one is already running (we don't
// support nested signal delivery).
uint64 sig_handler[32];
uint64 sig_restorer[32];
struct trapframe saved_sigtf;
int in_signal;
// rt_sigsuspend() support: the pre-suspend mask to restore once the
// handler that woke us finishes (sigsuspend swaps the mask atomically
// for the duration of the wait -- POSIX). sigsusp_active is set while a
// restore is owed; sys_rt_sigreturn() performs it.
uint64 sigsusp_oldmask;
int sigsusp_active;
struct vma vma[NVMA]; // VMA tracking for mmap
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
// ABI translation (Linux compat syscall number map)
int *syscall_map;
int map_size;
// Current working directory path; maintained by sys_chdir / sys_fchdir.
char cwd_path[256];
// --- POSIX process identity -------------------------------------------
// Appended at the end of the struct deliberately: trapasm.S hard-codes the
// offsets of the fields at the top (see the _Static_assert on fs_base in
// kernel/proc/proc.c), so nothing may be inserted above them.
// Exit status, already encoded the way wait(2) reports it -- see
// W_EXITCODE() below. Read by waitpid() before the slot is recycled.
int xstate;
// Signal that killed us, if any. Set by sigsend() alongside ->killed so the
// eventual exit() can report *which* signal, instead of a blanket SIGKILL.
int killsig;
// Process group and session. Job control is defined entirely in terms of
// these: a shell puts each pipeline in its own process group and signals the
// group, not individual pids. Without them a shell's wait loop cannot work,
// which is why mksh's job control never made progress.
int pgid;
int sid;
// Controlling terminal, 1-based (1 = /dev/tty1), 0 = none. TIOCSCTTY sets
// it; /dev/tty resolves through it. Was impossible to have before there was
// more than one terminal to choose between.
int ctty;
// Never scheduled again. Set by sysrq 'w' to produce a hang on purpose --
// see proc_wedge() and docs/architecture/init.md, "The heartbeat".
int wedged;
// Real/effective/saved user and group ids. Nothing enforces permissions with
// them yet (the on-disk format has no owner field until SchFS), but they
// are tracked honestly so getuid()/setuid() stop lying to userland.
uint uid, euid, suid;
uint gid, egid, sgid;
// --- Fair-share scheduling --------------------------------------------
// The whole of the scheduler's per-process state. What each field means,
// and why the scheduler is shaped this way, is in kernel/proc/proc.c above
// scheduler(); this is deliberately only the declaration.
int nice; // -20..19, UNIX convention: lower means more CPU
uint32 weight; // nice_weight[nice + 20], cached
uint64 vruntime; // weighted CPU time consumed, in nanoseconds
uint64 ran_at; // TSC when this process last entered RUNNING
uint64 slice_ns; // how long the current slice is allowed to be
uint64 slice_used; // how much of it has been spent, in nanoseconds
uint64 nivcsw, nvcsw; // preempted / voluntary context switches
// The process's x87/SSE registers while it is off the CPU, as fxsave
// writes them; and a copy taken when a signal handler starts, put back by
// rt_sigreturn. See "The FPU" above scheduler() in kernel/proc/proc.c.
uint8 fpu[512] __attribute__((aligned(16)));
uint8 sigfpu[512] __attribute__((aligned(16)));
};
#define FD_CLOEXEC 1
// What a process's FPU starts as: x87 control word 0x37F, every exception
// masked, MXCSR 0x1F80 -- the state FNINIT and a reset leave, and what the
// System V ABI promises a new program. In proc.c.
extern const uint8 fpu_default[512];
// wait(2) status encoding, as understood by musl's WIFEXITED/WEXITSTATUS/
// WIFSIGNALED/WTERMSIG macros:
// normal exit -> (code & 0xff) << 8
// killed by signal -> sig & 0x7f
#define W_EXITCODE(code, sig) ((((code) & 0xff) << 8) | ((sig) & 0x7f))

46
include/core/sbuf.h Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include "core/types.h"
// A bounded string builder, because there is no snprintf in this kernel.
//
// These lived inside kernel/dev/procfs.c, where they were written, and were
// static to it. The debugger's introspection registry needs the same thing --
// a subsystem rendering its own state into a caller's buffer, with no
// allocation and no way to run off the end -- and the choice was to write a
// second set or to move this one. See docs/architecture/debugging.md, D2.
//
// Nothing here can overflow: every writer checks cap, and a full buffer
// silently drops the rest. That is the right trade for something whose callers
// are a /proc read and a crash-time dump; a builder that can fail is a builder
// every caller has to check, and none of them would.
struct sbuf {
char *p;
int len;
int cap;
};
void sb_putc(struct sbuf *sb, char c);
void sb_puts(struct sbuf *sb, const char *s);
// A string in a fixed-width column, space-padded. Over-long values run past
// their column rather than being truncated: a misaligned row is easier to read
// than a mangled name.
void sb_pad(struct sbuf *sb, const char *s, int width);
// Four lowercase hex digits, zero-padded. PCI and USB identifiers are always
// written this way, and a lookup table keyed on "8086:100e" does not match
// "8086:100E" or "8086:10e".
void sb_puthex4(struct sbuf *sb, uint32 v);
void sb_putu(struct sbuf *sb, uint64 v);
// Zero-padded hex of a given width, for addresses.
void sb_puthex(struct sbuf *sb, uint64 v, int digits);
// A decimal number in a right-aligned column, so tables of sizes line up.
void sb_putu_pad(struct sbuf *sb, uint64 v, int width);
// "<label><value> <unit>\n" -- the shape almost every /proc line takes.
void sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit);

9
include/core/sleeplock.h Normal file
View file

@ -0,0 +1,9 @@
// Long-term locks for processes
struct sleeplock {
uint locked; // Is the lock held?
struct spinlock lk; // spinlock protecting this sleep lock
// For debugging:
char *name; // Name of lock.
int pid; // Process holding lock
};

9
include/core/spinlock.h Normal file
View file

@ -0,0 +1,9 @@
// Mutual exclusion lock.
struct spinlock {
uint64 locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
addr_t pcs[10]; // The call stack (an array of program counters)
// that locked the lock.
};

49
include/core/syscall.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
// Syscall numbers and definition macros — all derived from
// kernel/sys/table.inc, which is the single source of truth.
//
// Do NOT add SYS_* constants by hand, and do NOT add anything to
// kernel/sys/linux_abi.c: add one SYSCALL() line to table.inc and write the
// body with SYSCALL_DEFINE(). The constant, the dispatch slot and the Linux
// ABI mapping are all generated from that one line.
#define NSYSCALLS 512
// Scratch slot for syscalls with no Linux equivalent. Every such entry lands
// on this one index of the translation map, which is never dispatched (Linux
// x86_64 numbers currently top out around 450). Using a real index rather
// than a negative sentinel keeps the generated map a plain designated
// initialiser instead of needing a second macro.
#define NO_LINUX 511
#ifdef __ASSEMBLER__
// In .S files GCC passes through the C preprocessor but the assembler
// sees the output, so emit .set directives instead of an enum.
#define SYSCALL(nr, lnr, name) .set SYS_##name, nr
#define SYSCALL_ALIAS(lnr, name)
#include "../../kernel/sys/table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
#else
// In C files generate a proper enum so constants work in switch/designators.
enum syscall_nr {
_SYS_RESERVED = 0,
#define SYSCALL(nr, lnr, name) SYS_##name = (nr),
#define SYSCALL_ALIAS(lnr, name)
#include "../../kernel/sys/table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
};
// Every syscall body is written as:
//
// SYSCALL_DEFINE(getpid)
// {
// return myproc()->pid;
// }
//
// so the signature lives in exactly one place and every implementation is
// findable with a single grep.
#define SYSCALL_DEFINE(name) addr_t sys_##name(void)
#endif

79
include/core/types.h Normal file
View file

@ -0,0 +1,79 @@
#pragma once
typedef unsigned int uint;
typedef unsigned short ushort;
typedef unsigned char uchar;
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef signed char int8;
typedef signed short int16;
typedef signed int int32;
// uint64 has to be 64 bits everywhere this header is used, and `unsigned long`
// is not that everywhere. Linux and the x86_64-elf kernel target are LP64, so
// long is 64-bit; Windows is LLP64, where long is 32-bit and only long long is
// 64-bit.
//
// This matters far more than a portability nicety. The host tools -- mkfs,
// mkschfs, the SchFS suites -- include this header *and* the on-disk structure
// definitions in fs.h and schfs.h. Building them on Windows with a 32-bit
// uint64 does not fail: it quietly produces images whose inode numbers, sizes
// and block pointers are half the width the kernel reads back, which shows up
// as a corrupt filesystem long after the tool that wrote it has exited.
//
// Spelled as long/long long rather than <stdint.h>'s uint64_t so the `%l`
// format specifiers used throughout the kernel stay correct and untouched.
// Conditioned on the actual width of `long` rather than on the platform,
// because there are three cases and only one of them is about Windows:
// x86_64-elf / Linux LP64 -> long is 64-bit
// Windows LLP64 -> long is 32-bit, long long is 64
// i386-elf (boot/) ILP32 -> long is 32-bit, and stage2main.c includes
// this header too
#if defined(__SIZEOF_LONG__) && __SIZEOF_LONG__ == 8
typedef unsigned long uint64;
typedef signed long int64;
#else
typedef unsigned long long uint64;
typedef signed long long int64;
#endif
// Belt and braces: if the above is ever wrong on some new host, say so at
// compile time rather than in a filesystem image.
_Static_assert(sizeof(uint64) == 8, "uint64 must be 64 bits");
_Static_assert(sizeof(int64) == 8, "int64 must be 64 bits");
typedef uint64 uintptr_t;
typedef int64 intptr_t;
typedef int64 ptrdiff_t;
typedef uint64 addr_t;
typedef uint64 size_t;
typedef int64 ssize_t;
#ifndef MKFS
#ifndef _SIGSET_T
#define _SIGSET_T
typedef uint64 sigset_t;
#endif
#endif
typedef addr_t pte_t;
typedef addr_t pde_t;
typedef addr_t pdpe_t;
typedef addr_t pml4e_t;
struct mouse_data {
int x;
int y;
uint8 buttons;
};
#ifndef NULL
#define NULL ((void*)0)
#endif
#define TRUE 1
#define FALSE 0

49
include/core/uaccess.h Normal file
View file

@ -0,0 +1,49 @@
// User-memory access API, modeled on Linux's <linux/uaccess.h>.
//
// Rules for kernel code touching user pointers:
//
// 1. Never dereference a user-supplied address without validating it
// first. A syscall argument can point anywhere, including at kernel
// memory or at nothing at all.
//
// 2. Never assume a user buffer is physically contiguous. uva2ka() only
// translates ONE page; the direct-map alias it returns runs into an
// unrelated physical page at the next 4K boundary. Any multi-byte
// access through it must stay within a single page.
//
// These helpers encapsulate both rules. Because SchOS (like old x86
// without SMAP) keeps the user page table active in kernel mode, a raw
// user virtual address is directly dereferenceable once the whole range
// has been validated — that is what user_ptr() hands back, and why the
// result is safe across page boundaries where a uva2ka() pointer is not.
#ifndef SCHOS_UACCESS_H
#define SCHOS_UACCESS_H
#include "core/types.h"
// Nonzero if [uaddr, uaddr+size) is entirely mapped, user-accessible
// memory in the current process. size == 0 is always ok.
int access_ok(addr_t uaddr, uint64 size);
// Validate the whole range and return it as a directly usable pointer
// (the raw user VA). 0 on bad/NULL address.
void *user_ptr(addr_t uaddr, uint64 size);
// Copy between kernel and user memory, page by page.
// Return 0 on success, -EFAULT on any unmapped page.
int copy_from_user(void *dst, addr_t uaddr, uint64 n);
int copy_to_user(addr_t uaddr, const void *src, uint64 n);
// Fixed-size scalar variants, in the spirit of Linux's get_user/put_user.
#define get_user(x, uaddr) copy_from_user(&(x), (addr_t)(uaddr), sizeof(x))
#define put_user(x, uaddr) copy_to_user((addr_t)(uaddr), &(x), sizeof(x))
// Longest string fetchstr()/strlen_user() will scan before giving up.
#define USER_STR_MAX (128 * 1024) // = ARG_STRLEN_MAX: an argument is one
// Length of the NUL-terminated string at uaddr (excluding the NUL),
// validating every page it spans. -EFAULT if it runs off mapped memory
// or exceeds USER_STR_MAX.
long strlen_user(addr_t uaddr);
#endif // SCHOS_UACCESS_H