ajhahn.de
← FlashOS
Flash 219 lines
// task_layout: canonical extern-struct layouts shared across kernel
// modules.
//
// Single source of truth. These structs were previously duplicated as
// per-file mirrors that drifted (mm_user / utilc / trace lacked the
// `parent` and `pid` fields) — the layout-drift bug the per-file
// `extern struct` discipline failed to prevent.
//
// Every kernel module that needs a layout `@import`s and aliases it,
// never redeclares. Defaults are set so `Foo{}` literals work;
// consumers override non-default fields at the construction site.
//
// The .S files (sched.S, entry.S, irq.S) consume these layouts via
// raw offsets. Reordering fields here requires auditing the asm side.

// Per-task slot budget for both `mm.user_pages` (mapped UVA pages) and
// `mm.kernel_pages` (PGD/PUD/PMD/PTE tables). 16 was tight enough that
// the brk test (1 inherited UVA-0 page + 16 heap pages = 17) overflowed
// user_pages on the 17th map_page call. 32 leaves headroom for future
// tests without inflating TaskStruct beyond the 4 KiB kernel-stack page
// (TaskStruct ≈ CoreContext + scalars + MmStruct + parent + pid +
// wq_next + fd_table + open_files = ~104 + 40 + (8 + 32*16 + 32*8 + 8)
// + 8 + 4 + 8 + 8*8 + 8*8 + cwd 256 + creds 16 = ~1328 bytes; KeRegs
// sits in the top 272 bytes of the same page, leaving ~2.4 KiB of stack
// — still ample).
// The user_space/kernel_tests.zig brk test holds the canary comptime
// assert that catches NUM_BRK_PAGES overflowing this budget.
pub const MAX_PAGE_COUNT usize = 32

// Per-task fd-table slot count. Covers pipes, files, and console slots
// in the unified tagged-pointer fd-table.
pub const FD_TABLE_SIZE usize = 8

// Per-task working-directory byte budget. Fixed-size,
// rule-1 (no heap allocator). 256 bytes — sys_chdir's copy-from-user
// loop and the syscall-boundary relative-path join (src/path.zig)
// both honour this ceiling. NUL-terminated C-string layout; the
// active span is `std.mem.sliceTo(&task.cwd, 0)`.
pub const CWD_SIZE usize = 256

// Process state values (mirrored from sched.zig consumers).
pub const TASK_RUNNING i64 = 0
pub const TASK_ZOMBIE i64 = 1
pub const TASK_INTERRUPTIBLE i64 = 2

// Task `flags` bits.
pub const KTHREAD u64 = 1
pub const UTHREAD u64 = 0

pub const CoreContext = extern struct {
    x19 u64 = 0,
    x20 u64 = 0,
    x21 u64 = 0,
    x22 u64 = 0,
    x23 u64 = 0,
    x24 u64 = 0,
    x25 u64 = 0,
    x26 u64 = 0,
    x27 u64 = 0,
    x28 u64 = 0,
    fp u64 = 0,
    sp u64 = 0,
    lr u64 = 0,
}

pub const UserPage = extern struct {
    pa u64 = 0,
    uva u64 = 0,
    flags u64 = 0,
}

pub const MmStruct = extern struct {
    pgd u64 = 0,
    user_pages [MAX_PAGE_COUNT]UserPage = [_]UserPage{.{}} ** MAX_PAGE_COUNT,
    kernel_pages [MAX_PAGE_COUNT]u64 = [_]u64{0} ** MAX_PAGE_COUNT,
    // Heap break — top of the demand-allocated heap region. Initial
    // value (HEAP_BASE) is set by prepare_move_to_user_elf so an
    // empty heap is the legal `addr == brk` no-op state. Mutated by
    // sys_brk / sys_sbrk; read by the region-aware do_data_abort
    // dispatch. Field appended last so the .S consumers
    // that key off CoreContext (offset 0) stay byte-identical.
    brk u64 = 0,
}

pub const TaskStruct = extern struct {
    core_context CoreContext = .{},
    state i64 = 0,
    counter i64 = 0,
    priority i64 = 1,
    preempt_count i64 = 0,
    flags u64 = 0,
    mm MmStruct = .{},
    // Parent pointer for sys_wait. init_task has no parent.
    // Appended last (after mm) so sched.S — which only reads
    // core_context at offset 0 — is unaffected.
    parent ?*mut TaskStruct = null,
    // Monotonic pid, decoupled from the task[] slot index now that
    // do_wait frees slots and copy_process reuses them. Required so
    // sys_kill(pid) can't race a reap+reuse and target the wrong
    // process.
    pid i32 = 0,
    // Singly-linked-list pointer for WaitQueue chains. Null = not on
    // any queue. Per-task because a task can only be on one queue at
    // a time (mirrors Linux's task.wq_node). Appended after `pid` so
    // the .S consumers that key off CoreContext (offset 0) stay
    // byte-identical.
    wq_next ?*mut TaskStruct = null,
    // Fd table slots. Unified tagged-pointer fd-table covering pipes,
    // files, and console. Null slots are indicated by kind=0 (none).
    fds [FD_TABLE_SIZE]FdSlot = [_]FdSlot{.{}} ** FD_TABLE_SIZE,
    // Per-task working directory. NUL-terminated,
    // C-string layout; sys_chdir + the syscall-boundary relative-path
    // join (src/path.zig) read/write it. Defaults to "/" so init_task
    // (declared as `TaskStruct.{}` in sched.zig) and forked children
    // come up with a sane root. Appended last — after `fds` — so the
    // raw-offset .S consumers (sched.S/entry.S/irq.S key off
    // CoreContext at offset 0) stay byte-identical.
    cwd [CWD_SIZE]u8 = blk: {
        var c [CWD_SIZE]u8 = .{0} ** CWD_SIZE
        c[0] = '/'
        break :blk c
    },
    // Process credentials. Real + effective uid/gid for the
    // login/auth flow: sys_get/setuid/gid read and mutate these,
    // copy_process_impl copies them parent→child, and execve preserves
    // them (the same TaskStruct survives the image swap) so a privilege
    // drop in /bin/login carries into the shell it execs. Default 0 =
    // root — correct for init_task (declared as `TaskStruct{}` in
    // sched.zig) and overwritten for forked children. Appended last —
    // after `cwd` — so the raw-offset .S consumers (sched.S/entry.S/irq.S
    // key off CoreContext at offset 0) stay byte-identical.
    uid u32 = 0,
    gid u32 = 0,
    euid u32 = 0,
    egid u32 = 0,
    // Kernel-stack page base. The per-task kernel stack lives in
    // its OWN page, decoupled from this TaskStruct page, so a deep syscall
    // plus a nested timer-IRQ register save can never descend out of the
    // stack into the credential tail that sits just above it. 0 = no
    // separate page: init_task / the boot context run on the boot stack.
    // Appended last — after the creds — so the raw-offset .S consumers
    // (sched.S/entry.S/irq.S key off CoreContext at offset 0) stay
    // byte-identical.
    kstack u64 = 0,
}

pub const FdSlot = extern struct {
    ptr ?*mut anyopaque = null, // *Pipe | *File | null (console)
    kind u8 = 0,            // Kind; `none` == free slot
    _pad [7]u8 = .{0} ** 7,
}

// Open-file handle. Layout-only declaration; the lifetime helpers
// (alloc / unref / ref) and the FType tag enum live in src/file.zig
// (the fd-table proper lives in src/fdtable.zig). Defined here so TaskStruct
// can carry a typed `?*File` slot without a circular import on
// file.zig (file.zig imports task_layout for TaskStruct + File).
//
// `ftype = 0` is INITRAMFS_FILE — the only ftype populated today;
// FType in file.zig owns the tag→backend binding for future slots
// (FAT32, unified pipe).
pub const File = extern struct {
    ftype u8 = 0,
    _pad [3]u8 = .{ 0, 0, 0 },
    refs u32 = 0,
    offset u64 = 0,
    // INITRAMFS_FILE: kernel-VA pointer to the entry's data bytes
    // (already TTBR1-mapped via the .initramfs section).
    private u64 = 0,
    // Cached file size; saves re-walking the cpio on every read.
    size u64 = 0,
    // Backing superblock for VFS vtable dispatch.
    // `?*anyopaque`, not `?*vfs.SuperBlock`, to break the import
    // cycle: vfs.zig imports file/task_layout for the File type, so
    // task_layout.zig must not import vfs. sys.zig @ptrCast's this
    // back to *vfs.SuperBlock at the read/seek/close dispatch sites —
    // the same opaque-pointer pattern as the `fd_table` slot above.
    sb ?*mut anyopaque = null,
    // Permission metadata copied from OpenResult at open time.
    // Carried on the File so the per-write check in sys_write has the
    // owning ids + mode without a fresh VFS lookup. Defaults 0 =
    // root-owned / no permission bits (safe-deny for non-root callers).
    // Appended last; File is never referenced by raw offset from the
    // .S files (only TaskStruct is), so the append is layout-safe.
    mode u32 = 0,
    uid u32 = 0,
    gid u32 = 0,
    // On-disk directory-entry location, copied from OpenResult at open.
    // FAT32 write() uses it to rewrite the entry's first-cluster (when a
    // previously empty file gets its first data cluster) and file_size,
    // without an ambiguous re-walk by first cluster (0 is not unique
    // across empty files). dirent_off is the byte offset within the
    // dirent_lba sector. 0 = unset. Appended last; File is never
    // referenced by raw offset from the .S files, so the append is
    // layout-safe.
    dirent_lba u32 = 0,
    dirent_off u32 = 0,
}

pub const KeRegs = extern struct {
    regs [31]u64 = [_]u64{0} ** 31,
    sp u64 = 0,
    elr u64 = 0,
    pstate u64 = 0,
}

// ABI seam — the EL1 exception stub (arch/aarch64/entry.S) reserves exactly
// S_FRAME_SIZE bytes (arch/aarch64/asm_defs_common.inc) for the saved-register
// frame, and fork/context-switch resolve KeRegs at
// THREAD_SIZE - @sizeOf(KeRegs). The asm reservation and this struct MUST
// agree byte-for-byte; a field reorder or addition here would silently
// corrupt every context switch. Zig can't read the C #define, so pin the
// size and fail the build the moment it drifts.
comptime {
    if #sizeOf(KeRegs) != 272 {
        #compileError("KeRegs size changed — update S_FRAME_SIZE in arch/aarch64/asm_defs_common.inc to match")
    }
}