`∞`, `→`, `≤` and `'` could not be typed. `ą` and `ę` could. That pairing is
the whole diagnosis: KBD_UP was 0xE2 and its neighbours filled 0xE0..0xE9,
which are exactly the UTF-8 lead bytes for three-byte characters. Every
character in U+0800..U+FFFF -- most punctuation, every arrow, every maths
symbol, the whole of CJK -- arrived as a value the console read as an arrow key
and helpfully turned into `ESC [ A`. Two-byte sequences lead with 0xC4 and
nothing claimed 0xC4, so Polish came through and the rest did not.
The chord codes already knew this rule and said so in a comment. The navigation
keys predate them and were never brought in line. Stated once, in kbd.h: a key
produces either a Unicode codepoint or a value above 0x10FFFF, and nothing in
between means anything. KEY_* -> KBD_*, which also settles a silent collision
-- include/input.h defines its own KEY_HOME, KEY_UP and KEY_END as evdev
numbers, and kbd.c includes both headers, so three macros were being quietly
redefined in the one file that used them.
Then the layouts, which is what the codepoint-sized keycodes were for.
kernel/dev/keymap.c: us, pl, de. A layout is four tables -- plain, Shift,
AltGr, Shift+AltGr -- of uint32 codepoints. uint32 and not uchar is the reason
the file exists: `ó` fits in a byte and `ą` does not, and half a Polish
alphabet is not a Polish layout.
Two things deliberately are *not* tables:
- **Ctrl is derived from the plain codepoint.** The table it replaces was
indexed by scancode, which quietly assumed US -- on QWERTZ the Y and Z keys
swap, so Ctrl+Z would have suspended a job only on some layouts.
- **Caps Lock is a case toggle on the result**, so AltGr+a with Caps Lock is
`Ą`. No second table could say that, because Caps Lock combines with AltGr
rather than replacing it. Latin Extended-A alternates upper/lower by
codepoint but not all the same way round -- Ł is U+0141 (odd) and Ą is
U+0104 (even) -- so Polish lands in both halves and the parity matters.
kbdgetc() encodes to UTF-8 and returns the bytes one call at a time.
consoleintr()'s contract is bytes: the serial line hands it UTF-8 already and
it could not tell an encoded codepoint from a byte of the same value.
The layouts live in the kernel. Linux's loadkeys compiles a text keymap and
uploads it a key at a time through KDSKBENT, which puts the data in userspace
-- and a machine whose /usr has not mounted then has no layout at all. Here the
kernel always has a working keyboard and `loadkeys` only names which one.
KB_GETLAYOUT/KB_SETLAYOUT/KB_LISTLAYOUT; /etc/vconsole.conf and an
os.schos.keymap job for the boot-time choice. No dead keys, so French and
Spanish are absent rather than wrong.
/etc/locale.conf and LANG=C.UTF-8 in profile and zshenv. Only the charset half
of that is real -- musl decodes UTF-8 whatever the locale says and ships no
locale data -- but an unset LANG reads as "ASCII only" to everything that asks,
which is its own way of producing `?`.
And two symlink bugs found while chasing
zsh: locking failed for /root/.zsh_history: operation not permitted
on every boot. symlink() is the classic atomic lock -- create the link, and if
it exists someone else holds it -- and zsh distinguishes EEXIST ("contended,
retry") from anything else ("this cannot work, give up"). sys_symlink returned
a bare -1, which is -EPERM, so every zsh on the machine took the second branch
on a filesystem that supports symlinks perfectly well. Same errno class as
commit 75ccf98.
The second one the first was hiding: **divesymlink() has been in fs.c since the
fork and nothing ever called it.** A symlink behaved as a small file containing
its own target, so `cat link` printed the target's *name*. open() and openat()
follow the final component now (O_NOFOLLOW gives ELOOP), and stat() and lstat()
stop being the same function, since which one follows is the only difference
between them. Mid-path symlinks are still not resolved -- that belongs in
namex(), which would re-enter lookup holding a directory lock.
Verified on the emulated keyboard, not just over serial: AltGr+a/e/s gives
ąęś, Shift+AltGr gives ĄĘŚ, Caps Lock+AltGr gives ĄĘŚ, de types zy for the
physical y/z keys and @€µ on AltGr. `∞ 'x' → ≤ • — …` typed over serial now
survives. Three fresh boots: 0 lock failures, 0 faults.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
415 lines
12 KiB
C
415 lines
12 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
// SchOS — POSIX-compatibility types used by kernel syscall handlers.
|
|
#pragma once
|
|
#include "types.h"
|
|
#include "date.h"
|
|
|
|
// struct iovec is declared in two headers on purpose, and guarded so that
|
|
// including both is not an error.
|
|
//
|
|
// posix.h has it because readv/writev need it; this file has it because a
|
|
// userspace program including <socket.h> for sendmsg must get a complete
|
|
// struct msghdr without also having to know about a kernel header. Both
|
|
// definitions are identical and both are Linux's layout.
|
|
//
|
|
// The guard is not cosmetic. C23 permits redeclaring a struct with identical
|
|
// members and GCC 15 defaults to C23, so the duplicate was invisible on the
|
|
// gcc path -- while clang defaults to gnu17 and rejected it, which broke the
|
|
// one toolchain shell.nix exists to provide and which the UEFI loader needs.
|
|
#ifndef SCH_IOVEC_DEFINED
|
|
#define SCH_IOVEC_DEFINED
|
|
struct iovec {
|
|
void *iov_base;
|
|
uint64 iov_len;
|
|
};
|
|
#endif
|
|
|
|
// Linux's IOV_MAX. readv/writev used to cap iovcnt at 16 -- which is legal,
|
|
// since POSIX only requires 16 -- and returned EINVAL above it. But programs
|
|
// are written against the platform they run on, and on Linux that limit is
|
|
// 1024: toybox's `yes` builds a 256-entry iovec and writev()s it in a loop,
|
|
// so `yes | head -2` failed with "Invalid argument" before producing a single
|
|
// byte. Nothing here costs per-entry memory; the count is just a loop bound.
|
|
#define UIO_MAXIOV 1024
|
|
|
|
// Simplified termios (matches Linux x86_64 layout enough for basic use).
|
|
struct termios {
|
|
uint32 c_iflag;
|
|
uint32 c_oflag;
|
|
uint32 c_cflag;
|
|
uint32 c_lflag;
|
|
uint8 c_line;
|
|
uint8 c_cc[19];
|
|
uint32 c_ispeed;
|
|
uint32 c_ospeed;
|
|
};
|
|
|
|
// Terminal flags, with Linux's values. The kernel honours the ones marked
|
|
// below; the rest are accepted and stored so a program that sets them and
|
|
// reads them back sees what it wrote, which is what cfmakeraw() and every
|
|
// line-editing library actually check.
|
|
//
|
|
// c_iflag
|
|
#define IGNBRK 0000001
|
|
#define BRKINT 0000002
|
|
#define IGNPAR 0000004
|
|
#define PARMRK 0000010
|
|
#define INPCK 0000020
|
|
#define ISTRIP 0000040 // honoured: strip the eighth bit
|
|
#define INLCR 0000100 // honoured: NL -> CR on input
|
|
#define IGNCR 0000200 // honoured: drop CR
|
|
#define ICRNL 0000400 // honoured: CR -> NL on input
|
|
#define IUCLC 0001000
|
|
#define IXON 0002000
|
|
#define IXANY 0004000
|
|
#define IXOFF 0010000
|
|
#define IMAXBEL 0020000
|
|
#define IUTF8 0040000
|
|
|
|
// c_oflag
|
|
#define OPOST 0000001 // honoured: enable output processing
|
|
#define OLCUC 0000002
|
|
#define ONLCR 0000004 // honoured: NL -> CR NL on output
|
|
#define OCRNL 0000010
|
|
#define ONOCR 0000020
|
|
#define ONLRET 0000040
|
|
|
|
// c_cflag
|
|
#define CS8 0000060
|
|
#define CREAD 0000200
|
|
#define HUPCL 0002000
|
|
#define CLOCAL 0004000
|
|
|
|
// c_lflag
|
|
#define ISIG 0000001 // honoured: INTR/QUIT/SUSP generate signals
|
|
#define ICANON 0000002 // honoured: line-at-a-time with editing
|
|
#define ECHO 0000010 // honoured
|
|
#define ECHOE 0000020 // honoured: ERASE rubs the character out
|
|
#define ECHOK 0000040
|
|
#define ECHONL 0000100 // honoured: echo NL even with ECHO off
|
|
#define NOFLSH 0000200
|
|
#define TOSTOP 0000400
|
|
#define IEXTEN 0100000
|
|
|
|
// c_cc indices (Linux order). VMIN/VTIME share slots with VEOF/VEOL, which is
|
|
// why they are only meaningful in the mode the other pair is not.
|
|
#define VINTR 0
|
|
#define VQUIT 1
|
|
#define VERASE 2
|
|
#define VKILL 3
|
|
#define VEOF 4
|
|
#define VTIME 5
|
|
#define VMIN 6
|
|
#define VSWTC 7
|
|
#define VSTART 8
|
|
#define VSTOP 9
|
|
#define VSUSP 10
|
|
#define VEOL 11
|
|
#define VREPRINT 12
|
|
#define VDISCARD 13
|
|
#define VWERASE 14
|
|
#define VLNEXT 15
|
|
#define VEOL2 16
|
|
|
|
struct winsize {
|
|
uint16 ws_row;
|
|
uint16 ws_col;
|
|
uint16 ws_xpixel;
|
|
uint16 ws_ypixel;
|
|
};
|
|
|
|
struct rlimit {
|
|
uint64 rlim_cur;
|
|
uint64 rlim_max;
|
|
};
|
|
|
|
// Linux ioctl codes we handle.
|
|
#define TCGETS 0x5401
|
|
#define TCSETS 0x5402
|
|
#define TCSETSW 0x5403
|
|
#define TCSETSF 0x5404
|
|
#define TCFLSH 0x540B
|
|
// TCFLSH queue selectors. Not a pointer -- these are the argument itself.
|
|
#define TCIFLUSH 0
|
|
#define TCOFLUSH 1
|
|
#define TCIOFLUSH 2
|
|
#define TIOCGPGRP 0x540F
|
|
#define TIOCSPGRP 0x5410
|
|
#define TIOCGWINSZ 0x5413
|
|
#define TIOCSWINSZ 0x5414
|
|
#define TIOCSCTTY 0x540E
|
|
|
|
// Virtual terminal control, Linux's numbers. The argument to VT_ACTIVATE and
|
|
// VT_WAITACTIVE is a 1-based terminal number passed by value, not a pointer.
|
|
#define VT_OPENQRY 0x5600 // first free terminal, out through a pointer
|
|
#define VT_GETMODE 0x5601
|
|
#define VT_SETMODE 0x5602
|
|
#define VT_GETSTATE 0x5603 // struct vt_stat
|
|
#define VT_ACTIVATE 0x5606
|
|
#define VT_WAITACTIVE 0x5607
|
|
|
|
// VT_SETMODE modes. VT_AUTO is the kernel switching on its own; VT_PROCESS
|
|
// means a program has taken switching over and the kernel must keep its hands
|
|
// off the keyboard.
|
|
#define VT_AUTO 0x00
|
|
#define VT_PROCESS 0x01
|
|
#define VT_ACKACQ 0x02
|
|
|
|
// VT_SETCHORD / VT_GETCHORD -- which key combination the kernel switches on.
|
|
// SchOS's own, not Linux's: Linux hardcodes Ctrl+Alt+F<n> and nobody has ever
|
|
// needed to move it, because Linux is the host. A guest does need to move it,
|
|
// because the right answer depends entirely on what the host grabs first --
|
|
// and that is not something the guest can discover.
|
|
#define VT_GETCHORD 0x5620
|
|
#define VT_SETCHORD 0x5621
|
|
|
|
// KB_GETLAYOUT / KB_SETLAYOUT / KB_LISTLAYOUT -- which keyboard layout the
|
|
// console uses. SchOS's own, and deliberately not Linux's KDGKBENT: that
|
|
// interface hands over one key of one table at a time and expects userspace to
|
|
// own the whole map, which means a `loadkeys` that ships its own data files and
|
|
// a boot that is broken until one is loaded. The layouts live in the kernel
|
|
// here, and userspace names the one it wants.
|
|
#define KB_GETLAYOUT 0x5622 // fill in the active layout
|
|
#define KB_SETLAYOUT 0x5623 // switch to the layout named in `name`
|
|
#define KB_LISTLAYOUT 0x5624 // describe layout `index`; -EINVAL past end
|
|
|
|
struct kb_layout {
|
|
uint16 index; // in for KB_LISTLAYOUT, out for KB_GETLAYOUT
|
|
char name[16]; // "us", "pl", "de"
|
|
char desc[96]; // human-readable, for `loadkeys -l`
|
|
};
|
|
|
|
// Modifier bits, matched exactly. These mirror kernel/dev/kbd.h's values so
|
|
// that userspace and the keyboard driver cannot drift apart.
|
|
#define VTC_SHIFT 0x01 // kbd.h SHIFT (1<<0)
|
|
#define VTC_CTRL 0x02 // kbd.h CTL (1<<1)
|
|
#define VTC_ALT 0x04 // kbd.h ALT (1<<2)
|
|
#define VTC_RALT 0x80 // kbd.h RALT (1<<7) -- NOT (1<<3); that is
|
|
// CAPSLOCK, and bits 3..6 are the other locks
|
|
// and the E0 prefix flag
|
|
|
|
// Well-known base scancodes for the `base` field below.
|
|
#define VTC_BASE_F1 0x3B // F1..F6
|
|
#define VTC_BASE_1 0x02 // the number row, 1..6
|
|
|
|
struct vt_chord {
|
|
uint16 mods; // exactly these modifiers held, and no others
|
|
uint16 base; // scancode of the key meaning terminal 1
|
|
};
|
|
|
|
struct vt_mode {
|
|
char mode; // VT_AUTO or VT_PROCESS
|
|
char waitv; // unused here
|
|
short relsig; // signal on release -- not delivered yet
|
|
short acqsig; // signal on acquire -- not delivered yet
|
|
short frsig; // unused
|
|
};
|
|
|
|
struct vt_stat {
|
|
uint16 v_active; // active terminal, 1-based
|
|
uint16 v_signal; // unused; Linux keeps it for VT_SETMODE
|
|
uint16 v_state; // bitmask of terminals in use, bit 1 = tty1
|
|
};
|
|
#define TIOCGSERIAL 0x541E
|
|
#define FIONREAD 0x541B
|
|
// FICLONE: share another file's data blocks with this one. The argument is a
|
|
// file descriptor, not a pointer. Linux's value, so `cp --reflink` works.
|
|
#define FICLONE 0x40049409
|
|
#define TIOCEXCL 0x540C
|
|
#define TIOCNXCL 0x540D
|
|
|
|
// arch_prctl commands
|
|
#define ARCH_SET_GS 0x1001
|
|
#define ARCH_SET_FS 0x1002
|
|
#define ARCH_GET_FS 0x1003
|
|
#define ARCH_GET_GS 0x1004
|
|
|
|
// clone flags (subset)
|
|
#define CLONE_VM 0x00000100
|
|
#define CLONE_THREAD 0x00010000
|
|
#define CLONE_SIGHAND 0x00000800
|
|
|
|
// futex ops
|
|
#define FUTEX_WAIT 0
|
|
#define FUTEX_WAKE 1
|
|
#define FUTEX_WAIT_PRIVATE 128
|
|
#define FUTEX_WAKE_PRIVATE 129
|
|
|
|
// Resource limit codes (POSIX)
|
|
#define RLIMIT_AS 9
|
|
#define RLIMIT_STACK 3
|
|
#define RLIMIT_NOFILE 7
|
|
|
|
// clock IDs
|
|
#define CLOCK_REALTIME 0
|
|
#define CLOCK_MONOTONIC 1
|
|
|
|
struct timespec {
|
|
long tv_sec;
|
|
long tv_nsec;
|
|
};
|
|
|
|
struct itimerval {
|
|
struct timeval it_interval;
|
|
struct timeval it_value;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
struct tms {
|
|
long tms_utime;
|
|
long tms_stime;
|
|
long tms_cutime;
|
|
long tms_cstime;
|
|
};
|
|
|
|
struct sysinfo {
|
|
long uptime;
|
|
uint64 loads[3];
|
|
uint64 totalram;
|
|
uint64 freeram;
|
|
uint64 sharedram;
|
|
uint64 bufferram;
|
|
uint64 totalswap;
|
|
uint64 freeswap;
|
|
uint16 procs;
|
|
uint16 pad;
|
|
uint32 pad2;
|
|
uint64 totalhigh;
|
|
uint64 freehigh;
|
|
uint32 mem_unit;
|
|
char _f[8];
|
|
};
|
|
|
|
// Must match Linux's x86-64 struct statfs field for field, because the programs
|
|
// reading it were compiled against Linux's.
|
|
//
|
|
// f_fsid is the trap. Linux's fsid_t is `struct { int val[2]; }` -- eight bytes
|
|
// in total, not two eight-byte words -- and declaring it as int64[2] made this
|
|
// struct one slot too long from f_fsid onwards. Everything after it landed in
|
|
// the wrong place: userland read f_frsize out of f_namelen and got 255, so df
|
|
// computed its block counts in units of 255 bytes and reported a 32 MB root
|
|
// filesystem as 1992 KB. It went unnoticed for as long as it did because every
|
|
// field here used to be an invented constant, so the numbers were meaningless
|
|
// whichever slot they came from.
|
|
struct statfs {
|
|
int64 f_type;
|
|
int64 f_bsize;
|
|
int64 f_blocks;
|
|
int64 f_bfree;
|
|
int64 f_bavail;
|
|
int64 f_files;
|
|
int64 f_ffree;
|
|
int32 f_fsid[2];
|
|
int64 f_namelen;
|
|
int64 f_frsize;
|
|
int64 f_flags;
|
|
int64 f_spare[4];
|
|
};
|
|
|
|
// at-syscall dirfd sentinel
|
|
#define AT_FDCWD (-100)
|
|
#define AT_REMOVEDIR 0x200
|
|
#define AT_SYMLINK_NOFOLLOW 0x100
|
|
#define AT_EMPTY_PATH 0x1000
|
|
#define AT_NO_AUTOMOUNT 0x800
|
|
|
|
// prctl options
|
|
#define PR_SET_NAME 15
|
|
#define PR_GET_NAME 16
|
|
#define PR_SET_DUMPABLE 4
|
|
#define PR_GET_DUMPABLE 3
|
|
|
|
// signal numbers (Linux x86_64 values), used by kill dispatch and
|
|
// default-action classification in sigsend()
|
|
#define SIGHUP 1
|
|
#define SIGINT 2
|
|
#define SIGQUIT 3
|
|
#define SIGILL 4
|
|
#define SIGABRT 6
|
|
#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
|
|
#define SIGCHLD 17
|
|
#define SIGCONT 18
|
|
#define SIGSTOP 19
|
|
// Job-control and the remaining standard signals. A shell needs the TSTP/
|
|
// TTIN/TTOU trio to implement ^Z and background-job terminal access at all;
|
|
// they were simply absent, so any reference to them failed to compile.
|
|
#define SIGTRAP 5
|
|
#define SIGTSTP 20
|
|
#define SIGTTIN 21
|
|
#define SIGTTOU 22
|
|
#define SIGURG 23
|
|
#define SIGXCPU 24
|
|
#define SIGXFSZ 25
|
|
#define SIGVTALRM 26
|
|
#define SIGPROF 27
|
|
#define SIGWINCH 28
|
|
#define SIGIO 29
|
|
#define SIGPWR 30
|
|
#define SIGSYS 31
|
|
|
|
// getrusage who
|
|
#define RUSAGE_SELF 0
|
|
#define RUSAGE_CHILDREN (-1)
|
|
|
|
// fallocate mode
|
|
#define FALLOC_FL_KEEP_SIZE 1
|
|
|
|
// poll(2) event bits (Linux values)
|
|
// mmap(2) protection and flags.
|
|
//
|
|
// These lived only in kernel/sys/sys_mem.c, so every userspace caller wrote
|
|
// its own copy -- cmd/fbinfo had one and cmd/pcitest wanted a third. Two
|
|
// private copies of a number the kernel compares against is a mismatch
|
|
// waiting to happen, and the symptom would be a mapping with the wrong
|
|
// permissions rather than an error.
|
|
#define PROT_NONE 0x0
|
|
#define PROT_READ 0x1
|
|
#define PROT_WRITE 0x2
|
|
#define PROT_EXEC 0x4
|
|
|
|
#define MAP_SHARED 0x01
|
|
#define MAP_PRIVATE 0x02
|
|
#define MAP_FIXED 0x10
|
|
#define MAP_ANONYMOUS 0x20
|
|
|
|
// waitpid() options. The kernel tests `options & 1`; this is that bit, named.
|
|
#define WNOHANG 0x001
|
|
|
|
#define POLLIN 0x001
|
|
#define POLLPRI 0x002
|
|
#define POLLOUT 0x004
|
|
#define POLLERR 0x008
|
|
#define POLLHUP 0x010
|
|
#define POLLNVAL 0x020
|
|
|
|
struct pollfd {
|
|
int fd;
|
|
short events;
|
|
short revents;
|
|
};
|