diff --git a/Makefile b/Makefile index c465fef..ef58aff 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,19 @@ KERN_ACPI = $(addprefix $(BUILD)/uacpi/, \ namespace.o notify.o opcodes.o opregion.o osi.o registers.o \ resources.o shareable.o sleep.o stdlib.o tables.o types.o \ uacpi.o utilities.o) -KERN_ACPI += $(addprefix $(BUILD)/kernel/acpi/, host.o aml.o) +KERN_ACPI += $(addprefix $(BUILD)/kernel/acpi/, host.o aml.o i2c_hid.o) + +# zstd's decoder, for SCFF -- the compressed live root (include/fs/scff.h). +# Unpacked by ports/zstd into build/ports/zstd/src and compiled into the kernel +# like uACPI; kernel/scff/ is the host side of it. +ZSTDDIR = $(BUILD)/ports/zstd/src/lib +KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \ + common/entropy_common.o common/error_private.o \ + common/fse_decompress.o common/zstd_common.o common/xxhash.o \ + common/debug.o decompress/huf_decompress.o \ + decompress/zstd_ddict.o decompress/zstd_decompress.o \ + decompress/zstd_decompress_block.o) +KERN_ZSTD += $(BUILD)/kernel/scff/scff.o KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \ proc.o) @@ -114,7 +126,7 @@ KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \ KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \ - $(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_PROC) $(KERN_SYS) + $(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(KERN_PROC) $(KERN_SYS) # entry.o must be first in link order; remove it from KERNOBJS for the rule KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) @@ -392,6 +404,7 @@ endif # looks satisfied. MKFS = $(BUILD)/tools/mkfs$(EXE) MKSCHFS = $(BUILD)/tools/mkschfs$(EXE) +MKSCFF = $(BUILD)/tools/mkscff$(EXE) SCHFS_TEST = $(BUILD)/tools/schfs_test$(EXE) SCHFS_OBJT = $(BUILD)/tools/schfs_objtest$(EXE) SCHFS_FSCK = $(BUILD)/tools/schfs_fsck$(EXE) @@ -431,7 +444,7 @@ CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA) # copies and memset into %xmm, and an interrupt then scribbles over the # registers of the program it interrupted. Kernel objects only -- cmd/ is # built with these CFLAGS too and is welcome to use SSE. -$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o: CFLAGS += -mgeneral-regs-only +$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o $(BUILD)/zstd/%.o: CFLAGS += -mgeneral-regs-only # lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip). # Only the network files are compiled with these: putting them in the global @@ -461,14 +474,14 @@ CFLAGS_SYS = $(CFLAGS) -I./kernel/sys # loads that by PIO one sector at a time -- a kernel with a userland inside it # takes visibly longer to boot than a 350 KB one. # -# 8000 blocks of 4 KB is 32 MB. It was 6000, which the userland had quietly -# grown to fill: 5157 of 6000 used, 86%, after toybox gained its pending -# applets and an editor arrived. That still fit, and the *next* thing added -# would not have -- and what mkschfs says when an image is full is "try a -# larger --size", which is a poor way to find out that a build variant you were -# not using has been broken for a while. +# Sized to fit, not fixed. The image is what the root uses plus RAMDISK_SLACK +# blocks and an eighth, measured by building it once large; the kernel then +# grows the volume into half of the free memory at boot (kernel/dev/ramdisk.c), +# which is where `apk add` on the live system writes. It was a fixed 96 MB, +# 60 of it zeros carried in the kernel and twice over on the ISO, and room for +# 60 MB of packages whatever the machine had. # ----------------------------------------------------------------------- -RAMDISK_BLOCKS ?= 8000 +RAMDISK_SLACK ?= 1024 # RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table. # The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates @@ -486,10 +499,10 @@ RAMDISK_BLOCKS ?= 8000 # This is not hypothetical: a 33 MB test payload once landed here and overflowed # the 6 MB image, and mkfs's "increase FSSIZE" message then pointed at a constant # that had nothing to do with it. -# usb.ids and pci.ids are 2 MB of a 6 MB image and nothing needs them to boot. -# They are lookup tables for userspace tools, so they stay on the disk image -# and out of the kernel binary. -RAMDISK_EXCLUDE = usr/share/hwdata +# usb.ids and pci.ids used to be on it, as 2 MB of a 6 MB image. They are +# Alpine's hwdata packages now: apk owns them, and a root missing files its +# database lists would be one apk had to repair. +RAMDISK_EXCLUDE = RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p)) ifdef RAMROOT @@ -519,6 +532,17 @@ $(shell mkdir -p $(BUILD); \ printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \ fi) +# ----------------------------------------------------------------------- +# The root filesystem is an Alpine root that apk manages, with SchOS installed +# into it as the package schos-base -- see docs/plan/alpine.md and "The +# apk-managed root" below. Both images are made from it: schfs.img, which +# `make qemu` boots, and the ramdisk inside the kernel, which `make iso` +# boots. sysroot/ is what schos-base is packaged from, not a root of its own. +# ----------------------------------------------------------------------- +AROOT_DIR = $(BUILD)/aroot +ROOT_TREE = $(AROOT_DIR)/root +ROOT_STAMP = $(AROOT_DIR)/stamp + # ----------------------------------------------------------------------- # Top-level targets # ----------------------------------------------------------------------- @@ -757,9 +781,9 @@ iso: $(MAKE) RAMROOT=1 schos.iso # One El Torito entry, pointing at a FAT image holding the loader and kernel. -# sysroot-install because the disc also carries the tree as a real hierarchy. -schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI tools/posix/mkiso.sh sysroot-install - SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ +# The root is in the kernel, as its ramdisk; the disc carries no other copy. +schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI tools/posix/mkiso.sh $(ROOT_STAMP) + sh tools/posix/mkiso.sh $@ # A partitioned disk -- GPT, an ESP and a SchFS partition -- which is what a # real machine boots from: dd it to a USB stick or a disk. See @@ -816,7 +840,7 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o $(OBJDUMP) -S $@ > $(BUILD)/kernel/kernel.asm # The ramdisk root image, and the object wrapping it for the linker. Built from -# the same staged sysroot as the main image, just capped at RAMDISK_BLOCKS -- +# the same staged sysroot as the main image, sized to fit (see RAMDISK_SLACK) -- # mkfs's content dedup means the whole userland still fits in a few MB. # Built with mkschfs, not mkfs. # @@ -825,19 +849,33 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o # volume on the root device" and made `make iso` and `make ramroot` unbootable. # RAMDISK_NINODES is gone with it: SchFS has no preallocated inode table, which # is what that knob existed to size. -$(BUILD)/fs-ram.img: $(MKSCHFS) $(BUILD)/sysroot.stamp +$(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP) @mkdir -p $(BUILD) @rm -f $@ - $(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \ - $(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT) + @used=$$($(MKSCHFS) $@ --size 262144 --label schos \ + $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \ + sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \ + test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \ + rm -f $@; \ + $(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \ + $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) -$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img +# The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks, +# each its own zstd frame, unpacked by the kernel the first time a block in it +# is read. -19 is slow to write and costs nothing to read -- zstd decodes at +# the same speed whatever level made the frame -- and mkscff uses every core. +SCFF_LEVEL ?= 19 +SCFF_CHUNK ?= 128 +$(BUILD)/fs-ram.scff: $(BUILD)/fs-ram.img $(MKSCFF) + $(MKSCFF) $< $@ --level $(SCFF_LEVEL) --chunk $(SCFF_CHUNK) + +$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.scff @mkdir -p $(BUILD)/kernel $(LD) -r -b binary -m elf_x86_64 -o $@ $< $(OBJCOPY) \ - --redefine-sym _binary_build_fs_ram_img_start=_binary_ramdisk_start \ - --redefine-sym _binary_build_fs_ram_img_end=_binary_ramdisk_end \ - --redefine-sym _binary_build_fs_ram_img_size=_binary_ramdisk_size \ + --redefine-sym _binary_build_fs_ram_scff_start=_binary_ramdisk_start \ + --redefine-sym _binary_build_fs_ram_scff_end=_binary_ramdisk_end \ + --redefine-sym _binary_build_fs_ram_scff_size=_binary_ramdisk_size \ $@ # The console font, linked into the kernel. @@ -1003,6 +1041,30 @@ $(UACPIDIR): $(patsubst $(BUILD)/uacpi/%.o,$(UACPIDIR)/source/%.c,$(filter $(BUILD)/uacpi/%,$(KERN_ACPI))): | $(UACPIDIR) @: +# zstd at -O2 whatever OPT says: the decoder runs on every first read of a +# root block, and at -O0 it is several times slower. No assembly, no BMI2 +# dispatch, no legacy formats, no tracing hooks -- the kernel has none of +# what those need. compat/ stands in for the libc headers it includes. +ZSTD_CFLAGS = -Ikernel/scff/compat -I$(ZSTDDIR) -I$(ZSTDDIR)/common -O2 \ + -DZSTD_DISABLE_ASM -DDYNAMIC_BMI2=0 -DZSTD_LEGACY_SUPPORT=0 \ + -DZSTD_TRACE=0 -DDEBUGLEVEL=0 -DZSTD_STRIP_ERROR_STRINGS \ + -DXXH_NAMESPACE=ZSTD_ +ZSTD_CC = $(CC) $(filter-out -I./include,$(CFLAGS)) $(ZSTD_CFLAGS) + +$(BUILD)/zstd/%.o: $(ZSTDDIR)/%.c | $(ZSTDDIR) + @mkdir -p $(dir $@) + $(ZSTD_CC) -Wno-unused-function -c $< -o $@ + +$(BUILD)/kernel/scff/%.o: kernel/scff/%.c | $(ZSTDDIR) + @mkdir -p $(dir $@) + $(ZSTD_CC) -c $< -o $@ + +$(ZSTDDIR): + $(MAKE) -C ports zstd + +$(patsubst $(BUILD)/zstd/%.o,$(ZSTDDIR)/%.c,$(filter $(BUILD)/zstd/%,$(KERN_ZSTD))): | $(ZSTDDIR) + @: + $(BUILD)/kernel/proc/%.o: kernel/proc/%.c @mkdir -p $(BUILD)/kernel/proc $(CC) $(CFLAGS) -c $< -o $@ @@ -1206,6 +1268,14 @@ $(SCHFS_FSCK): tools/schfs_fsck.c $(SCHFS_SRC) $(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_fsck.c mkschfs: $(MKSCHFS) + +# The SCFF writer, built from the same zstd release as the kernel's decoder. +MKSCFF_ZSTD = $(wildcard $(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c) +$(MKSCFF): tools/mkscff.c include/fs/scff.h | $(ZSTDDIR) + @mkdir -p $(BUILD)/tools + $(HOSTCC) -O2 -pthread -iquote ./include -I$(ZSTDDIR) -DZSTD_DISABLE_ASM \ + -DZSTD_LEGACY_SUPPORT=0 -o $@ tools/mkscff.c \ + $(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c schfs-fsck: $(SCHFS_FSCK) # End-to-end filesystem test: build an image from a directory tree, read it @@ -1548,12 +1618,15 @@ qemu-console: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) # is "try a larger --size", which is a worse thing to read than a number here # is to change. A raw disk image is sparse on any filesystem worth the name, # so the unused part costs nothing until it is used. -SCHFS_BLOCKS ?= 25600 +# +# 262144 blocks, 1 GB, since the root became one apk installs into: Xorg alone +# is 270 MB. Sparse, so the empty part costs nothing on the host. +SCHFS_BLOCKS ?= 262144 -$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp +$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP) @mkdir -p $(BUILD) @rm -f $@ - ./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(SYSROOT) + ./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE) # --- The apk-managed root ------------------------------------------------------ # @@ -1561,14 +1634,11 @@ $(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp # apk package schos-base -- a root that `apk add` and `apk upgrade` manage. # See tools/posix/mkaroot.sh for what goes where and why. # -# make aroot build/aroot.img, a SchFS root volume of AROOT_BLOCKS -# make qemu-aroot boot it as the root disk (the disk-root kernel) +# make aroot the tree and build/schfs.img; `make qemu` boots it # make alpine-lock re-resolve ports/alpine/world against Alpine's mirror; # the one step that looks for newer packages # # The build itself is offline once dist/ has the files world.lock names. -AROOT_BLOCKS ?= 262144 -AROOT_DIR = $(BUILD)/aroot AROOT_KEYS = $(BUILD)/apk-keys APK_STATIC = $(BUILD)/ports/alpine/apk.static ALPINE_MINI = dist/alpine-minirootfs-3.24.3-x86_64.tar.gz @@ -1585,21 +1655,9 @@ $(AROOT_DIR)/stamp: $(BUILD)/sysroot.stamp ports/alpine/world.lock tools/posix/m $(AROOT_KEYS) $(SCHOS_PKGVER) ports/alpine/world.lock dist/alpine touch $@ -$(BUILD)/aroot.img: $(MKSCHFS) $(AROOT_DIR)/stamp - @rm -f $@ - ./$(MKSCHFS) $@ --size $(AROOT_BLOCKS) --label schos --sysroot $(AROOT_DIR)/root +aroot: $(BUILD)/schfs.img -aroot: $(BUILD)/aroot.img - -qemu-aroot: esp $(BUILD)/aroot.img $(OVMF_CODE) $(OVMF_VARS) - $(QEMU) -machine pc-i440fx-9.2 \ - -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) \ - -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) \ - -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk,format=raw \ - -drive file=$(BUILD)/aroot.img,format=raw,index=1,media=disk \ - -m 2G -smp 2 $(QEMU_ACCEL) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) $(QEMU_AUDIO) - -.PHONY: alpine-port alpine-lock aroot qemu-aroot +.PHONY: alpine-port alpine-lock aroot # Boot with a SchFS volume attached as the second disk. # diff --git a/NOTICE b/NOTICE index 333b4f6..4578dce 100644 --- a/NOTICE +++ b/NOTICE @@ -151,6 +151,46 @@ interpreter, used for PCI interrupt routing, the power button and power-off. The host layer written for SchOS (kernel/acpi/) is GPL-2.0-or-later and is not part of uACPI; uACPI's own sources are unmodified. +-------------------------------------------------------------------------------- +zstd 1.5.7 BSD 3-Clause (or GPL-2.0) +-------------------------------------------------------------------------------- +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Fetched by ports/zstd. Its decoder is compiled into the kernel to read SCFF, +the compressed live root (docs/architecture/scff.md), and tools/mkscff builds +the whole library on the host to write it. zstd is dual-licensed; SchOS takes +it under the BSD licence above. The glue written for SchOS (kernel/scff/) is +GPL-2.0-or-later and is not part of zstd; zstd's own sources are unmodified. + -------------------------------------------------------------------------------- Intel 8254x device identifiers BSD 3-Clause -------------------------------------------------------------------------------- diff --git a/docs/architecture/scff.md b/docs/architecture/scff.md new file mode 100644 index 0000000..5c1439e --- /dev/null +++ b/docs/architecture/scff.md @@ -0,0 +1,79 @@ +# SCFF: the SchOS Compressed File Format + +The live root is shipped compressed. `make iso` builds the root as a SchFS +image, `tools/mkscff` writes that image as SCFF, and the kernel carries the +`.scff` file as its ramdisk. Blocks are decompressed the first time something +reads them. + +| | raw image | SCFF | +|---|---|---| +| root with Xorg and Window Maker | 358 MB | 102 MB | +| kernel and ISO | carry all 358 MB | carry 102 MB | +| memory the root takes at boot | 358 MB | what has been read | + +## Layout + +The definition is `include/fs/scff.h`, which the kernel and `mkscff` both +use. + + offset 0 struct scff_header (64 bytes, "SCFF", version 1) + index_off uint64 off[nchunks + 1] + off[i]..off[i+1] chunk i + +The image is cut into chunks of `chunk_bytes`, 128 KB by default. Each chunk +is stored in one of three ways, and its stored length says which: + +- **Zero bytes:** every byte of the chunk is zero. The slack at the end of the + image costs nothing. +- **The chunk's full length:** stored as it is, because it did not compress. +- **Anything else:** one zstd frame, with a content checksum. + +## Why it is shaped like this + +**Chunks, not one stream.** Each chunk can be decompressed without the others, +so reading one file does not mean decompressing the root. This costs a little +ratio: the 358 MB root is 104 MB in 128 KB chunks and 97 MB in 1 MB chunks. +128 KB is squashfs's default too. + +**A block image, not a compressed filesystem.** squashfs compresses files and +is read-only, so a writable root needs an overlay filesystem above it. SCFF +compresses a SchFS image instead, and the ramdisk is still a block device that +takes writes. A block that is written gets a page of its own, exactly as it +did before SCFF. SchFS does not know the image was ever compressed. + +**zstd.** It compresses close to xz at the levels a build can afford (`-19`). +It decompresses at memory speed whatever level made the frame. Its decoder is +dual BSD and GPL-2.0, and it is a plain C library. `ports/zstd` pins the +release. The kernel compiles only the decoder half. `mkscff` compiles all of +it from the same release, so neither side depends on the host's libzstd. + +## In the kernel + +`kernel/dev/ramdisk.c` reads the header at boot, checks it and the index, and +panics if either is wrong. A damaged image is found before anything mounts. + +The ramdisk's page directory covers every block. A block with no page has not +been unpacked yet. Reading one decompresses its whole chunk into `rd_chunk`, +and every block in that chunk without a page gets one. A block of zeros points +at one shared zero page and gets a real page only when it is written. + +`kernel/scff/scff.c` holds the decoder: one static zstd context in a 256 KB +workspace, so nothing allocates. **Decompression runs on a 64 KB stack of its +own.** A kernel stack is 8 KB, and a chunk is decompressed at the bottom of a +read that has already used most of it. zstd's sequence decoder needs several +KB of locals, and Linux gives it a 16 KB stack. The chunk lock (`rd_zlock`) +makes that stack its holder's alone. `kernel/scff/compat/` stands in for the +few libc headers zstd includes. + +zstd is built at `-O2` whatever `OPT` says, because it runs on every first +read of a block. + +## Building + + make iso builds build/fs-ram.scff and links it in + make iso SCFF_LEVEL=3 much faster to build, about a third bigger + make iso SCFF_CHUNK=256 bigger chunks: a little smaller, more work per miss + +`mkscff` uses every core. At `-19` the root takes about 11 seconds on 16 +threads. It decompresses every chunk again before writing it, so an image +that would not boot fails the build instead. diff --git a/docs/plan/alpine.md b/docs/plan/alpine.md index a63a801..5e141ab 100644 --- a/docs/plan/alpine.md +++ b/docs/plan/alpine.md @@ -38,10 +38,12 @@ only what is its own (see `tools/posix/mkaroot.sh`): ## Building - make aroot build/aroot.img: the root, as a SchFS volume - make qemu-aroot boot it + make qemu boots build/schfs.img, the root as a 1 GB SchFS disk + make iso the same root as the live system's ramdisk make alpine-lock re-resolve world against Alpine (needs the network) +It is the only root. sysroot/ is what schos-base is packaged from. + Only `alpine-lock` asks Alpine what is current. A normal build installs exactly what `world.lock` names: files in `dist/alpine/`, fetched once and checked against the lock's sha256, and against Alpine's signatures by apk. @@ -66,8 +68,13 @@ All of it found by running apk: ## Next -- The live ISO and USB stick are still the ramdisk root. They need the apk - root on the stick itself, or an installer that writes it to a disk. -- Xorg from apk (`xorg-server xf86-video-fbdev xf86-input-evdev`) instead of - the hand port in [xorg.md](xorg.md). The packages install; running them is - the next test. +- The live ISO runs the apk root from RAM. The image in the kernel is only + what the root holds; at boot the volume grows into half of the free memory + (4 GB of root on an 8 GB machine), backed by pages as they are written. + Xorg installs into it in about a minute. All of it is gone at reboot. Keeping packages needs an installer that writes the root + to a SATA or NVMe disk, and a kernel that finds its root there by the + SchFS GPT type rather than as IDE disk 1. +- Xorg and Window Maker are in `world`, so the image starts with them: + `startx` runs `wmaker` from `/root/.xinitrc`. Mesa brings LLVM with it, + about 200 MB of the 358 MB root; the root ships compressed + ([SCFF](../architecture/scff.md)), so that costs about 60 MB of ISO. diff --git a/etc/zsh/zshrc b/etc/zsh/zshrc index 7e500ba..1b79ebc 100644 --- a/etc/zsh/zshrc +++ b/etc/zsh/zshrc @@ -22,6 +22,12 @@ setopt EXTENDED_GLOB setopt INTERACTIVE_COMMENTS # a # in an interactive line is a comment setopt NO_BEEP # SchOS has no audio, but it would also scroll unsetopt FLOW_CONTROL # ^S/^Q are worth more as keys than as flow control +# No PROMPT_SP. It marks output that did not end in a newline by printing a +# `#` and then padding to $COLUMNS, trusting the terminal to wrap so the next +# CR can erase it. The console is mirrored to the serial port, and a host +# terminal narrower than the SchOS screen wraps the padding early: every +# prompt came with a `#` line above it. +unsetopt PROMPT_SP # --------------------------------------------------------------------------- # Keys diff --git a/include/core/defs.h b/include/core/defs.h index 94ce346..c7cbd0c 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -368,6 +368,7 @@ void input_mouse(int dx, int dy, int buttons); void ramdiskinit(void); void ramdiskrw(struct buf*); int ramdisk_present(void); +uint64 ramdisk_blocks(void); // usb_storage.c void usb_storage_rw(struct buf*); @@ -491,6 +492,10 @@ 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 sockbind_abstract(struct socket*, uint64 key); +int sockname_unix(struct socket*, int peer, char *path); +int sockname_inet(struct socket*, int peer, uint *addr, ushort *port); +#define SOCK_ABSTRACT_DEV 0xFFFFFFFFu // no filesystem has it; see unix.c int sockreadable(struct socket*); int sock_hup(struct socket*); int sock_shutdown(struct socket*, int how); @@ -522,12 +527,18 @@ 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_name(void *h, int peer, uint *addr, ushort *port); 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); +int inet_shutdown(void *h, int how); +int file_describe(struct file *f, char *buf, int cap); +int proc_fd_mask(int pid, uint64 mask[2]); +int proc_fd_link(int pid, int fd, char *buf, int cap); +const char *ev_kind_name(struct evobj *o); void inet_close(void *h); // proc.c @@ -543,6 +554,11 @@ int proc_pgid(int pid); int proc_setpgid(int pid, int pgid); int proc_setsid(void); int sigsend(int pid, int sig); +void alarm_arm(struct proc *p, uint after, uint every); +uint alarm_left(struct proc *p); +void alarm_thread_start(void); +void poll_kick(void); +int poll_wait_tick(void); int sig_default_kills(int sig); int proc_exists(int); int proc_num(void); diff --git a/include/core/proc.h b/include/core/proc.h index 369ddef..fbb692e 100644 --- a/include/core/proc.h +++ b/include/core/proc.h @@ -231,7 +231,11 @@ struct proc { uint64 signal_mask; uint64 pending_signals; - int alarm_ticks; + // ITIMER_REAL, which alarm() is the whole-second form of: SIGALRM is due at + // tick alarm_at (0 = not armed) and then every alarm_every ticks after. + // alarmd, in kernel/proc/proc.c, is what posts it. + uint alarm_at; + uint alarm_every; char ofile_flags[NOFILE]; // Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's diff --git a/include/dev/hid.h b/include/dev/hid.h new file mode 100644 index 0000000..a390ddb --- /dev/null +++ b/include/dev/hid.h @@ -0,0 +1,17 @@ +#pragma once + +#include "core/types.h" + +// HID over transports other than USB. The report parser and the keyboard and +// pointer paths are kernel/drivers/usb/hid/usb_hid.c's; a transport hands +// over the report descriptor once and every input report after it. + +// The device described by `desc`, or 0 if it is neither a keyboard nor a +// pointer. `what` names it in the log. +void *hid_attach(const uchar *desc, int n, const char *what); +// One input report, report ID included when the device uses them. +void hid_attached_input(void *h, const uchar *rep, int len); + +// I2C-HID: the touchpads (and some keyboards) of laptops. Started once ACPI +// and kernel threads are up; see kernel/drivers/i2c/i2c_hid.c. +void i2c_hid_start(void); diff --git a/include/fs/file.h b/include/fs/file.h index 6da4970..7759a7b 100644 --- a/include/fs/file.h +++ b/include/fs/file.h @@ -25,6 +25,11 @@ struct file { int memfd_seg; struct evobj *ev; uint64 off; + // What /proc//fd shows. `ino` names the open thing (one per socket, + // one per pipe); `path` is the absolute path an inode was opened by, as + // written -- symlinks along it are not resolved. kmalloc'd, or 0. + uint64 ino; + char *path; struct file *fnext; // file.c's free list, while ref is 0 }; diff --git a/include/fs/scff.h b/include/fs/scff.h new file mode 100644 index 0000000..19e0c44 --- /dev/null +++ b/include/fs/scff.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SCFF, the SchOS Compressed File Format: a block image cut into chunks, each +// compressed on its own, with an index in front. +// +// It is what the live root is shipped as. The kernel carries the compressed +// image and decompresses a chunk the first time any block in it is read, so +// the kernel and the ISO are the compressed size, and the memory the root +// takes is what has been touched, not the whole of it. See kernel/dev/ramdisk.c +// for the reader and tools/mkscff.c for the writer; this header is the one +// definition of the layout both of them use. +// +// offset 0 struct scff_header +// index_off uint64_t off[nchunks + 1], from the start of the file +// off[i]..off[i+1] chunk i +// +// A chunk is chunk_bytes of the image (the last one may be shorter). Its +// stored length says how it is kept: +// +// 0 every byte is zero, and nothing is stored +// its full length stored as it is: it did not compress +// anything else one zstd frame +// +// Little-endian throughout, which is every machine SchOS runs on. + +#ifndef SCHOS_FS_SCFF_H +#define SCHOS_FS_SCFF_H + +#include + +#define SCFF_MAGIC "SCFF" +#define SCFF_VERSION 1 +#define SCFF_CODEC_ZSTD 1 + +// The kernel decompresses one chunk at a time into a buffer this big, so it +// is the largest chunk a reader accepts. 128 KB is squashfs's default and +// the point past which a bigger chunk buys little ratio for a lot of +// decompression on every miss. +#define SCFF_CHUNK_MAX (1u << 20) + +struct scff_header { + char magic[4]; // "SCFF" + uint16_t version; // SCFF_VERSION + uint16_t codec; // SCFF_CODEC_ZSTD + uint32_t chunk_bytes; // a power of two, a multiple of 4096, <= SCFF_CHUNK_MAX + uint32_t nchunks; + uint64_t image_bytes; // the uncompressed image + uint64_t index_off; + uint64_t stored_bytes; // the whole file, header and index included + uint8_t reserved[24]; +}; + +_Static_assert(sizeof(struct scff_header) == 64, "scff_header is 64 bytes"); + +#endif diff --git a/include/gfx/fb.h b/include/gfx/fb.h index e32ee79..92a4786 100644 --- a/include/gfx/fb.h +++ b/include/gfx/fb.h @@ -19,6 +19,7 @@ #define FBIOGET_VSCREENINFO 0x4600 #define FBIOPUT_VSCREENINFO 0x4601 #define FBIOGET_FSCREENINFO 0x4602 +#define FBIOPUTCMAP 0x4605 #define FBIOBLANK 0x4611 #define FBIOPAN_DISPLAY 0x4606 diff --git a/include/net/socket.h b/include/net/socket.h index 15753d9..ccfdcdf 100644 --- a/include/net/socket.h +++ b/include/net/socket.h @@ -330,6 +330,9 @@ struct socket { // bidirectional connection. struct socket *peer; int peer_closed; + // shutdown(SHUT_WR): this end has said all it will. The peer reads EOF but + // can still answer, which is what a request/response helper depends on. + int wr_shut; char data[SOCKBUF]; uint nread, nwrite; diff --git a/kernel/acpi/i2c_hid.c b/kernel/acpi/i2c_hid.c new file mode 100644 index 0000000..d62ffc2 --- /dev/null +++ b/kernel/acpi/i2c_hid.c @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Laptop touchpads: HID over I2C, on the SoC's DesignWare I2C controller. +// +// A laptop's touchpad is not on PS/2 any more and not on USB either. It hangs +// off an I2C bus inside the chipset, and the only place that says so is ACPI: +// a device whose _HID or _CID is PNP0C50, with an I2cSerialBus resource +// naming the controller and the touchpad's address, a GpioInt naming the pin +// it pulls when it has something to say, and a _DSM giving the one register +// number the protocol needs to bootstrap itself. Without this the touchpad of +// every recent laptop -- the ASUS TUF A15's included -- does nothing at all. +// +// Three layers, all here because each is small: +// +// The controller. Every x86 chipset since Haswell, AMD's included, uses the +// same Synopsys DesignWare I2C block (AMDI0010 on Ryzen, INT33C3, INT3433 +// and the PCI LPSS functions on Intel). Driven by polling: a touchpad report +// is about 30 bytes, a millisecond on the bus. +// +// HID over I2C (Microsoft's specification, version 1.0). Read the HID +// descriptor from the register _DSM names; power the device on and reset +// it; read the report descriptor; then every input report is a read of the +// input register, two bytes of length and the report. +// +// The interrupt line. It is level-triggered and stays asserted for as long +// as a report is waiting, so instead of routing it through the GPIO +// controller's interrupt the pin's *level* is read, every tick: low means +// read a report. That is exact -- no report is read twice and none is +// missed -- and needs nothing from the GPIO controller but its registers. +// On AMD those are one 32-bit word per pin. Elsewhere there is no pin to +// watch, and the input register is simply read every tick; devices answer +// a read with nothing waiting with a zero length. +// +// The reports themselves go through the USB HID driver's parser and pointer +// path (include/dev/hid.h). A Windows Precision Touchpad starts in mouse mode +// after a reset -- relative motion, a button for a click and for a tap -- and +// that is the report taken: it is what makes it a mouse on /dev/input/event1, +// for the console and for X. Multi-finger reports (two-finger scroll) need the +// device switched to touchpad mode and a gesture layer; not yet. + +#include "core/types.h" +#include "core/defs.h" +#include "core/param.h" +#include "core/spinlock.h" +#include "arch/mmu.h" +#include "dev/hid.h" +#include "dev/acpi.h" + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// DesignWare I2C +// --------------------------------------------------------------------------- + +#define IC_CON 0x00 +#define IC_TAR 0x04 +#define IC_DATA_CMD 0x10 +#define IC_SS_SCL_HCNT 0x14 +#define IC_SS_SCL_LCNT 0x18 +#define IC_FS_SCL_HCNT 0x1C +#define IC_FS_SCL_LCNT 0x20 +#define IC_INTR_MASK 0x30 +#define IC_RAW_INTR_STAT 0x34 +#define IC_RX_TL 0x38 +#define IC_TX_TL 0x3C +#define IC_CLR_INTR 0x40 +#define IC_CLR_TX_ABRT 0x54 +#define IC_CLR_STOP_DET 0x60 +#define IC_ENABLE 0x6C +#define IC_STATUS 0x70 +#define IC_TXFLR 0x74 +#define IC_RXFLR 0x78 +#define IC_SDA_HOLD 0x7C +#define IC_TX_ABRT_SOURCE 0x80 +#define IC_ENABLE_STATUS 0x9C +#define IC_COMP_PARAM_1 0xF4 +#define IC_COMP_TYPE 0xFC + +#define DW_COMP_TYPE 0x44570140 + +#define CON_MASTER (1 << 0) +#define CON_SPEED_STD (1 << 1) +#define CON_SPEED_FAST (2 << 1) +#define CON_RESTART_EN (1 << 5) +#define CON_SLAVE_DISABLE (1 << 6) + +#define CMD_READ (1 << 8) +#define CMD_STOP (1 << 9) +#define CMD_RESTART (1 << 10) + +#define INTR_TX_ABRT (1 << 6) +#define INTR_STOP_DET (1 << 9) + +#define NCTL 4 + +struct dwi2c { + uint64 phys; + volatile uint32 *regs; + int txdepth, rxdepth; + int fast; +}; + +static struct dwi2c ctls[NCTL]; +static int nctl; + +static uint32 +rd(struct dwi2c *c, int reg) +{ + return c->regs[reg / 4]; +} + +static void +wr(struct dwi2c *c, int reg, uint32 v) +{ + c->regs[reg / 4] = v; +} + +static int +dw_enable(struct dwi2c *c, int on) +{ + int i; + + for(i = 0; i < 1000; i++){ + wr(c, IC_ENABLE, on); + if((rd(c, IC_ENABLE_STATUS) & 1) == (uint32)on) + return 0; + microdelay(25); + } + return -1; +} + +// SCL high and low periods, in controller clocks, from the firmware's own +// numbers when it gives them: SSCN and FMCN are packages of (HCNT, LCNT, +// SDA hold) for exactly this controller's clock. +static int +acpi_counts(uacpi_namespace_node *node, const char *name, + uint32 *hcnt, uint32 *lcnt, uint32 *hold) +{ + uacpi_object *o = 0; + uacpi_object_array a; + uacpi_u64 v[3]; + int i; + + if(uacpi_eval_package(node, name, 0, &o) != UACPI_STATUS_OK) + return -1; + if(uacpi_object_get_package(o, &a) != UACPI_STATUS_OK || a.count < 2){ + uacpi_object_unref(o); + return -1; + } + v[2] = 0; + for(i = 0; i < 3 && i < (int)a.count; i++) + if(uacpi_object_get_integer(a.objects[i], &v[i]) != UACPI_STATUS_OK){ + uacpi_object_unref(o); + return -1; + } + uacpi_object_unref(o); + *hcnt = v[0]; + *lcnt = v[1]; + *hold = v[2]; + return 0; +} + +static int +dw_init(struct dwi2c *c, uacpi_namespace_node *node, uint32 speed) +{ + uint32 type, param, hcnt, lcnt, hold = 0; + + c->regs = ioremap(c->phys, 0x1000); + if(c->regs == 0) + return -1; + type = rd(c, IC_COMP_TYPE); + if(type != DW_COMP_TYPE){ + cprintf("i2c: controller at 0x%lx is not DesignWare (0x%x)\n", + c->phys, type); + return -1; + } + param = rd(c, IC_COMP_PARAM_1); + c->txdepth = param ? ((param >> 16) & 0xFF) + 1 : 8; + c->rxdepth = param ? ((param >> 8) & 0xFF) + 1 : 8; + c->fast = speed >= 400000; + + if(dw_enable(c, 0) < 0) + return -1; + wr(c, IC_CON, CON_MASTER | CON_SLAVE_DISABLE | CON_RESTART_EN | + (c->fast ? CON_SPEED_FAST : CON_SPEED_STD)); + + // Without the firmware's numbers, periods long enough for a 150 MHz clock, + // the fastest any of these runs at: on a slower clock the bus is merely + // slower than it could be, never out of specification. + if(c->fast){ + if(acpi_counts(node, "FMCN", &hcnt, &lcnt, &hold) < 0){ + hcnt = 135; // 0.6 us high, 1.3 us low, plus rise and fall + lcnt = 240; + } + wr(c, IC_FS_SCL_HCNT, hcnt); + wr(c, IC_FS_SCL_LCNT, lcnt); + } else { + if(acpi_counts(node, "SSCN", &hcnt, &lcnt, &hold) < 0){ + hcnt = 645; // 4.0 us high, 4.7 us low, plus the same + lcnt = 750; + } + wr(c, IC_SS_SCL_HCNT, hcnt); + wr(c, IC_SS_SCL_LCNT, lcnt); + } + if(hold) + wr(c, IC_SDA_HOLD, hold); + wr(c, IC_INTR_MASK, 0); + wr(c, IC_RX_TL, 0); + wr(c, IC_TX_TL, 0); + rd(c, IC_CLR_INTR); + return 0; +} + +// Write `wn` bytes to `addr`, then read `rn` bytes back after a repeated +// start, all as one transaction. 0, or -1 if the device did not answer. +// +// The command FIFO holds reads as well as writes, and each read becomes a +// byte in the receive FIFO, so reads are queued no faster than they are +// drained: a full receive FIFO stalls the bus. +static int +dw_xfer(struct dwi2c *c, int addr, const uchar *w, int wn, uchar *r, int rn) +{ + int total = wn + rn, sent = 0, got = 0, spin; + uint32 raw; + + if(dw_enable(c, 0) < 0) + return -1; + wr(c, IC_TAR, addr); + rd(c, IC_CLR_INTR); + if(dw_enable(c, 1) < 0) + return -1; + + for(spin = 0; got < rn || sent < total; spin++){ + raw = rd(c, IC_RAW_INTR_STAT); + if(raw & INTR_TX_ABRT) + goto abort; + while(got < rn && rd(c, IC_RXFLR) > 0) + r[got++] = rd(c, IC_DATA_CMD); + while(sent < total && (int)rd(c, IC_TXFLR) < c->txdepth){ + uint32 cmd; + if(sent >= wn && (sent - wn) - got >= c->rxdepth) + break; + if(sent < wn) + cmd = w[sent]; + else + cmd = CMD_READ | (sent == wn && wn > 0 ? CMD_RESTART : 0); + if(sent == total - 1) + cmd |= CMD_STOP; + wr(c, IC_DATA_CMD, cmd); + sent++; + } + if(spin > 20000) // 200 ms: no transfer of ours takes that long + goto abort; + microdelay(10); + } + for(spin = 0; !(rd(c, IC_RAW_INTR_STAT) & INTR_STOP_DET); spin++){ + if(rd(c, IC_RAW_INTR_STAT) & INTR_TX_ABRT) + goto abort; + if(spin > 2000) + goto abort; + microdelay(10); + } + rd(c, IC_CLR_STOP_DET); + dw_enable(c, 0); + return 0; + +abort: + rd(c, IC_TX_ABRT_SOURCE); + rd(c, IC_CLR_TX_ABRT); + rd(c, IC_CLR_INTR); + dw_enable(c, 0); + return -1; +} + +// --------------------------------------------------------------------------- +// ACPI +// --------------------------------------------------------------------------- + +struct found { + uint64 mem; // the first memory range + int i2c, addr; + uint32 speed; + char bus[64]; // the controller's namespace path + int gpio, pin, active_low; + char gpioctl[64]; +}; + +static void +copy_source(char *dst, const uacpi_resource_source *s) +{ + int n = s->string && s->length ? s->length : 0; + if(n > 63) + n = 63; + if(n) + memmove(dst, s->string, n); + dst[n] = 0; +} + +static uacpi_iteration_decision +take_resource(void *user, uacpi_resource *r) +{ + struct found *f = user; + + switch(r->type){ + case UACPI_RESOURCE_TYPE_FIXED_MEMORY32: + if(!f->mem) f->mem = r->fixed_memory32.address; + break; + case UACPI_RESOURCE_TYPE_MEMORY32: + if(!f->mem) f->mem = r->memory32.minimum; + break; + case UACPI_RESOURCE_TYPE_ADDRESS32: + if(!f->mem && r->address32.common.type == UACPI_RANGE_MEMORY) + f->mem = r->address32.minimum; + break; + case UACPI_RESOURCE_TYPE_ADDRESS64: + if(!f->mem && r->address64.common.type == UACPI_RANGE_MEMORY) + f->mem = r->address64.minimum; + break; + case UACPI_RESOURCE_TYPE_SERIAL_I2C_CONNECTION: + if(!f->i2c){ + f->i2c = 1; + f->addr = r->i2c_connection.slave_address; + f->speed = r->i2c_connection.connection_speed; + copy_source(f->bus, &r->i2c_connection.common.source); + } + break; + case UACPI_RESOURCE_TYPE_GPIO_CONNECTION: + if(!f->gpio && r->gpio_connection.type == UACPI_GPIO_CONNECTION_INTERRUPT && + r->gpio_connection.pin_table_length > 0){ + f->gpio = 1; + f->pin = r->gpio_connection.pin_table[0]; + f->active_low = r->gpio_connection.intr.polarity != UACPI_POLARITY_ACTIVE_HIGH; + copy_source(f->gpioctl, &r->gpio_connection.source); + } + break; + } + return UACPI_ITERATION_DECISION_CONTINUE; +} + +static int +resources(uacpi_namespace_node *node, struct found *f) +{ + memset(f, 0, sizeof(*f)); + return uacpi_for_each_device_resource(node, "_CRS", take_resource, f) == + UACPI_STATUS_OK ? 0 : -1; +} + +// The I2C-HID _DSM, function 1: the HID descriptor's register. +static int +hid_desc_register(uacpi_namespace_node *node, uint32 *reg) +{ + static const uchar uuid[16] = { // 3CDFF6F7-4267-4555-AD05-B30A3D8938DE + 0xF7, 0xF6, 0xDF, 0x3C, 0x67, 0x42, 0x55, 0x45, + 0xAD, 0x05, 0xB3, 0x0A, 0x3D, 0x89, 0x38, 0xDE, + }; + uacpi_data_view view = { .const_bytes = uuid, .length = sizeof(uuid) }; + uacpi_object_array none = { 0, 0 }; + uacpi_object *args[4], *ret = 0; + uacpi_object_array a = { args, 4 }; + uacpi_u64 v = 0; + int rc = -1, i; + + args[0] = uacpi_object_create_buffer(view); + args[1] = uacpi_object_create_integer(1); // revision + args[2] = uacpi_object_create_integer(1); // function + args[3] = uacpi_object_create_package(none); + if(args[0] && args[1] && args[2] && args[3] && + uacpi_eval(node, "_DSM", &a, &ret) == UACPI_STATUS_OK && ret && + uacpi_object_get_integer(ret, &v) == UACPI_STATUS_OK){ + *reg = v; + rc = 0; + } + if(ret) + uacpi_object_unref(ret); + for(i = 0; i < 4; i++) + if(args[i]) + uacpi_object_unref(args[i]); + return rc; +} + +static uacpi_namespace_node * +node_at(const char *path) +{ + uacpi_namespace_node *n = 0; + if(path[0] == 0 || + uacpi_namespace_node_find(0, path, &n) != UACPI_STATUS_OK) + return 0; + return n; +} + +static struct dwi2c * +controller(const char *path, uint32 speed) +{ + uacpi_namespace_node *node = node_at(path); + struct found f; + struct dwi2c *c; + int i; + + if(node == 0 || resources(node, &f) < 0 || f.mem == 0){ + cprintf("i2c: no registers for controller %s\n", path); + return 0; + } + for(i = 0; i < nctl; i++) + if(ctls[i].phys == f.mem) + return &ctls[i]; + if(nctl == NCTL) + return 0; + uacpi_execute(node, "_PS0", 0); + c = &ctls[nctl]; + memset(c, 0, sizeof(*c)); + c->phys = f.mem; + if(dw_init(c, node, speed) < 0) + return 0; + nctl++; + cprintf("i2c: %s at 0x%lx, %s mode, FIFOs %d/%d\n", path, c->phys, + c->fast ? "fast" : "standard", c->txdepth, c->rxdepth); + return c; +} + +// --------------------------------------------------------------------------- +// HID over I2C +// --------------------------------------------------------------------------- + +#define NI2CHID 4 + +struct i2chid { + struct dwi2c *c; + int addr; + uint32 cmdreg, inreg, maxin; + volatile uint32 *pin; // the interrupt pin's register, or 0 + int active_low; + void *hid; + uchar *buf; // one page, for input reports + char name[8]; +}; + +static struct i2chid devs[NI2CHID]; +static int ndev; + +static int +reg_write(struct i2chid *d, const uchar *w, int n) +{ + return dw_xfer(d->c, d->addr, w, n, 0, 0); +} + +static int +reg_read(struct i2chid *d, uint32 reg, uchar *r, int n) +{ + uchar w[2] = { reg & 0xFF, reg >> 8 }; + return dw_xfer(d->c, d->addr, w, 2, r, n); +} + +// SET_POWER and RESET, the two commands needed: written to the command +// register as (register, opcode byte, power state / report byte). +static int +command(struct i2chid *d, int opcode, int arg) +{ + uchar w[4] = { d->cmdreg & 0xFF, d->cmdreg >> 8, arg, opcode }; + return reg_write(d, w, 4); +} + +#define OP_RESET 0x01 +#define OP_SET_POWER 0x08 + +static void +wait_ticks(uint n) +{ + acquire(&tickslock); + uint start = ticks; + while(ticks - start < n) + sleep(&ticks, &tickslock); + release(&tickslock); +} + +// AMD's GPIO controller: one 32-bit register per pin, its level in bit 16. +#define AMD_PIN_STS (1u << 16) + +static int +asserted(struct i2chid *d) +{ + int high = (*d->pin & AMD_PIN_STS) != 0; + return d->active_low ? !high : high; +} + +static volatile uint32 * +interrupt_pin(const struct found *f) +{ + static const char *const amd[] = { "AMDI0030", "AMD0030", 0 }; + uacpi_namespace_node *node = node_at(f->gpioctl); + struct found g; + + if(!f->gpio || node == 0 || !uacpi_device_matches_pnp_id(node, amd)) + return 0; + if(resources(node, &g) < 0 || g.mem == 0) + return 0; + return (volatile uint32 *)((char *)ioremap(g.mem, 0x1000) + f->pin * 4); +} + +// One input report: two bytes of length, length included, then the report. +// A length of 0 is the device saying it has nothing, or acknowledging a +// reset. +static int +read_input(struct i2chid *d) +{ + int len; + + if(dw_xfer(d->c, d->addr, 0, 0, d->buf, d->maxin) < 0) + return -1; + len = d->buf[0] | (d->buf[1] << 8); + if(len <= 2 || len > (int)d->maxin) + return 0; + hid_attached_input(d->hid, d->buf + 2, len - 2); + return 1; +} + +static uacpi_iteration_decision +probe(void *user, uacpi_namespace_node *node, uacpi_u32 depth) +{ + struct i2chid *d; + struct found f; + uchar desc[30], *rdesc; + uint32 descreg, rlen, rreg; + int i; + + (void)user; + (void)depth; + if(ndev == NI2CHID) + return UACPI_ITERATION_DECISION_BREAK; + d = &devs[ndev]; + memset(d, 0, sizeof(*d)); + memmove(d->name, uacpi_namespace_node_name(node).text, 4); + + if(resources(node, &f) < 0 || !f.i2c){ + cprintf("i2c-hid: %s has no I2C connection\n", d->name); + return UACPI_ITERATION_DECISION_CONTINUE; + } + if(hid_desc_register(node, &descreg) < 0){ + cprintf("i2c-hid: %s: _DSM gave no descriptor register\n", d->name); + return UACPI_ITERATION_DECISION_CONTINUE; + } + if((d->c = controller(f.bus, f.speed)) == 0) + return UACPI_ITERATION_DECISION_CONTINUE; + d->addr = f.addr; + uacpi_execute(node, "_PS0", 0); + + if(reg_read(d, descreg, desc, sizeof(desc)) < 0 || + (desc[0] | desc[1] << 8) != 30){ + cprintf("i2c-hid: %s at 0x%x does not answer\n", d->name, d->addr); + return UACPI_ITERATION_DECISION_CONTINUE; + } + rlen = desc[4] | desc[5] << 8; + rreg = desc[6] | desc[7] << 8; + d->inreg = desc[8] | desc[9] << 8; + d->maxin = desc[10] | desc[11] << 8; + d->cmdreg = desc[16] | desc[17] << 8; + if(d->maxin < 3 || d->maxin > PGSIZE || rlen == 0 || rlen > PGSIZE){ + cprintf("i2c-hid: %s: unusable descriptor\n", d->name); + return UACPI_ITERATION_DECISION_CONTINUE; + } + d->pin = interrupt_pin(&f); + d->active_low = f.active_low; + + command(d, OP_SET_POWER, 0); // 0 = on + wait_ticks(2); + command(d, OP_RESET, 0); + wait_ticks(10); + d->buf = (uchar *)kalloc(); + rdesc = (uchar *)kalloc(); + if(d->buf == 0 || rdesc == 0) + return UACPI_ITERATION_DECISION_BREAK; + // The reset is acknowledged with an empty report. Read it, and anything + // else already waiting, so the first real report is not behind it. + for(i = 0; i < 4 && (d->pin == 0 || asserted(d)); i++) + if(dw_xfer(d->c, d->addr, 0, 0, d->buf, d->maxin) < 0) + break; + + if(reg_read(d, rreg, rdesc, rlen) < 0){ + cprintf("i2c-hid: %s: report descriptor unreadable\n", d->name); + kfree((char *)rdesc); + return UACPI_ITERATION_DECISION_CONTINUE; + } + char what[24] = "i2c-hid "; + memmove(what + 8, d->name, 4); + d->hid = hid_attach(rdesc, rlen, what); + kfree((char *)rdesc); + if(d->hid == 0){ + cprintf("i2c-hid: %s is neither a keyboard nor a pointer\n", d->name); + return UACPI_ITERATION_DECISION_CONTINUE; + } + cprintf("i2c-hid: %s: %04x:%04x at 0x%x, %s\n", d->name, + desc[20] | desc[21] << 8, desc[22] | desc[23] << 8, d->addr, + d->pin ? "interrupt pin watched" : "polled"); + ndev++; + return UACPI_ITERATION_DECISION_CONTINUE; +} + +static void +i2c_hid_thread(void) +{ + static const char *const ids[] = { "PNP0C50", "ACPI0C50", 0 }; + int i, n; + + uacpi_find_devices_at(uacpi_namespace_root(), ids, probe, 0); + if(ndev == 0) + for(;;) + wait_ticks(1000000); + + for(;;){ + wait_ticks(1); + for(i = 0; i < ndev; i++){ + struct i2chid *d = &devs[i]; + if(d->pin == 0){ + read_input(d); + continue; + } + // Everything waiting, up to a bound so that a stuck line cannot keep + // the other devices from their turn. + for(n = 0; n < 16 && asserted(d); n++) + if(read_input(d) < 0) + break; + } + } +} + +void +i2c_hid_start(void) +{ + if(acpi_rsdp_phys() == 0) + return; + if(kthread_create(i2c_hid_thread, "i2chid") < 0) + cprintf("i2c-hid: no thread; touchpads will not work\n"); +} diff --git a/kernel/arch/x86_64/trapasm.S b/kernel/arch/x86_64/trapasm.S index e5dfed9..1e6ab24 100644 --- a/kernel/arch/x86_64/trapasm.S +++ b/kernel/arch/x86_64/trapasm.S @@ -1,3 +1,4 @@ +#include "core/param.h" /* KSTACKSIZE */ # trapasm.S — kernel/user entry/exit with FS_BASE swapping for TLS # # The kernel uses __thread struct cpu *cpu and __thread struct proc *proc, @@ -209,7 +210,13 @@ syscall_entry: movq %rax, %fs:(8) # save rax in per-CPU scratch slot movq %gs:24, %rax # proc pointer (PERCPU_PROC) movq 0x10(%rax), %rax # proc->kstack (offset 16) - addq $(4096-16), %rax # point to tf->rsp slot + # The top of the whole stack, where allocproc() put the trapframe and + # where a trap from user mode lands (TSS rsp0). This was $(4096-16) from + # when the stack was one page, and stayed that when it became two: every + # syscall ran in the bottom 4 KB with the top 4 KB unused, and a deep + # enough read -- startx exec'ing through a SCFF chunk unpack -- ran + # through the canary at kstack[0]. + addq $(KSTACKSIZE-16), %rax # point to tf->rsp slot movq %rsp, (%rax) # save user rsp movq %rax, %rsp # switch to kernel stack movq %fs:(8), %rax # restore rax diff --git a/kernel/core/console.c b/kernel/core/console.c index 099d000..2e97a19 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -442,7 +442,8 @@ erase_one(struct vt *v) // Keyboard input // --------------------------------------------------------------------------- -// Ctrl+Alt+Shift+F1..F6 switch terminals. kbd.c reports them as +// Alt+F1..F6, Ctrl+Alt+F1..F6 and the settable chord (AltGr+F1..F6 by +// default) switch terminals. kbd.c reports them as // KBD_VT_SWITCH+n so that the // decision of what a chord *means* is made here, where the terminals are, // rather than in a driver that should not know they exist. diff --git a/kernel/core/main.c b/kernel/core/main.c index 475e171..dd3c0a2 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -12,6 +12,7 @@ #include "core/klog.h" #include "dev/usb.h" #include "dev/acpi.h" +#include "dev/hid.h" #include "arch/vmap.h" #include "core/hwinfo.h" @@ -391,6 +392,9 @@ kmain(uint64 fb_addr) usb_hotplug_start(); // Where AML runs for the SCI: the power button, GPEs, notifications. acpi_thread_start(); + // Laptop touchpads, on the chipset's I2C bus; found through ACPI, so after + // the namespace is loaded, and polled from a thread of their own. + i2c_hid_start(); // The debugger's own thread, for the same reason netinit's comes after // sinit: kthread_create needs a usable process table. From here on Ctrl-G // on the console answers, with no filesystem mounted and nothing in @@ -400,6 +404,7 @@ kmain(uint64 fb_addr) random_init(); netuser_init(); dbg_thread_start(); + alarm_thread_start(); if (boot_verbose) cprintf("verbose boot: dbg thread started\n"); // The greeting is not printed here. diff --git a/kernel/dev/fb.c b/kernel/dev/fb.c index 0b9412a..0a62efd 100644 --- a/kernel/dev/fb.c +++ b/kernel/dev/fb.c @@ -230,6 +230,13 @@ fb_ioctl(int request, addr_t arg) // blanking does too, as Linux's own drivers without the hook do. case FBIOBLANK: return 0; + + // A palette, on a framebuffer that is always true-colour. Linux's drivers + // keep the values for the console's sixteen colours and succeed; refusing + // made Xorg's fbdev driver log an error for every entry it loads, 256 of + // them at each start. + case FBIOPUTCMAP: + return 0; } return -ENOTTY; diff --git a/kernel/dev/input.c b/kernel/dev/input.c index 78c8801..228e6c2 100644 --- a/kernel/dev/input.c +++ b/kernel/dev/input.c @@ -156,6 +156,7 @@ input_sync(int dev) return; input_report(dev, EV_SYN, SYN_REPORT, 0); wakeup(&idev[dev]); + poll_kick(); // Xorg polls its devices; a mouse moves at the next tick otherwise } int diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index 6acae66..f7cc3b5 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -152,6 +152,16 @@ kbdgetc(void) if (data == SCAN_EQUALS) return KBD_VT_NEXT; } + // And Linux's own: Alt+F1..F6 and Ctrl+Alt+F1..F6, whatever the chord is set + // to. These are what anyone who has used Linux presses, on a real machine, + // and nothing reached them: SchOS answered only AltGr+F-keys, which no one + // thinks to try. Under X the terminal is in VT_PROCESS, console_vt_hotkey() + // lets the keys through, and X switches with them itself -- as on Linux. + if (data >= SCAN_F1 && data < SCAN_F1 + NVT && + ((shift & (CTL | SHIFT | ALT | RALT)) == ALT || + (shift & (CTL | SHIFT | ALT | RALT)) == (CTL | ALT))) + return KBD_VT_SWITCH + (data - SCAN_F1); + cp = keymap_lookup(data, shift); if (cp == 0) return 0; diff --git a/kernel/dev/ramdisk.c b/kernel/dev/ramdisk.c index 055ad99..e988716 100644 --- a/kernel/dev/ramdisk.c +++ b/kernel/dev/ramdisk.c @@ -14,6 +14,28 @@ // // Enabled by building with RAMROOT=1. Without it this file still compiles, the // device reports zero size, and the kernel boots from the IDE disk as before. +// +// The device is larger than the image. The image is only what the root holds, +// with a little slack; past its end the device goes on to half of the memory +// free at mount time, and those blocks are pages taken from kalloc the first +// time each is written -- a block nobody has written reads as zeros and costs +// nothing. SchFS grows the volume to the device's size when it mounts (see +// grow_to in kernel/fs/schfs/vol.c), so `apk add` on a live system has as +// much room as the machine has memory to give, rather than whatever empty +// space was built into the ISO. The ISO used to carry 60 MB of zeros for it. +// +// Pages are not given back when SchFS frees a block: there is no discard. +// A freed block is reused by the next write, so the cost is the high-water +// mark, not a leak. +// +// **A compressed image.** The blob can also be SCFF (include/fs/scff.h): the +// image cut into chunks, each compressed with zstd. Then no block is served in +// place. The page directory covers the image too, and a block with no page +// has not been unpacked yet: reading it decompresses its whole chunk, and +// every block of that chunk nobody has written gets a page of its own. The +// kernel and the ISO are the compressed size -- the 360 MB root with Xorg in +// it is 104 MB -- and the root costs memory only for what has been read or +// written. A block of zeros points at one shared zero page and costs nothing. #include "core/types.h" #include "core/defs.h" @@ -24,6 +46,8 @@ #include "core/sleeplock.h" #include "fs/fs.h" #include "fs/buf.h" +#include "fs/scff.h" +#include "../scff/scff_zstd.h" #ifdef RAMDISK_ROOT // Supplied by `ld -r -b binary` over build/fs-ram.img, renamed by objcopy. @@ -33,15 +57,63 @@ extern uchar _binary_ramdisk_size[]; static uchar *rd_base; static uint64 rd_size; // bytes -static uint rd_blocks; // rd_size / BSIZE +static uint rd_blocks; // blocks in the image +static uint rd_first; // the first block with a page: rd_blocks, or 0 for SCFF + +// SCFF only. rd_zlock is held across a chunk's decompression, and makes +// rd_chunk -- the one chunk-sized buffer -- its holder's. +static struct scff_header *rd_scff; +static uint64 *rd_index; +static struct sleeplock rd_zlock; +static uchar rd_chunk[SCFF_CHUNK_MAX] __attribute__((aligned(PGSIZE))); +static uchar rd_zero[BSIZE] __attribute__((aligned(PGSIZE))); +static uint64 rd_unpacked; // chunks decompressed + +// rd_dir[i] is a page of pointers, entry j the page holding block +// rd_first + i*RD_PTRS + j, or 0 for one never written. 32768 pages of +// 512 pointers reach 64 GB, which is as far as half of any machine this runs +// on is likely to go, and costs 256 KB of pointers here. +#define RD_PTRS (PGSIZE / sizeof(uchar *)) +#define RD_DIRS 32768 +static uchar **rd_dir[RD_DIRS]; +static uint64 rd_total; // blocks, image and the rest; 0 until sized +static uint64 rd_pages; // pages taken for blocks past the image +static struct spinlock rd_lock; void ramdiskinit(void) { + initlock(&rd_lock, "ramdisk"); #ifdef RAMDISK_ROOT rd_base = _binary_ramdisk_start; rd_size = (uint64)_binary_ramdisk_size; rd_blocks = (uint)(rd_size / BSIZE); + rd_first = rd_blocks; + + if(rd_size >= sizeof(struct scff_header) && memcmp(rd_base, SCFF_MAGIC, 4) == 0){ + struct scff_header *h = (struct scff_header *)rd_base; + uint64 chunks = h->chunk_bytes ? (h->image_bytes + h->chunk_bytes - 1) / h->chunk_bytes : 0; + if(h->version != SCFF_VERSION || h->codec != SCFF_CODEC_ZSTD || + h->chunk_bytes < BSIZE || h->chunk_bytes > SCFF_CHUNK_MAX || + (h->chunk_bytes & (h->chunk_bytes - 1)) || h->image_bytes % BSIZE || + chunks != h->nchunks || h->stored_bytes > rd_size || + h->index_off + (h->nchunks + 1) * sizeof(uint64) > rd_size) + panic("ramdiskinit: the SCFF header does not describe this image"); + rd_index = (uint64 *)(rd_base + h->index_off); + for(uint i = 0; i < h->nchunks; i++) + if(rd_index[i] > rd_index[i + 1] || rd_index[i + 1] > h->stored_bytes || + rd_index[i + 1] - rd_index[i] > h->chunk_bytes) + panic("ramdiskinit: the SCFF index is damaged"); + if(scff_zstd_init() < 0) + panic("ramdiskinit: no room for the zstd decoder"); + initsleeplock(&rd_zlock, "scff"); + rd_scff = h; + rd_blocks = (uint)(h->image_bytes / BSIZE); + rd_first = 0; + cprintf("ramdisk: SCFF, %u MB in %u MB, %u chunks of %u KB\n", + (uint)(h->image_bytes >> 20), (uint)(rd_size >> 20), + h->nchunks, h->chunk_bytes >> 10); + } if(rd_blocks == 0) panic("ramdiskinit: embedded image is empty"); @@ -51,8 +123,9 @@ ramdiskinit(void) if(rd_blocks < 2) panic("ramdiskinit: embedded image smaller than a superblock"); - cprintf("ramdisk: %u blocks (%u KB) at %p\n", - rd_blocks, (uint)(rd_size / 1024), rd_base); + if(!rd_scff) + cprintf("ramdisk: %u blocks (%u KB) at %p\n", + rd_blocks, (uint)(rd_size / 1024), rd_base); #else rd_base = 0; rd_size = 0; @@ -66,6 +139,115 @@ ramdisk_present(void) return rd_blocks != 0; } +// The device's size in blocks, fixed the first time anyone asks -- which is +// SchFS mounting the root, after every page has reached the allocator. Asked +// earlier, during ramdiskinit, the free count would be the first few +// megabytes kinit1 handed over and nothing more. +uint64 +ramdisk_blocks(void) +{ + if (rd_blocks == 0) + return 0; + acquire(&rd_lock); + if (rd_total == 0) { + uint64 extra = kalloc_free_bytes() / 2 / BSIZE; + uint64 room = (uint64)RD_DIRS * RD_PTRS - (rd_blocks - rd_first); + if (extra > room) + extra = room; + rd_total = rd_blocks + extra; + cprintf("ramdisk: %u MB image, %u MB of memory to grow into\n", + (uint)(rd_size >> 20), (uint)(extra * BSIZE >> 20)); + } + release(&rd_lock); + return rd_total; +} + +// The page behind a block, made when `make` says so. One that is only the +// shared zero page gets a page of its own before anything writes to it. +static uchar * +rd_page(uint64 blk, int make) +{ + uint64 i = (blk - rd_first) / RD_PTRS, j = (blk - rd_first) % RD_PTRS; + uchar *pg = 0; + + acquire(&rd_lock); + if (rd_dir[i] == 0 && make) { + if ((rd_dir[i] = (uchar **)kalloc()) != 0) + memset(rd_dir[i], 0, PGSIZE); + } + if (rd_dir[i]) { + pg = rd_dir[i][j]; + if ((pg == 0 || pg == rd_zero) && make && (pg = (uchar *)kalloc()) != 0) { + if (rd_dir[i][j] == rd_zero) + memset(pg, 0, BSIZE); + rd_dir[i][j] = pg; + rd_pages++; + } + } + release(&rd_lock); + if (make && pg == 0) + panic("ramdisk: out of memory for the live root"); + return pg; +} + +// Give every block of SCFF chunk c that has no page one: its contents, or the +// zero page. Blocks already there -- written since boot, or unpacked by +// whoever held the lock before us -- are left as they are. +static void +rd_unpack(uint c) +{ + uint per = rd_scff->chunk_bytes / BSIZE; + uint64 first = (uint64)c * per; + uint n = rd_blocks - first < per ? (uint)(rd_blocks - first) : per; + uint64 len = rd_index[c + 1] - rd_index[c]; + uchar *src = rd_base + rd_index[c]; + uint64 want = (uint64)n * BSIZE; + uchar *data = rd_chunk; + + acquiresleep(&rd_zlock); + if (len == 0) { + data = 0; // all zeros + } else if (len == want) { + data = src; // stored as it is + } else { + long got = scff_zstd_decompress(rd_chunk, sizeof(rd_chunk), src, len); + if (got != (long)want) + panic("ramdisk: an SCFF chunk does not decompress"); + rd_unpacked++; + } + + for (uint k = 0; k < n; k++) { + uint64 blk = first + k, i = blk / RD_PTRS, j = blk % RD_PTRS; + uchar *b = data ? data + (uint64)k * BSIZE : 0; + int zero = 1; + if (b) + for (uint w = 0; w < BSIZE / 8 && zero; w++) + zero = ((uint64 *)b)[w] == 0; + + uchar *pg = zero ? rd_zero : (uchar *)kalloc(); + if (pg == 0) + panic("ramdisk: out of memory for the live root"); + if (!zero) + memmove(pg, b, BSIZE); + + acquire(&rd_lock); + if (rd_dir[i] == 0 && (rd_dir[i] = (uchar **)kalloc()) != 0) + memset(rd_dir[i], 0, PGSIZE); + if (rd_dir[i] == 0) + panic("ramdisk: out of memory for the live root"); + if (rd_dir[i][j] == 0) { + rd_dir[i][j] = pg; + if (!zero) + rd_pages++; + pg = 0; + } + release(&rd_lock); + if (pg && pg != rd_zero) + kfree((char *)pg); // someone was there first + } + releasesleep(&rd_zlock); +} + // Sync buf with the in-memory image. Mirrors iderw()'s contract: on a read, // fill b->data and set B_VALID; on a write, copy out and clear B_DIRTY. void @@ -77,8 +259,25 @@ ramdiskrw(struct buf *b) panic("ramdiskrw: nothing to do"); if(rd_base == 0) panic("ramdiskrw: no ramdisk"); - if(b->blockno >= rd_blocks) - panic("ramdiskrw: block out of range"); + if(b->blockno >= rd_first){ + if(b->blockno >= ramdisk_blocks()) + panic("ramdiskrw: block out of range"); + uchar *pg = rd_page(b->blockno, (b->flags & B_DIRTY) != 0); + if(pg == 0 && rd_scff && b->blockno < rd_blocks && !(b->flags & B_DIRTY)){ + rd_unpack((uint)(b->blockno / (rd_scff->chunk_bytes / BSIZE))); + pg = rd_page(b->blockno, 0); + } + if(b->flags & B_DIRTY){ + memmove(pg, b->data, BSIZE); + b->flags &= ~B_DIRTY; + } else if(pg){ + memmove(b->data, pg, BSIZE); + } else { + memset(b->data, 0, BSIZE); // never written + } + b->flags |= B_VALID; + return; + } uchar *p = rd_base + (uint64)b->blockno * BSIZE; diff --git a/kernel/drivers/usb/hid/usb_hid.c b/kernel/drivers/usb/hid/usb_hid.c index 569697e..94cea98 100644 --- a/kernel/drivers/usb/hid/usb_hid.c +++ b/kernel/drivers/usb/hid/usb_hid.c @@ -25,6 +25,9 @@ #include "core/dma.h" #include "dev/usb.h" #include "dev/input.h" +#include "dev/hid.h" +#include "dev/kbd.h" +#include "dev/vt.h" #define NHID 8 @@ -47,6 +50,11 @@ struct hid { // report, for a mouse whose descriptor could not be used. int boot; int report_id; // 0 when reports carry no ID byte + // 1 + the only report ID to take pointer fields from, 0 for any; and + // 1 + the first report ID with a relative X. A touchpad describes both a + // mouse report and a multi-finger one with absolute coordinates, and only + // the mouse report makes sense as a pointer; see hid_attach(). + int only_id1, rel_id1; struct hfield x, y, wheel, btn[HID_NBTN]; // A keyboard in report protocol: where its keys are, so that each report @@ -106,7 +114,7 @@ static const uchar hid_shifted[0x39] = { // which means the entire line discipline is reused rather than reimplemented. #define KQSIZE 64 static struct { - uchar buf[KQSIZE]; + int buf[KQSIZE]; // characters, and KBD_* codes past KBD_SPECIAL uint r, w; } kq; @@ -149,6 +157,32 @@ usb_kbd_getc(void) } // Was this usage in that report? +// The keys past the character table, as the console names them. The PS/2 +// path has always produced these (kernel/dev/kbd.c); this one skipped every +// usage from 0x39 up, so on a USB keyboard the console had no arrows, no +// Home/End/PgUp/PgDn/Delete, and no way to switch terminals. +static int +hid_special(uchar usage, uchar mods) +{ + // F1..F6 with Alt, AltGr or Ctrl+Alt: switch terminals, as on Linux. + if (usage >= 0x3A && usage < 0x3A + NVT && (mods & HID_MOD_ALT) && + !(mods & HID_MOD_SHIFT)) + return KBD_VT_SWITCH + (usage - 0x3A); + switch (usage) { + case 0x49: return KBD_INS; + case 0x4A: return KBD_HOME; + case 0x4B: return KBD_PGUP; + case 0x4C: return KBD_DEL; + case 0x4D: return KBD_END; + case 0x4E: return KBD_PGDN; + case 0x4F: return KBD_RT; + case 0x50: return KBD_LF; + case 0x51: return KBD_DN; + case 0x52: return KBD_UP; + } + return 0; +} + static int was_down(const uchar *prev, uchar usage) { @@ -222,18 +256,23 @@ keyboard_report(struct hid *h, const uchar *rep, int len) for(i = 2; i < 8; i++){ uchar usage = rep[i]; - if(usage == 0 || usage >= 0x39) + if(usage == 0) continue; if(was_down(h->prev, usage)) continue; // still held from last time, not a new press - c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage]; - if(c == 0) - continue; + if(usage >= 0x39){ + if((c = hid_special(usage, mods)) == 0) + continue; + } else { + c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage]; + if(c == 0) + continue; + } // Ctrl folds a letter down into its control character, which is how ^C // and ^D reach the line discipline at all. - if(mods & HID_MOD_CTRL){ + if((mods & HID_MOD_CTRL) && c < KBD_SPECIAL){ if(c >= 'a' && c <= 'z') c = c - 'a' + 1; else if(c >= 'A' && c <= 'Z') @@ -242,6 +281,8 @@ keyboard_report(struct hid *h, const uchar *rep, int len) continue; } kq_put(c); + if(c >= KBD_VT_SWITCH && c < KBD_VT_SWITCH + NVT) + continue; // a held chord switches once // The newest key is the one that repeats, as on any keyboard. acquire(&kqlock); @@ -308,6 +349,10 @@ take_field(struct hid *h, uint32 usage, int bit, int size, int lmin, int lmax, struct hfield *f = 0; int page = usage >> 16, u = usage & 0xFFFF; + if(page == PAGE_DESKTOP && u == U_X && rel && h->rel_id1 == 0) + h->rel_id1 = id + 1; + if(h->only_id1 && id + 1 != h->only_id1) + return; if(page == PAGE_DESKTOP && u == U_X) f = &h->x; else if(page == PAGE_DESKTOP && u == U_Y) f = &h->y; else if(page == PAGE_DESKTOP && u == U_WHEEL) f = &h->wheel; @@ -595,12 +640,8 @@ pointer_report(struct hid *h, const uchar *rep, int len) // --------------------------------------------------------------------------- static void -hid_complete(struct usb_dev *dev, void *buf, int len) +hid_input(struct hid *h, const uchar *buf, int len) { - struct hid *h = dev->drvdata; - - if(h == 0) - return; // One interface can be both: a wireless receiver describes its keyboard // and its mouse as two report IDs of one descriptor. if(h->rkbd) @@ -611,6 +652,70 @@ hid_complete(struct usb_dev *dev, void *buf, int len) pointer_report(h, buf, len); } +static void +hid_complete(struct usb_dev *dev, void *buf, int len) +{ + struct hid *h = dev->drvdata; + + if(h != 0) + hid_input(h, buf, len); +} + +static struct hid * +hid_slot(void) +{ + int i; + + if(!kqinit){ + initlock(&kqlock, "usbkbd"); + kqinit = 1; + } + for(i = 0; i < NHID; i++) + if(!hids[i].in_use){ + memset(&hids[i], 0, sizeof(hids[i])); + return &hids[i]; + } + return 0; +} + +// A HID device on some other transport -- I2C, for a laptop's touchpad -- +// handing over its report descriptor. Keyboards and pointers come out of it +// exactly as from USB. A descriptor with a relative pointer is driven +// through that report alone, even if an absolute one comes first: that is a +// touchpad's mouse report, which it sends until told to report fingers. +void * +hid_attach(const uchar *desc, int n, const char *what) +{ + struct hid *h = hid_slot(); + int rel; + + if(h == 0) + return 0; + if(parse_report(h, desc, n) < 0) + return 0; + rel = h->rel_id1; + if(h->x.ok && !h->x.rel && rel){ + memset(h, 0, sizeof(*h)); + h->only_id1 = rel; + if(parse_report(h, desc, n) < 0) + return 0; + } + h->proto = !h->x.ok ? USB_HID_PROTO_KEYBOARD : + h->x.rel ? USB_HID_PROTO_MOUSE : USB_HID_PROTO_TABLET; + h->in_use = 1; + cprintf("%s: %s%s\n", what, + h->proto == USB_HID_PROTO_KEYBOARD ? "keyboard" : + h->proto == USB_HID_PROTO_MOUSE ? "pointer" : "absolute pointer", + h->rkbd && h->x.ok ? " and keyboard" : ""); + return h; +} + +void +hid_attached_input(void *h, const uchar *rep, int len) +{ + hid_input(h, rep, len); +} + static int hid_request(struct usb_dev *dev, int request, int value) { @@ -686,18 +791,10 @@ hid_probe(struct usb_dev *dev) cprintf("usb-hid: no interrupt IN endpoint; nothing to read\n"); return -1; } - if(!kqinit){ - initlock(&kqlock, "usbkbd"); - kqinit = 1; - } - - for(i = 0; i < NHID; i++) - if(!hids[i].in_use){ h = &hids[i]; break; } - if(h == 0){ + if((h = hid_slot()) == 0){ cprintf("usb-hid: %d devices already; this one is not used\n", NHID); return -1; } - memset(h, 0, sizeof(*h)); if(boot && proto == USB_HID_PROTO_KEYBOARD){ // Ask for the fixed layout explicitly. A device that supports the boot diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index befd9a3..43c27c9 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -83,6 +83,11 @@ static const struct devnode devnodes[] = { was one terminal. Minor 10 resolves through proc->ctty; see vt_for_minor() in kernel/core/console.c. */ { "tty", 1, 10, D_ROOT }, + /* The terminal on screen, as on Linux. Xorg opens it to find a free VT and + switch to it, and without one ran on no VT at all: the console went on + drawing into the framebuffer over the X screen. Any minor that is not a + numbered terminal or the controlling one resolves to vt_current(). */ + { "tty0", 1, 0, D_ROOT }, { "tty1", 1, 11, D_ROOT }, { "tty2", 1, 12, D_ROOT }, { "tty3", 1, 13, D_ROOT }, diff --git a/kernel/fs/exec.c b/kernel/fs/exec.c index 2744fb8..98edb15 100644 --- a/kernel/fs/exec.c +++ b/kernel/fs/exec.c @@ -574,11 +574,15 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev) // first delivered signal jump to a stale address in the new image. // mmap_next likewise belongs to the old address space; left alone it // could hand out mmap ranges overlapping the new image's layout. + // Ignored signals stay ignored, and the mask is inherited (execve(2)): + // neither points into the old image. `nohup` and every program that ignores + // SIGPIPE before running a helper depend on it. memset(myproc()->vma, 0, sizeof(myproc()->vma)); - memset(myproc()->sig_handler, 0, sizeof(myproc()->sig_handler)); + for (int i = 0; i < 32; i++) + if (myproc()->sig_handler[i] != 1) + myproc()->sig_handler[i] = 0; memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer)); myproc()->fs_base = 0; - myproc()->signal_mask = 0; myproc()->pending_signals = 0; myproc()->in_signal = 0; myproc()->sigsusp_active = 0; @@ -615,11 +619,11 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev) myproc()->tf->rcx = entry; myproc()->tf->rsp = sp; - // syscall_entry saves user RSP at kstack+PGSIZE-16 (not in the trapframe). + // syscall_entry saves user RSP at kstack+KSTACKSIZE-16 (not in the trapframe). // sysretq reads RSP from that slot, so update it to the new stack pointer. // Without this, sysretq returns with the old parent's RSP, causing lksh to // start with an invalid/wrong stack pointer. - *(uint64*)((char*)myproc()->kstack + PGSIZE - 16) = sp; + *(uint64*)((char*)myproc()->kstack + KSTACKSIZE - 16) = sp; // Userspace is running now, so the splash has done its job and the display // becomes a terminal. Only the first exec does anything; every later one diff --git a/kernel/fs/file.c b/kernel/fs/file.c index a696823..382da87 100644 --- a/kernel/fs/file.c +++ b/kernel/fs/file.c @@ -24,6 +24,7 @@ struct { struct file *free; struct kmem_cache *more; uint n; + uint64 serial; // the last ino handed out } ftable; void @@ -63,6 +64,8 @@ filealloc(void) ftable.n++; } f->ref = 1; + f->ino = ++ftable.serial; + f->path = 0; FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1 // Cleared here rather than left to the caller: every creator sets // readable/writable/off, but only open() sets nonblock, so a struct file @@ -101,12 +104,16 @@ fileclose(struct file *f) lock_close_file(f); ff = *f; f->ref = 0; + f->path = 0; f->type = FD_NONE; f->fnext = ftable.free; ftable.free = f; FSSTAT_DROP(fsstat.file_used); // 1 -> 0 release(&ftable.lock); + if(ff.path) + kmfree(ff.path); + if(ff.type == FD_MEMFD) shm_detach(ff.memfd_seg); // the last mapper frees the pages else if(ff.type == FD_EVENT) @@ -129,6 +136,42 @@ fileclose(struct file *f) } } +// What /proc//fd/N points at, in Linux's spelling: the path, or +// "socket:[ino]", "pipe:[ino]", "anon_inode:[eventpoll]". Returns the length. +// ftable.lock is what keeps `path` alive: fileclose() takes it from the file +// under the lock and frees it after. +int +file_describe(struct file *f, char *buf, int cap) +{ + const char *pre = 0, *s = 0; + int n = 0; + + acquire(&ftable.lock); + if(f->ref > 0){ + switch(f->type){ + case FD_INODE: s = f->path; if(!s) pre = "inode:["; break; + case FD_SOCKET: pre = "socket:["; break; + case FD_PIPE: pre = "pipe:["; break; + case FD_MEMFD: s = "/memfd: (deleted)"; break; + case FD_EVENT: s = ev_kind_name(f->ev); break; + default: break; + } + } + if(pre){ + char num[24]; + int k = 0; + uint64 v = f->type == FD_INODE && f->ip ? f->ip->inum : f->ino; + do { num[k++] = '0' + v % 10; v /= 10; } while(v); + while(*pre && n < cap) buf[n++] = *pre++; + while(k && n < cap) buf[n++] = num[--k]; + if(n < cap) buf[n++] = ']'; + } else if(s){ + while(*s && n < cap) buf[n++] = *s++; + } + release(&ftable.lock); + return n; +} + int filestat(struct file *f, struct stat *st) { diff --git a/kernel/fs/pipe.c b/kernel/fs/pipe.c index f5c67a8..62c5189 100644 --- a/kernel/fs/pipe.c +++ b/kernel/fs/pipe.c @@ -11,24 +11,40 @@ #include "uapi/posix.h" #include "uapi/errno.h" -#define PIPESIZE 512 +// 64 KB, as on Linux, in pages taken the first time each is written. +// +// It was 512 bytes in the struct. That is xv6's size, and it made every pipe a +// handoff per half kilobyte: busybox's wget does TLS in a helper process that +// talks to it over pipes, a TLS record is 16 KB, and an HTTPS download that +// should have taken nine seconds was still going after a minute and a half. A +// shell pipeline that only ever moves a line still costs one page. +#define PIPESIZE 65536 +#define PIPE_PAGES (PIPESIZE / PGSIZE) +#define PIPE_BUF 4096 // writes up to this size are not interleaved struct pipe { struct spinlock lock; - char data[PIPESIZE]; + char *page[PIPE_PAGES]; uint nread; // number of bytes read uint nwrite; // number of bytes written int readopen; // read fd is still open int writeopen; // write fd is still open }; -// A pipe is about 560 bytes and used to come from kalloc(), which is a whole -// 4096-byte page -- so every `|` in a shell command burned three and a half -// kilobytes it never touched. This is what kmem_cache exists for, and the -// cache is created on first use rather than at boot so a kernel that never -// opens a pipe holds no page for one. +// The struct is small and comes from a cache; the buffer is pages. The cache +// is created on first use so a kernel that never opens a pipe holds no page +// for one. static struct kmem_cache *pipe_cache; +static void +pipe_free(struct pipe *p) +{ + for(int i = 0; i < PIPE_PAGES; i++) + if(p->page[i]) + kfree(p->page[i]); + kmem_cache_free(pipe_cache, p); +} + int pipealloc(struct file **f0, struct file **f1) { @@ -42,6 +58,7 @@ pipealloc(struct file **f0, struct file **f1) goto bad; if((p = kmem_cache_alloc(pipe_cache)) == 0) goto bad; + memset(p->page, 0, sizeof(p->page)); p->readopen = 1; p->writeopen = 1; p->nwrite = 0; @@ -55,6 +72,7 @@ pipealloc(struct file **f0, struct file **f1) (*f1)->readable = 0; (*f1)->writable = 1; (*f1)->pipe = p; + (*f1)->ino = (*f0)->ino; // one pipe, as /proc//fd names it return 0; //PAGEBREAK: 20 @@ -81,9 +99,10 @@ pipeclose(struct pipe *p, int writable) } if(p->readopen == 0 && p->writeopen == 0){ release(&p->lock); - kmem_cache_free(pipe_cache, p); + pipe_free(p); } else release(&p->lock); + poll_kick(); // a poller sees the hangup now, not at the tick } //PAGEBREAK: 40 @@ -108,8 +127,12 @@ pipewrite(struct pipe *p, char *addr, int n) return -EPIPE; } - for(i = 0; i < n; i++){ - while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full + // A write of PIPE_BUF or less goes in whole or waits for room, so two + // writers' records never interleave; a larger one goes in as room appears. + uint want_room = (n <= PIPE_BUF) ? (uint)n : 1; + i = 0; + while(i < n){ + while(PIPESIZE - (p->nwrite - p->nread) < want_room){ //DOC: pipewrite-full if(p->readopen == 0){ release(&p->lock); sigsend(myproc()->pid, SIGPIPE); @@ -120,12 +143,32 @@ pipewrite(struct pipe *p, char *addr, int n) return -1; } wakeup(&p->nread); + release(&p->lock); + poll_kick(); + acquire(&p->lock); + if(PIPESIZE - (p->nwrite - p->nread) >= want_room) + break; sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep } - p->data[p->nwrite++ % PIPESIZE] = addr[i]; + // One run: up to the end of this page, of what is left, of the room. + uint off = p->nwrite % PIPESIZE; + uint run = PGSIZE - off % PGSIZE; + uint room = PIPESIZE - (p->nwrite - p->nread); + if(run > (uint)(n - i)) run = (uint)(n - i); + if(run > room) run = room; + char **pg = &p->page[off / PGSIZE]; + if(*pg == 0 && (*pg = kalloc()) == 0){ + release(&p->lock); + return i > 0 ? i : -ENOMEM; + } + memmove(*pg + off % PGSIZE, addr + i, run); + p->nwrite += run; + i += run; + want_room = 1; } wakeup(&p->nread); //DOC: pipewrite-wakeup1 release(&p->lock); + poll_kick(); return n; } @@ -142,13 +185,19 @@ piperead(struct pipe *p, char *addr, int n) } sleep(&p->nread, &p->lock); //DOC: piperead-sleep } - for(i = 0; i < n; i++){ //DOC: piperead-copy - if(p->nread == p->nwrite) - break; - addr[i] = p->data[p->nread++ % PIPESIZE]; + i = 0; + while(i < n && p->nread != p->nwrite){ //DOC: piperead-copy + uint off = p->nread % PIPESIZE; + uint run = PGSIZE - off % PGSIZE; + if(run > (uint)(n - i)) run = (uint)(n - i); + if(run > p->nwrite - p->nread) run = p->nwrite - p->nread; + memmove(addr + i, p->page[off / PGSIZE] + off % PGSIZE, run); + p->nread += run; + i += run; } wakeup(&p->nwrite); //DOC: piperead-wakeup release(&p->lock); + poll_kick(); // a writer polling for room return i; } @@ -168,8 +217,8 @@ pipe_poll(struct pipe *p, int readable, int writable) if(writable){ if(p->readopen == 0) rev |= POLLERR; - else if(p->nwrite < p->nread + PIPESIZE) - rev |= POLLOUT; + else if(PIPESIZE - (p->nwrite - p->nread) >= PIPE_BUF) + rev |= POLLOUT; // room for a whole PIPE_BUF write, as on Linux } release(&p->lock); return rev; diff --git a/kernel/fs/procfs.c b/kernel/fs/procfs.c index 584a226..069d9a4 100644 --- a/kernel/fs/procfs.c +++ b/kernel/fs/procfs.c @@ -43,18 +43,22 @@ // identity of the object. // // 1 the root, /proc +// 2 /proc/self // PROCFS_GLOBAL | id a global file, id being the PROC_* minor that // kernel/dev/procfs.c already uses // pid << 8 the directory /proc/ // (pid << 8) | fileid a file inside it +// (pid << 8) | 0x80 | fd /proc//fd/ // -// pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot -// collide. Shifting by 8 rather than packing tightly keeps the numbers -// readable in a debugger, which matters more than density here. +// File ids stay below 0x80 and fds below 128 (NOFILE). pids are not bounded +// by NPROC -- they count up -- so the global bit sits far above any pid << 8; +// at 0x10000 it was /proc/256. Shifting by 8 rather than packing tightly keeps +// the numbers readable in a debugger, which matters more than density here. // --------------------------------------------------------------------------- #define PROCFS_ROOT 1ULL -#define PROCFS_GLOBAL 0x10000ULL +#define PROCFS_SELF 2ULL +#define PROCFS_GLOBAL (1ULL << 40) #define PF_DIR 0 // the directory itself #define PF_STAT 1 @@ -63,10 +67,14 @@ #define PF_STATM 4 #define PF_EXE 5 #define PF_SCHEDSTAT 6 -#define PF_MAX 6 +#define PF_FD 7 // the directory /proc//fd +#define PF_MAX 7 +#define PF_FDENT 0x80 // | fd: one link in it #define INO_PID(ino) ((int)((ino) >> 8)) #define INO_FILE(ino) ((int)((ino) & 0xFF)) +#define INO_IS_FDENT(ino) (!INO_IS_GLOBAL(ino) && (INO_FILE(ino) & PF_FDENT)) +#define INO_FD(ino) (INO_FILE(ino) & 0x7F) #define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0) // The global files, by the name they appear under and the PROC_* id that @@ -121,6 +129,9 @@ static const struct global_file pidfiles[] = { // inventing a wait time that is not measured would be worse than saying // something true in its place. { "schedstat", PF_SCHEDSTAT }, + // One symlink per open descriptor, named by its number and pointing at the + // path it was opened by, or at "socket:[ino]" and the like. + { "fd", PF_FD }, }; #define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0]))) @@ -267,6 +278,15 @@ gen_file(uint64 ino, char *buf, int cap) if (INO_IS_GLOBAL(ino)) return proc_gen_global((int)(ino & 0xFF), buf, cap); + // Whoever is asking: the one file whose content depends on the reader. + if (ino == PROCFS_SELF) { + sbputi(&s, myproc()->pid); + return s.n; + } + if (INO_IS_FDENT(ino)) { + int n = proc_fd_link(INO_PID(ino), INO_FD(ino), buf, cap); + return n > 0 ? n : 0; + } if (proc_snapshot_pid(INO_PID(ino), &pi) < 0) return 0; // it exited between lookup and read @@ -310,7 +330,8 @@ procfs_iload(struct inode *ip) ip->showmode = 1; ip->atime = ip->mtime = ip->ctime = 0; - if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) { + if (ino == PROCFS_ROOT || + (!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD))) { ip->type = T_DIR; ip->size = 0; ip->nlink = 2; @@ -329,8 +350,12 @@ procfs_iload(struct inode *ip) // symlink whose size is 0 resolves to the empty string -- which is worse // than the regular file was, because it looks like it worked. Generating it // here costs one small render per stat() of one file. - if (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE) { - char path[64]; + // /proc/self and /proc//fd/N are links too. Their targets can change + // under a cached inode, which is why readlink() does not trust this size + // for procfs. + if (ino == PROCFS_SELF || INO_IS_FDENT(ino) || + (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE)) { + char path[256]; int len = gen_file(ino, path, sizeof(path)); ip->type = T_SYMLINK; ip->size = len > 0 ? (uint64)len : 0; @@ -469,8 +494,19 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff) if (namecmp(name, ".") == 0) return iget(dp->dev, dp->inum); - if (namecmp(name, "..") == 0) - return iget(dp->dev, PROCFS_ROOT); // /proc//.. is /proc + if (namecmp(name, "..") == 0) // /proc//.. is /proc + return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ? (dp->inum & ~0xFFULL) + : PROCFS_ROOT); + + // Inside /proc//fd/: a number that is an open descriptor. + if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_FD) { + char tmp[4]; + int fd = name_to_pid(name); + if (fd < 0 || fd >= NOFILE || fd >= 128 || + proc_fd_link(INO_PID(dp->inum), fd, tmp, sizeof(tmp)) < 0) + return 0; + return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_FDENT | fd); + } // Inside /proc// if (dp->inum != PROCFS_ROOT) { @@ -480,6 +516,9 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff) return 0; } + if (namecmp(name, "self") == 0) + return iget(dp->dev, PROCFS_SELF); + // In /proc itself: a number is a process, a word is a global file. pid = name_to_pid(name); if (pid > 0) { @@ -516,6 +555,7 @@ struct linux_dirent64 { #define DT_DIR_ 4 #define DT_REG_ 8 +#define DT_LNK_ 10 static int emit(char *out, int cap, int used, uint64 ino, int type, const char *name) @@ -574,17 +614,30 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off) if (dp->inum == PROCFS_ROOT) { EMIT(PROCFS_ROOT, DT_DIR_, "."); EMIT(PROCFS_ROOT, DT_DIR_, ".."); + EMIT(PROCFS_SELF, DT_LNK_, "self"); for (i = 0; i < NGLOBALS; i++) EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name); for (i = 0; proc_snapshot(i, &pi) == 0; i++) { utoa((uint)pi.pid, name); EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name); } + } else if (INO_FILE(dp->inum) == PF_FD) { + uint64 mask[2]; + EMIT(dp->inum, DT_DIR_, "."); + EMIT(dp->inum & ~0xFFULL, DT_DIR_, ".."); + if (proc_fd_mask(INO_PID(dp->inum), mask) == 0) + for (i = 0; i < 128; i++) + if (mask[i / 64] & (1ULL << (i % 64))) { + utoa((uint)i, name); + EMIT((dp->inum & ~0xFFULL) | PF_FDENT | i, DT_LNK_, name); + } } else { EMIT(dp->inum, DT_DIR_, "."); EMIT(PROCFS_ROOT, DT_DIR_, ".."); for (i = 0; i < NPIDFILES; i++) - EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name); + EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, + pidfiles[i].id == PF_FD ? DT_DIR_ : pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_, + pidfiles[i].name); } full: diff --git a/kernel/fs/schfs/kmount.c b/kernel/fs/schfs/kmount.c index a16528e..cbee97d 100644 --- a/kernel/fs/schfs/kmount.c +++ b/kernel/fs/schfs/kmount.c @@ -641,6 +641,9 @@ kmount_slot(int slot, uint bdev) // Opening with a page allocator also builds the allocation bitmap, by walking // the tree — the only mount-time cost SchFS has, and what buys it a mount // that cannot disagree with the filesystem it just adopted. + // The live root fills its ramdisk, however much memory that turned out to + // be. See kernel/dev/ramdisk.c. + m->vol.grow_to = (bdev == RAMDISK_DEV) ? ramdisk_blocks() : 0; int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc, kd_pfree); if (r == -1) return -EINVAL; /* not a SchFS volume at all */ diff --git a/kernel/fs/schfs/schfs_int.h b/kernel/fs/schfs/schfs_int.h index 0a0d302..991bcfc 100644 --- a/kernel/fs/schfs/schfs_int.h +++ b/kernel/fs/schfs/schfs_int.h @@ -104,6 +104,13 @@ struct schfs_vol { uint64_t total_blocks; uint64_t free_blocks; + // Set by the opener before schfs_vol_open: the device's size, when the + // volume should grow to fill it. Nothing on disk records which blocks are + // free -- open rebuilds that from the tree -- so growing is only a larger + // total before the map is built, and the next commit writes it down. The + // live root uses it to fill a ramdisk sized from memory at boot. 0, and + // anything not larger than the volume, leaves it alone. + uint64_t grow_to; uint64_t next_objectid; uint64_t root_objectid; uint64_t generation; // last *committed* transaction group diff --git a/kernel/fs/schfs/vol.c b/kernel/fs/schfs/vol.c index b7b2254..4d3e1e2 100644 --- a/kernel/fs/schfs/vol.c +++ b/kernel/fs/schfs/vol.c @@ -215,6 +215,8 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, } vol_from_uber(v, u, best_slot); + if (v->grow_to > v->total_blocks) + v->total_blocks = v->grow_to; v->readonly = 1; // until an allocation map exists, below if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0) diff --git a/kernel/net/inet.c b/kernel/net/inet.c index 47426ab..0e43fd9 100644 --- a/kernel/net/inet.c +++ b/kernel/net/inet.c @@ -94,6 +94,7 @@ struct inet_sock { int inuse; int type; // SOCK_STREAM or SOCK_DGRAM int proto; // 0, or IPPROTO_ICMP for a ping socket + int iphdr; // SOCK_RAW: reads start at the IP header struct tcp_pcb *tpcb; struct udp_pcb *upcb; @@ -114,6 +115,7 @@ struct inet_sock { int connected; int conn_failed; int peer_closed; // FIN seen, or the connection died + int wr_shut; // shutdown(SHUT_WR): our FIN is sent int listening; // Connections lwIP has completed but nobody has accept()ed yet. @@ -149,13 +151,36 @@ net_seq(void) return s; } -// Sleep until the count moves past `seen`, or this process is killed. kill() -// makes a sleeping process runnable, so the loop re-checks and leaves. +// A blocked socket call gives up for a kill and for any signal it can take. +// The second used to be missing, so a signal woke the sleeper and the loop +// below put it straight back: busybox's ping sends each request from a +// SIGALRM handler while it sits in recvfrom, and got one reply and then +// silence. There is no SA_RESTART, so the call returns EINTR and the caller +// asks again, which is what every one of them is written to do. +// +// Only signals with a handler count, as on Linux. A SIGCHLD left at its +// default is posted to a parent all the same, and ending a server's accept() +// for it would be an EINTR that program had never been handed before. +static int +net_interrupted(void) +{ + struct proc *p = myproc(); + if (p->killed) + return 1; + uint64 live = p->pending_signals & ~p->signal_mask; + for (int sig = 1; sig < 32; sig++) + if ((live & (1ULL << sig)) && p->sig_handler[sig] > 1) // not DFL/IGN + return 1; + return 0; +} + +// Sleep until the count moves past `seen`, or a signal arrives. kill() makes +// a sleeping process runnable, so the loop re-checks and leaves. static void net_wait(uint seen) { acquire(&netwait_lock); - while (netwait_seq == seen && !myproc()->killed) + while (netwait_seq == seen && !net_interrupted()) sleep(&netwait_seq, &netwait_lock); release(&netwait_lock); } @@ -170,6 +195,7 @@ net_wake(void) netwait_seq++; release(&netwait_lock); wakeup(&netwait_seq); + poll_kick(); // and poll/select/epoll waiters, not at the tick } /* ------------------------------------------------------------------ */ @@ -438,6 +464,11 @@ inet_udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, // processes ping at once; with one ping at a time the difference is invisible, // and a socket that sees a reply to somebody else's request will read a // sequence number it never sent and ignore it. +// +// A SOCK_RAW ICMP socket is the same socket, except that a read starts at the +// IP header: that is the raw-socket contract, and busybox's ping reads the TTL +// out of it and finds the ICMP message by IHL. Writing is the same for both -- +// without IP_HDRINCL a raw socket's caller sends the ICMP message alone too. static u8_t inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_addr_t *addr) @@ -462,7 +493,8 @@ inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p, if (pbuf_copy_partial(p, &type, 1, hlen) != 1 || type != 0) return 0; - u16_t icmplen = (u16_t)(p->tot_len - hlen); + u16_t skip = is->iphdr ? 0 : hlen; + u16_t icmplen = (u16_t)(p->tot_len - skip); if (icmplen <= rx_space(is) && dq_used(is) < NDGRAM) { struct dgram *d = &is->dq[is->dhead % NDGRAM]; @@ -471,9 +503,9 @@ inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p, d->len = icmplen; is->dhead++; - // Copy the ICMP message out, skipping the IP header, across whatever pbuf - // chain it arrived in. - u16_t off = hlen; + // Copy the message out, skipping the IP header unless this is a raw + // socket, across whatever pbuf chain it arrived in. + u16_t off = skip; while (off < p->tot_len) { char tmp[64]; u16_t want = (u16_t)(p->tot_len - off); @@ -515,8 +547,15 @@ inet_alloc(int type) void * inet_new(int type, int proto) { - if (type != SOCK_STREAM && type != SOCK_DGRAM) + if (type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_RAW) return 0; + if (type == SOCK_RAW && proto != IPPROTO_ICMP) + return 0; + // Past this point a raw ICMP socket is a ping socket with one flag: every + // datagram path below already does the right thing for it. + int iphdr = (type == SOCK_RAW); + if (iphdr) + type = SOCK_DGRAM; // Ready, not *up*: see net_is_ready() in kernel/net/net.c. Requiring an // address here made every socket() fail until DHCP answered. if (!net_is_ready()) @@ -529,6 +568,7 @@ inet_new(int type, int proto) return 0; } is->proto = proto; + is->iphdr = iphdr; if (proto == IPPROTO_ICMP) { // A ping socket. raw_new registers for one IP protocol number; the @@ -688,7 +728,7 @@ inet_accept(void *h) uint seen = net_seq(); lwip_sys_unlock(); - if (myproc()->killed) + if (net_interrupted()) return 0; net_wait(seen); } @@ -729,7 +769,7 @@ inet_connect(void *h, uint32 addr, uint16 port) return 0; if (failed) return -ECONNREFUSED; - if (myproc()->killed) + if (net_interrupted()) return -EINTR; net_wait(seen); } @@ -742,6 +782,47 @@ inet_connect(void *h, uint32 addr, uint16 port) // peer it is connected to and is the same for every byte; for a datagram // socket it belongs to the individual datagram, which is why UDP needs the // queue above and TCP does not. +// getsockname / getpeername: this end's address or the other's, the address +// in network order and the port in host order, as inet_recvfrom gives them. A +// socket not yet bound reads as 0.0.0.0:0 locally, which is what Linux says; +// one with no peer is ENOTCONN. A listening pcb has no remote fields at all +// -- lwIP's listen pcb is the smaller struct -- so it is never asked for them. +int +inet_name(void *h, int peer, uint *addr, ushort *port) +{ + struct inet_sock *is = h; + int r = 0; + + *addr = 0; + *port = 0; + lwip_sys_lock(); + if (is->tpcb) { + if (peer && (is->listening || is->tpcb->state < SYN_SENT)) { + r = -ENOTCONN; + } else if (peer) { + *addr = ip4_addr_get_u32(ip_2_ip4(&is->tpcb->remote_ip)); + *port = is->tpcb->remote_port; + } else { + *addr = ip4_addr_get_u32(ip_2_ip4(&is->tpcb->local_ip)); + *port = is->tpcb->local_port; + } + } else if (is->upcb) { + if (peer && !(is->upcb->flags & UDP_FLAGS_CONNECTED)) { + r = -ENOTCONN; + } else if (peer) { + *addr = ip4_addr_get_u32(ip_2_ip4(&is->upcb->remote_ip)); + *port = is->upcb->remote_port; + } else { + *addr = ip4_addr_get_u32(ip_2_ip4(&is->upcb->local_ip)); + *port = is->upcb->local_port; + } + } else if (peer) { + r = -ENOTCONN; // a ping socket has no peer + } + lwip_sys_unlock(); + return r; +} + int inet_recvfrom(void *h, char *buf, int n, uint *addr, ushort *port) { @@ -787,7 +868,7 @@ inet_recvfrom(void *h, char *buf, int n, uint *addr, ushort *port) return got; if (eof) return 0; // end of file, not an error - if (myproc()->killed) + if (net_interrupted()) return -EINTR; net_wait(seen); } @@ -879,7 +960,7 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port) uint seen; lwip_sys_lock(); - if (is->peer_closed || is->tpcb == 0) { + if (is->peer_closed || is->wr_shut || is->tpcb == 0) { lwip_sys_unlock(); // Same contract as a pipe with no reader, which is what the AF_UNIX side // does: the signal first, then the error. @@ -909,7 +990,7 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port) lwip_sys_unlock(); if (done < n) { - if (myproc()->killed) + if (net_interrupted()) return done > 0 ? done : -EINTR; // Woken by inet_sent_cb when an acknowledgement frees send buffer, or by // a failure. Without tcp_sent registered this would be a sleep with @@ -926,6 +1007,32 @@ inet_write(void *h, char *buf, int n) return inet_sendto(h, buf, n, 0, 0); } +// shutdown(2) on a TCP socket. SHUT_WR sends our FIN and the peer can keep +// sending; SHUT_RD is local, reads see EOF. The pcb is never freed here -- +// tcp_shutdown(pcb, 1, 1) would free it -- so close() still owns it. +int +inet_shutdown(void *h, int how) +{ + struct inet_sock *is = h; + + if (is->type != SOCK_STREAM) + return 0; + lwip_sys_lock(); + if (is->tpcb == 0 || is->listening || !is->connected) { + lwip_sys_unlock(); + return -ENOTCONN; + } + if ((how == SHUT_WR || how == SHUT_RDWR) && !is->wr_shut) { + is->wr_shut = 1; + tcp_shutdown(is->tpcb, 0, 1); + } + if (how == SHUT_RD || how == SHUT_RDWR) + is->peer_closed = 1; + lwip_sys_unlock(); + net_wake(); + return 0; +} + int inet_readable(void *h) { diff --git a/kernel/net/unix.c b/kernel/net/unix.c index 335aa66..fca12e9 100644 --- a/kernel/net/unix.c +++ b/kernel/net/unix.c @@ -230,6 +230,7 @@ sockclose(struct socket *s) s->ref = 0; wakeup(&s->nqueue); // an accept() blocked on this listener release(&stab.lock); + poll_kick(); // the peer's poll() sees the hangup now } void @@ -459,6 +460,7 @@ sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout // on a full buffer that this call has just made room in. wakeup(&s->nwrite); release(&stab.lock); + poll_kick(); // a writer polling for room // Outside the lock, because fileclose can sleep. if(fout){ @@ -520,7 +522,7 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf) // look for it. Same contract as a pipe with no reader: a signal, then an // error. A process that writes to a dead connection and is not told about // it spins forever believing it is making progress. - if(s->peer == 0){ + if(s->peer == 0 || s->wr_shut){ release(&stab.lock); sigsend(myproc()->pid, SIGPIPE); return -EPIPE; @@ -569,6 +571,9 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf) wakeup(&s->peer->nread); release(&stab.lock); + // poll() and epoll waiters, now rather than at the next tick: an X client + // and its server talk over these, a request and a reply at a time. + poll_kick(); return i; } @@ -688,6 +693,61 @@ sockbind(struct socket *s, const char *path, uint dev, uint64 ino) return 0; } +// The abstract namespace: a sun_path that starts with a NUL names no file. +// Linux keeps these names in the socket layer, and Xorg binds one before its +// /tmp/.X11-unix path -- which this used to read as the empty path, resolve +// to the directory it was called from and refuse with EADDRINUSE, so Xorg +// reported "server already running" and exited. +// +// The name becomes a key on a device number no filesystem has, and the rest +// is the inode machinery unchanged: connect() finds the listener bound to +// (SOCK_ABSTRACT_DEV, key). The name goes when the socket does, because +// nothing else holds it. +int +sockbind_abstract(struct socket *s, uint64 key) +{ + acquire(&stab.lock); + if(s->state != SS_UNBOUND){ + release(&stab.lock); + return -EINVAL; + } + for(struct socket *o = stab.sock; o < stab.sock + NSOCK; o++) + if(o != s && o->state != SS_FREE && o->state != SS_UNBOUND && + o->bind_dev == SOCK_ABSTRACT_DEV && o->bind_ino == key){ + release(&stab.lock); + return -EADDRINUSE; + } + s->name[0] = '\0'; // getsockname reports it unnamed, not as a path + s->bind_dev = SOCK_ABSTRACT_DEV; + s->bind_ino = key; + s->state = SS_BOUND; + release(&stab.lock); + return 0; +} + +// The path for getsockname (peer 0) or getpeername (peer 1) of an AF_UNIX +// socket. Empty for an unnamed one -- a client that never bound, or an +// abstract name, which the caller reports as the family alone. +int +sockname_inet(struct socket *s, int peer, uint *addr, ushort *port) +{ + return inet_name(s->inet, peer, addr, port); +} + +int +sockname_unix(struct socket *s, int peer, char *path) +{ + acquire(&stab.lock); + struct socket *o = peer ? s->peer : s; + if(o == 0){ + release(&stab.lock); + return -ENOTCONN; + } + safestrcpy(path, o->name, UNIX_PATH_MAX); + release(&stab.lock); + return 0; +} + int socklisten(struct socket *s, int backlog) { @@ -799,6 +859,7 @@ sockconnect(struct socket *s, uint dev, uint64 ino) wakeup(&srv->nqueue); // an accept() may be blocked on this release(&stab.lock); + poll_kick(); // or a listener in poll(), as Xorg's is // Deliberately does *not* wait for accept(). On Unix a queued connection is // already established: the client may write, and the bytes sit in the @@ -817,6 +878,9 @@ sockconnect(struct socket *s, uint dev, uint64 ino) int sock_shutdown(struct socket *s, int how) { + if(s->domain == AF_INET) + return inet_shutdown(s->inet, how); + acquire(&stab.lock); if(s->state != SS_CONNECTED){ release(&stab.lock); @@ -827,9 +891,20 @@ sock_shutdown(struct socket *s, int how) s->peer_closed = 1; wakeup(&s->nread); } - if(how == SHUT_WR || how == SHUT_RDWR) + if(how == SHUT_RDWR) orphan_peer(s); + else if(how == SHUT_WR){ + // Half-close: the peer sees EOF once it has read what is there, and the + // link stays, so its reply still reaches us. Cutting the link here made + // the reply a SIGPIPE -- busybox wget does exactly this to ssl_client. + s->wr_shut = 1; + if(s->peer){ + s->peer->peer_closed = 1; + wakeup(&s->peer->nread); + } + } release(&stab.lock); + poll_kick(); return 0; } diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index f132828..bde72f0 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -311,6 +311,7 @@ found: memset(p->sig_handler, 0, sizeof(p->sig_handler)); memset(p->sig_restorer, 0, sizeof(p->sig_restorer)); p->pending_signals = 0; + p->alarm_at = p->alarm_every = 0; // a child's timers start disarmed p->utime = p->stime = p->cutime = p->cstime = 0; // A recycled slot must not inherit a dead process's place in the queue -- @@ -1543,6 +1544,97 @@ sig_kills_now(struct proc *p, int sig) return p->sig_handler[sig] == 0 && sig_default_kills(sig); } +// SIGALRM, on time. +// +// alarm() and setitimer() stored a countdown that nothing ever counted down, +// so no timer anyone set has ever fired. Most programs never noticed, because +// what they used it for was a timeout; busybox's ping sends every request but +// the first from its SIGALRM handler, and on the Alpine root it is /bin/ping, +// so ping printed one reply and then nothing. +// +// A kernel thread rather than the timer interrupt, which is in trap.c, and +// which would have to walk the process table with interrupts off: this walks +// it once a tick, and only while some process has a timer armed. +static int alarms_armed; // processes with alarm_at set; a hint, not a lock + +void +alarm_arm(struct proc *p, uint after, uint every) +{ + acquire(&ptable.lock); + if (p->alarm_at == 0 && after) + alarms_armed++; + else if (p->alarm_at && after == 0) + alarms_armed--; + p->alarm_at = after ? ticks + after : 0; + if (p->alarm_at == 0 && after) + p->alarm_at = 1; // 0 means disarmed; a tick late is fine + p->alarm_every = after ? every : 0; + release(&ptable.lock); +} + +// Ticks until p's timer is due, 0 if it is not armed. +uint +alarm_left(struct proc *p) +{ + uint at = p->alarm_at; + if (at == 0) + return 0; + int left = (int)(at - ticks); + return left > 0 ? (uint)left : 1; +} + +static void +alarm_thread(void) +{ + for (;;) { + acquire(&tickslock); + sleep(&ticks, &tickslock); + uint now = ticks; + release(&tickslock); + if (alarms_armed == 0) + continue; + + acquire(&ptable.lock); + for (struct proc *p = ptable.proc; p < &ptable.proc[NPROC]; p++) { + if (p->alarm_at == 0 || (int)(now - p->alarm_at) < 0) + continue; + if (p->state == UNUSED || p->state == ZOMBIE) { + p->alarm_at = 0; + alarms_armed--; + continue; + } + if (p->alarm_every) { + p->alarm_at = now + p->alarm_every; + } else { + p->alarm_at = 0; + alarms_armed--; + } + // sigsend takes ptable.lock itself, and with this many lines it is + // clearer to repeat its three effects than to split it. + if (!init_ignores(p, SIGALRM)) { + p->pending_signals |= 1ULL << SIGALRM; + if (sig_kills_now(p, SIGALRM)) { + p->killed = 1; + if (p->killsig == 0) + p->killsig = SIGALRM; + } + if (p->state == SLEEPING) { + p->state = RUNNABLE; + sched_enqueue(p); + } + } + } + release(&ptable.lock); + } +} + +void +alarm_thread_start(void) +{ + if (kthread_create(alarm_thread, "alarmd") < 0) + cprintf("alarm: no kernel thread; alarm() and setitimer() will not fire\n"); +} + int sigsend(int pid, int sig) { @@ -1815,6 +1907,50 @@ proc_snapshot_pid(int pid, struct procinfo *out) return -1; } +// /proc//fd: which descriptors are open, and what one points at. The +// ptable lock keeps the process from being reaped -- exit() closes its files +// first -- and file_describe() holds the file steady itself. +int +proc_fd_mask(int pid, uint64 mask[2]) +{ + struct proc *p; + int r = -1; + + mask[0] = mask[1] = 0; + acquire(&ptable.lock); + for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) { + if (p->state == UNUSED || p->pid != pid) + continue; + for (int fd = 0; fd < NOFILE && fd < 128; fd++) + if (p->ofile[fd]) + mask[fd / 64] |= 1ULL << (fd % 64); + r = 0; + break; + } + release(&ptable.lock); + return r; +} + +int +proc_fd_link(int pid, int fd, char *buf, int cap) +{ + struct proc *p; + int r = -1; + + if (fd < 0 || fd >= NOFILE) + return -1; + acquire(&ptable.lock); + for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) { + if (p->state == UNUSED || p->pid != pid) + continue; + if (p->ofile[fd]) + r = file_describe(p->ofile[fd], buf, cap); + break; + } + release(&ptable.lock); + return r; +} + int proc_num(void) { struct proc *p; diff --git a/kernel/scff/compat/assert.h b/kernel/scff/compat/assert.h new file mode 100644 index 0000000..bb6e6fd --- /dev/null +++ b/kernel/scff/compat/assert.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// zstd builds with DEBUGLEVEL 0, where assert() is never expanded. +#ifndef SCFF_COMPAT_ASSERT_H +#define SCFF_COMPAT_ASSERT_H +#define assert(x) ((void)0) +#endif diff --git a/kernel/scff/compat/stdio.h b/kernel/scff/compat/stdio.h new file mode 100644 index 0000000..454095e --- /dev/null +++ b/kernel/scff/compat/stdio.h @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Included by zstd's debug printing, which DEBUGLEVEL 0 compiles out. +#ifndef SCFF_COMPAT_STDIO_H +#define SCFF_COMPAT_STDIO_H +#endif diff --git a/kernel/scff/compat/stdlib.h b/kernel/scff/compat/stdlib.h new file mode 100644 index 0000000..be9b1d1 --- /dev/null +++ b/kernel/scff/compat/stdlib.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// zstd's allocator hooks. The kernel decoder is a static context in a fixed +// workspace (kernel/scff/scff.c), so nothing allocates: these exist only for +// code paths that are compiled in and never taken. +#ifndef SCFF_COMPAT_STDLIB_H +#define SCFF_COMPAT_STDLIB_H +#include +static inline void *malloc(size_t n) { (void)n; return 0; } +static inline void *calloc(size_t n, size_t s) { (void)n; (void)s; return 0; } +static inline void free(void *p) { (void)p; } +#endif diff --git a/kernel/scff/compat/string.h b/kernel/scff/compat/string.h new file mode 100644 index 0000000..e238d0d --- /dev/null +++ b/kernel/scff/compat/string.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// What zstd asks of , from the kernel's own (kernel/core/string.c). +// zstd calls __builtin_memcpy and friends where it can, so these are the +// fallbacks for the sizes the compiler does not open-code. +#ifndef SCFF_COMPAT_STRING_H +#define SCFF_COMPAT_STRING_H +#include +void *memcpy(void *, const void *, unsigned int); +void *memmove(void *, const void *, unsigned long); +void *memset(void *, int, unsigned long); +#endif diff --git a/kernel/scff/scff.c b/kernel/scff/scff.c new file mode 100644 index 0000000..5c2acfc --- /dev/null +++ b/kernel/scff/scff.c @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// The zstd decoder for SCFF chunks (include/fs/scff.h). +// +// One static decompression context in a fixed workspace, so nothing here +// allocates, and one caller at a time: kernel/dev/ramdisk.c holds its chunk +// lock across every call. +// +// **The decoder runs on a stack of its own.** A kernel stack is 8 KB, and a +// chunk is decompressed at the bottom of a read -- syscall, namei, SchFS, +// bread, ramdiskrw -- with most of it already used. zstd's sequence decoder +// keeps several KB of state in locals, which Linux gives it on a 16 KB stack. +// Here that would run off the end of the process's stack into whatever is +// below it. So the call is made on scff_stack, which the chunk lock makes +// this caller's alone. An interrupt taken meanwhile lands on it too, which +// is fine: it is a kernel stack like any other, and a big one. +// +// This file sees zstd's headers and none of the kernel's, which keeps the two +// sets of type definitions from meeting. + +#define ZSTD_STATIC_LINKING_ONLY +#include "zstd.h" + +#include "scff_zstd.h" + +static unsigned char scff_work[256 * 1024] __attribute__((aligned(64))); +static unsigned char scff_stack[64 * 1024] __attribute__((aligned(16))); +static ZSTD_DCtx *dctx; + +// call_on_stack(fn, arg, top): fn(arg) with %rsp at top, then back. +// %rbp keeps the old stack pointer across the call; it is callee-saved. +long call_on_stack(long (*fn)(void *), void *arg, void *top); +__asm__( + ".text\n" + ".globl call_on_stack\n" + "call_on_stack:\n" + " pushq %rbp\n" + " movq %rsp, %rbp\n" + " movq %rdx, %rsp\n" + " movq %rdi, %rax\n" + " movq %rsi, %rdi\n" + " callq *%rax\n" + " movq %rbp, %rsp\n" + " popq %rbp\n" + " retq\n"); + +struct job { + void *dst; + unsigned long cap; + const void *src; + unsigned long n; +}; + +static long +run(void *arg) +{ + struct job *j = arg; + size_t r = ZSTD_decompressDCtx(dctx, j->dst, j->cap, j->src, j->n); + return ZSTD_isError(r) ? -1 : (long)r; +} + +int +scff_zstd_init(void) +{ + if (dctx) + return 0; + if (ZSTD_estimateDCtxSize() > sizeof(scff_work)) + return -1; + dctx = ZSTD_initStaticDCtx(scff_work, sizeof(scff_work)); + return dctx ? 0 : -1; +} + +long +scff_zstd_decompress(void *dst, unsigned long cap, const void *src, unsigned long n) +{ + struct job j = { dst, cap, src, n }; + + if (!dctx) + return -1; + return call_on_stack(run, &j, scff_stack + sizeof(scff_stack)); +} diff --git a/kernel/scff/scff_zstd.h b/kernel/scff/scff_zstd.h new file mode 100644 index 0000000..8badcf6 --- /dev/null +++ b/kernel/scff/scff_zstd.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// The SCFF decoder's interface, in types both the kernel's headers and zstd's +// agree on. See kernel/scff/scff.c. +#ifndef SCHOS_SCFF_ZSTD_H +#define SCHOS_SCFF_ZSTD_H + +// 0, or -1 if the workspace is too small for this zstd. +int scff_zstd_init(void); +// Decompress one zstd frame. The length written, or -1 for a bad frame. +// One caller at a time. +long scff_zstd_decompress(void *dst, unsigned long cap, const void *src, unsigned long n); + +#endif diff --git a/kernel/sys/sys_event.c b/kernel/sys/sys_event.c index 6846177..dd0b0d3 100644 --- a/kernel/sys/sys_event.c +++ b/kernel/sys/sys_event.c @@ -113,19 +113,13 @@ clock_ns(int clock) return sec * 1000000000ULL + usec * 1000ULL; } -// One tick, or until a signal or kill makes waiting pointless. +// One tick, a poll_kick, or a signal or kill that makes waiting pointless: +// poll's wait, so data arriving wakes an epoll_wait as it wakes a poll. +// Returns 1 for a tick, 0 for a kick, -EINTR. static int ev_block(void) { - struct proc *p = myproc(); - if (p->killed || (p->pending_signals & ~p->signal_mask)) - return -EINTR; - acquire(&tickslock); - uint t0 = ticks; - while (ticks == t0 && !p->killed && !(p->pending_signals & ~p->signal_mask)) - sleep(&ticks, &tickslock); - release(&tickslock); - return 0; + return poll_wait_tick(); } static int @@ -178,6 +172,19 @@ ev_arg(int n, int kind, struct file **pf) return f->ev; } +// The name Linux gives each kind in /proc//fd. +const char * +ev_kind_name(struct evobj *o) +{ + switch (o ? o->kind : 0) { + case EV_EVENTFD: return "anon_inode:[eventfd]"; + case EV_TIMERFD: return "anon_inode:[timerfd]"; + case EV_SIGNALFD: return "anon_inode:[signalfd]"; + case EV_EPOLL: return "anon_inode:[eventpoll]"; + } + return "anon_inode:[event]"; +} + // Called by fileclose() for the last reference. void ev_close(struct evobj *o) @@ -714,11 +721,12 @@ epoll_wait_common(int timeout_ms) got = ep_ready(o, buf, max); if (got > 0 || ticks_left == 0) break; - if (ev_block() < 0) { + int w = ev_block(); + if (w < 0) { got = -EINTR; break; } - if (ticks_left > 0) + if (w > 0 && ticks_left > 0) ticks_left--; } if (got > 0 && copy_to_user(uevents, buf, got * sizeof(*buf)) < 0) diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 8ba18f9..1f6eaff 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -13,6 +13,7 @@ #include "uapi/posix.h" #include "core/syscall.h" #include "core/uaccess.h" +#include "core/kmem.h" #include "uapi/errno.h" #include "fs/schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */ #include "gfx/gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */ @@ -69,6 +70,22 @@ linux_makedev(uint32 major, uint32 minor) | ((uint64)(major & ~0xfffu) << 32) | ((uint64)(minor & ~0xffu) << 12); } +// Terminals as Linux numbers them: tty0..63 are (4, n), /dev/tty is (5, 0) and +// /dev/console (5, 1). SchOS keeps them all under major 1 (kernel/fs/devfs.c), +// and Linux programs look at the number rather than the name. Xorg is the one +// that matters: it uses VTs only if stat("/dev/tty0") says major 4 +// (xf86HasTTYs), and otherwise runs on none -- no VT switch, no KD_GRAPHICS, +// the console drawing over it, and Ctrl+Alt+F-keys doing nothing. +static uint64 +linux_tty_rdev(uint32 major, uint32 minor) +{ + if (major == 1 && minor == 0) return linux_makedev(4, 0); + if (major == 1 && minor >= 11 && minor <= 16) return linux_makedev(4, minor - 10); + if (major == 1 && minor == 10) return linux_makedev(5, 0); + if (major == 1 && minor == 1) return linux_makedev(5, 1); + return linux_makedev(major, minor); +} + static void stati_linux(struct inode *ip, struct linux_stat *st) { @@ -101,7 +118,7 @@ stati_linux(struct inode *ip, struct linux_stat *st) // entry in /dev printed as "0, 0" under ls -l -- harmless while nothing // read it, and wrong the moment something has to tell two disks apart. if (ip->type == T_DEV) - st->st_rdev = linux_makedev(ip->major, ip->minor); + st->st_rdev = linux_tty_rdev(ip->major, ip->minor); st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime; } @@ -139,6 +156,53 @@ fdalloc(struct file *f) return -1; } +// Remember the absolute path `f` was opened by, for /proc//fd. `path` is +// joined to the directory it was resolved from -- the cwd, or dirfd's own +// remembered path -- and `.`, `..` and repeated slashes are folded out. This +// is lexical: a symlink along the way stays in the name. Nothing depends on +// it being there, so running out of memory just leaves it unset. +static void +file_setpath(struct file *f, int dirfd, const char *path) +{ + const char *base = ""; + if (path[0] != '/') { + struct file *d; + if (dirfd == AT_FDCWD) + base = myproc()->cwd_path; + else if (dirfd >= 0 && dirfd < NOFILE && (d = myproc()->ofile[dirfd]) && d->path) + base = d->path; + else + return; + } + char *buf = kalloc(); + if (!buf) return; + int n = 0; // buf is "/a/b", no trailing slash + for (int pass = 0; pass < 2; pass++) { + const char *s = pass ? path : base; + for (;;) { + while (*s == '/') s++; + const char *e = s; + while (*e && *e != '/') e++; + int len = (int)(e - s); + if (len == 0) break; + if (len == 2 && s[0] == '.' && s[1] == '.') { + while (n > 0 && buf[--n] != '/') ; + } else if (!(len == 1 && s[0] == '.')) { + if (n + 1 + len >= PGSIZE) { kfree(buf); return; } + buf[n++] = '/'; + memmove(buf + n, s, len); + n += len; + } + s = e; + } + } + if (n == 0) buf[n++] = '/'; + buf[n] = 0; + if ((f->path = kmalloc(n + 1)) != 0) + memmove(f->path, buf, n + 1); + kfree(buf); +} + // Resolves a dirfd (AT_FDCWD or a real fd) to the inode a relative path // should be resolved from. 0 on an invalid dirfd. static struct inode * @@ -602,6 +666,7 @@ addr_t sys_open(void) } iunlock(ip); end_op(); f->type = FD_INODE; f->ip = ip; + file_setpath(f, AT_FDCWD, path); f->off = (omode & O_APPEND) ? ip->size : 0; f->readable = !(omode & O_WRONLY); f->writable = (omode & O_WRONLY) || (omode & O_RDWR); @@ -659,6 +724,7 @@ addr_t sys_openat(void) uint64 start = (omode & O_APPEND) ? ip->size : 0; iunlock(ip); end_op(); f->type = FD_INODE; f->ip = ip; f->off = start; + file_setpath(f, dirfd, path); f->readable = !(omode & O_WRONLY); f->writable = (omode & O_WRONLY) || (omode & O_RDWR); f->nonblock = (omode & O_NONBLOCK) != 0; @@ -697,6 +763,19 @@ addr_t sys_fstat(void) } return 0; } + // A socket, a pipe or an anonymous inode: Linux answers with its type and + // an inode number, which is what `ls -l /proc//fd` and every program + // asking "is my stdin a socket?" look at. ino matches /proc//fd. + if (f->type != FD_INODE && f->type != FD_MEMFD && myproc()->syscall_map) { + struct linux_stat *ls = (struct linux_stat *)st; + memset(ls, 0, sizeof(*ls)); + ls->st_mode = f->type == FD_SOCKET ? LINUX_S_IFSOCK | 0777 + : f->type == FD_PIPE ? LINUX_S_IFIFO | 0600 : 0600; + ls->st_ino = f->ino; + ls->st_nlink = 1; + ls->st_blksize = PGSIZE; + return 0; + } if (f->type != FD_INODE) return -EINVAL; ilock(f->ip); if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st); @@ -1229,10 +1308,14 @@ addr_t sys_readlink(void) if (!(ip = namei(path))) return -ENOENT; ilock(ip); if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; } - int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz; - if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; } + // A /proc link can change after its inode was loaded -- an fd dup2()ed + // over -- so its size is not trusted: the read says how long it is. + int proc = ip->dev == PROCFS_DEV; + int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size; + int got = readi(ip, 0, (uint64)buf, 0, n); + if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; } iunlockput(ip); - return n; + return got; } // ----------------------------------------------------------------------- @@ -2090,10 +2173,14 @@ addr_t sys_readlinkat(void) if (!(ip = at_namei(dirfd, path))) return -ENOENT; ilock(ip); if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; } - int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz; - if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; } + // A /proc link can change after its inode was loaded -- an fd dup2()ed + // over -- so its size is not trusted: the read says how long it is. + int proc = ip->dev == PROCFS_DEV; + int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size; + int got = readi(ip, 0, (uint64)buf, 0, n); + if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; } iunlockput(ip); - return n; + return got; } addr_t sys_fchmodat(void) { return 0; } // stub diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index bf52d9e..fcf2038 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -66,6 +66,28 @@ fetch_sun_path(addr_t uaddr, int ulen, char *out) return 0; } +// For an abstract address -- sun_path[0] == 0 -- the name as a key, and 1. +// The name is every byte after the NUL up to the length given, NULs included, +// as Linux has it; FNV-1a, which is plenty for a table of NSOCK. +static int +sun_abstract(addr_t uaddr, int ulen, uint64 *key) +{ + struct sockaddr_un sa; + int plen = ulen - (int)sizeof(sa.sun_family); + + if (plen < 2 || ulen > (int)sizeof(sa)) + return 0; + if (copy_from_user(&sa, uaddr, (uint64)ulen) < 0 || sa.sun_path[0] != 0) + return 0; + uint64 h = 1469598103934665603ULL; + for (int i = 1; i < plen; i++) { + h ^= (uchar)sa.sun_path[i]; + h *= 1099511628211ULL; + } + *key = h ? h : 1; + return 1; +} + // Wrap an established socket in a file and a descriptor. Consumes the // socket's reference on success; on failure the caller still owns it. // @@ -144,7 +166,8 @@ SYSCALL_DEFINE(socket) type &= 0xFF; if (domain == AF_UNIX && type != SOCK_STREAM) return -EPROTOTYPE; - if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM) + if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM && + type != SOCK_RAW) return -EPROTOTYPE; // AF_NETLINK takes SOCK_RAW or SOCK_DGRAM and its own protocol numbering; // socknew checks both, because which protocols exist is the socket layer's @@ -168,10 +191,17 @@ SYSCALL_DEFINE(socket) (domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP); if (redundant) protocol = 0; - else if (!(domain == AF_INET && type == SOCK_DGRAM && + else if (!(domain == AF_INET && + (type == SOCK_DGRAM || type == SOCK_RAW) && protocol == IPPROTO_ICMP)) return -EPROTONOSUPPORT; } + // A raw socket is ICMP or nothing: it exists because busybox's ping, which + // is /bin/ping on the Alpine root, opens SOCK_RAW and stops at + // EPROTOTYPE. Raw TCP or UDP would be the stack handing over packets it is + // also processing, and nothing has asked for that. + if (domain == AF_INET && type == SOCK_RAW && protocol != IPPROTO_ICMP) + return -EPROTONOSUPPORT; // ENETDOWN rather than ENFILE when the stack itself is not running: the // old blanket ENFILE said "file table overflow" for a machine with no @@ -240,6 +270,10 @@ SYSCALL_DEFINE(bind) return sockbind_in(s, ip, port_from_net(port)); } + uint64 key; + if (sun_abstract(uaddr, ulen, &key)) + return sockbind_abstract(s, key); + if ((r = fetch_sun_path(uaddr, ulen, path)) < 0) return r; @@ -315,6 +349,10 @@ SYSCALL_DEFINE(connect) return sockconnect_in(s, ip, port_from_net(port)); } + uint64 key; + if (sun_abstract(uaddr, ulen, &key)) + return sockconnect(s, SOCK_ABSTRACT_DEV, key); + if ((r = fetch_sun_path(uaddr, ulen, path)) < 0) return r; @@ -550,9 +588,76 @@ SYSCALL_DEFINE(recvfrom) return r; } -// shutdown(2). Half-close is not modelled -- there is one buffer per -// direction and no per-direction flag -- so SHUT_WR and SHUT_RDWR both mean -// "the peer sees end-of-file", which is the behaviour programs depend on. +// getsockname(2) and getpeername(2). +// +// Neither existed, and the first program to mind was every X client: libxcb +// picks the cookie to send by the family of the connection's address, asked +// with getpeername and then getsockname, and with both failing it sent none. +// Xorg answered "Authorization required, but no authorization protocol +// specified" and startx gave up. +static int +sock_name(int peer) +{ + struct socket *s; + addr_t uaddr, ulenp; + int r, cap; + + if ((r = argsock(0, &s)) < 0) + return r; + argaddr(1, &uaddr); + argaddr(2, &ulenp); + if (get_user(cap, ulenp) < 0) + return -EFAULT; + if (cap < 0) + return -EINVAL; + + union { + struct sockaddr_in in; + struct sockaddr_un un; + } sa; + int alen; + memset(&sa, 0, sizeof(sa)); + + if (sock_domain(s) == AF_INET) { + uint ip; + ushort port; + if ((r = sockname_inet(s, peer, &ip, &port)) < 0) + return r; + sa.in.sin_family = AF_INET; + sa.in.sin_addr = ip; + sa.in.sin_port = port_to_net(port); + alen = (int)sizeof(sa.in); + } else if (sock_domain(s) == AF_NETLINK) { + // A sockaddr_nl of zeros: family, padding, port id 0 (the kernel), + // no groups. + sa.un.sun_family = AF_NETLINK; + alen = 12; + } else { + if ((r = sockname_unix(s, peer, sa.un.sun_path)) < 0) + return r; + sa.un.sun_family = (uint16)sock_domain(s); + // The family, the path and its NUL; an unnamed socket is the family + // alone, as on Linux. + alen = (int)sizeof(sa.un.sun_family); + if (sa.un.sun_path[0]) + alen += (int)strlen(sa.un.sun_path) + 1; + } + + // Truncated to the caller's buffer, with the full length written back. + if (cap > alen) + cap = alen; + if (cap > 0 && copy_to_user(uaddr, &sa, (uint64)cap) < 0) + return -EFAULT; + if (copy_to_user(ulenp, &alen, sizeof(alen)) < 0) + return -EFAULT; + return 0; +} + +SYSCALL_DEFINE(getsockname) { return sock_name(0); } +SYSCALL_DEFINE(getpeername) { return sock_name(1); } + +// shutdown(2). SHUT_WR is a half-close: the peer reads EOF and can still +// write back (see sock_shutdown). SYSCALL_DEFINE(sockshutdown) { struct socket *s; diff --git a/kernel/sys/sys_poll.c b/kernel/sys/sys_poll.c index a28da29..cdcfac3 100644 --- a/kernel/sys/sys_poll.c +++ b/kernel/sys/sys_poll.c @@ -24,22 +24,47 @@ #define POLL_MAX_FDS 64 -// Sleep one timer tick, or until killed/signaled. Returns 0 to keep -// waiting, -EINTR if the wait must be abandoned. -static int -poll_block_one_tick(void) +// Bumped, under tickslock, by anything that can make a descriptor ready +// sooner than the next tick: see poll_kick(). +static uint poll_seq; + +// Something a poller may be waiting for has happened. The wait below sleeps +// on the tick channel, so this wakes it now rather than at the tick. +// +// Without it every poll() that found nothing slept out the rest of a 10 ms +// tick whatever arrived in the meantime. busybox's wget polls before each +// read, and downloaded at 85 KB/s -- a segment per tick -- where apk, which +// blocks in read() and is woken by the data, managed 5.8 MB/s over the same +// connection. Called from the network stack's wakeup; anything else that +// makes a file readable can call it too, and until it does it is no worse +// than it was. +void +poll_kick(void) { - if (myproc()->killed) - return -EINTR; - if (myproc()->pending_signals & ~myproc()->signal_mask) + acquire(&tickslock); + poll_seq++; + release(&tickslock); + wakeup(&ticks); +} + +// Sleep one timer tick, or until a poll_kick, or until killed/signaled. +// Returns 1 if a tick went by, 0 if it was a kick -- which must not count +// against a timeout, or a timed poll on a busy network would end early -- +// and -EINTR if the wait must be abandoned. epoll waits with it too. +int +poll_wait_tick(void) +{ + struct proc *p = myproc(); + if (p->killed || (p->pending_signals & ~p->signal_mask)) return -EINTR; acquire(&tickslock); - uint t0 = ticks; - while (ticks == t0 && !myproc()->killed && - !(myproc()->pending_signals & ~myproc()->signal_mask)) + uint t0 = ticks, s0 = poll_seq; + while (ticks == t0 && poll_seq == s0 && !p->killed && + !(p->pending_signals & ~p->signal_mask)) sleep(&ticks, &tickslock); + int ticked = ticks != t0; release(&tickslock); - return 0; + return ticked; } // Shared core: fds[] already in kernel memory. timeout_ticks < 0 means @@ -67,9 +92,10 @@ do_poll(struct pollfd *fds, int nfds, int timeout_ticks) } if (nready > 0 || timeout_ticks == 0) return nready; - if (poll_block_one_tick() < 0) + int w = poll_wait_tick(); + if (w < 0) return -EINTR; - if (timeout_ticks > 0) + if (w > 0 && timeout_ticks > 0) timeout_ticks--; } } @@ -215,9 +241,10 @@ sys_select(void) } return nready; } - if (poll_block_one_tick() < 0) + int w = poll_wait_tick(); + if (w < 0) return -EINTR; - if (timeout_ticks > 0) + if (w > 0 && timeout_ticks > 0) timeout_ticks--; } } @@ -287,9 +314,10 @@ sys_pselect6(void) } return nready; } - if (poll_block_one_tick() < 0) + int w = poll_wait_tick(); + if (w < 0) return -EINTR; - if (timeout_ticks > 0) + if (w > 0 && timeout_ticks > 0) timeout_ticks--; } } diff --git a/kernel/sys/sys_time.c b/kernel/sys/sys_time.c index 9175c19..412a3fa 100644 --- a/kernel/sys/sys_time.c +++ b/kernel/sys/sys_time.c @@ -10,14 +10,36 @@ #include "core/uaccess.h" #include "uapi/errno.h" +// Timer values in ticks, rounding a nonzero time up: POSIX says a timer set +// to 1 us goes off, not that it is disarmed. +static uint +tv_ticks(struct timeval *tv) +{ + if (tv->tv_sec < 0 || tv->tv_usec < 0) + return 0; + uint64 t = (uint64)tv->tv_sec * HZ + + ((uint64)tv->tv_usec + USEC_PER_TICK - 1) / USEC_PER_TICK; + return t > 0x7fffffff ? 0x7fffffff : (uint)t; +} + +static void +ticks_tv(uint t, struct timeval *tv) +{ + tv->tv_sec = t / HZ; + tv->tv_usec = (t % HZ) * USEC_PER_TICK; +} + +// The seconds left on the previous alarm, rounded up, as POSIX asks. addr_t sys_alarm(void) { int seconds; if (argint(0, &seconds) < 0) return -1; - myproc()->alarm_ticks = seconds * HZ; - return 0; + struct proc *p = myproc(); + uint left = alarm_left(p); + alarm_arm(p, seconds > 0 ? (uint)seconds * HZ : 0, 0); + return (left + HZ - 1) / HZ; } addr_t @@ -77,21 +99,52 @@ sys_clock_gettime(void) return 0; } -addr_t sys_getitimer(void) { return 0; } // stub +// ITIMER_REAL only. ITIMER_VIRTUAL and ITIMER_PROF count CPU time, which +// nothing here measures per tick, and are refused rather than accepted and +// never fired -- which is what all three used to be. +static int +itimer_get(struct proc *p, struct itimerval *iv) +{ + ticks_tv(p->alarm_every, &iv->it_interval); + ticks_tv(alarm_left(p), &iv->it_value); + return 0; +} + +addr_t +sys_getitimer(void) +{ + int which; + addr_t curp; + argint(0, &which); argaddr(1, &curp); + if (which != 0) + return -EINVAL; + struct itimerval iv; + itimer_get(myproc(), &iv); + if (copy_to_user(curp, &iv, sizeof(iv)) != 0) + return -EFAULT; + return 0; +} addr_t sys_setitimer(void) { int which; - addr_t newp; - argint(0, &which); argaddr(1, &newp); - // We don't implement interval timers; just track the alarm if it's ITIMER_REAL. - if (which == 0 && newp) { - struct itimerval iv; - if (copy_from_user(&iv, newp, sizeof(iv)) == 0) - myproc()->alarm_ticks = (int)(iv.it_value.tv_sec * HZ + - iv.it_value.tv_usec / USEC_PER_TICK); + addr_t newp, oldp; + argint(0, &which); argaddr(1, &newp); argaddr(2, &oldp); + if (which != 0) + return -EINVAL; + struct proc *p = myproc(); + struct itimerval iv; + if (oldp) { + itimer_get(p, &iv); + if (copy_to_user(oldp, &iv, sizeof(iv)) != 0) + return -EFAULT; } + if (newp == 0) + return 0; + if (copy_from_user(&iv, newp, sizeof(iv)) != 0) + return -EFAULT; + alarm_arm(p, tv_ticks(&iv.it_value), tv_ticks(&iv.it_interval)); return 0; } diff --git a/kernel/sys/table.inc b/kernel/sys/table.inc index 0002680..c2ac019 100644 --- a/kernel/sys/table.inc +++ b/kernel/sys/table.inc @@ -184,8 +184,8 @@ SYSCALL(102, 157, prctl) SYSCALL(103, 234, tgkill) SYSCALL(104, 200, tkill) SYSCALL(105, 34, pause) -SYSCALL(106, NO_LINUX, getitimer) -SYSCALL(107, NO_LINUX, setitimer) +SYSCALL(106, 36, getitimer) +SYSCALL(107, 38, setitimer) SYSCALL(108, 98, getrusage) SYSCALL(109, 100, times) SYSCALL(110, 99, sysinfo) @@ -296,6 +296,8 @@ SYSCALL(203, 281, epoll_pwait) SYSCALL(204, 282, signalfd) SYSCALL(205, 284, eventfd) SYSCALL(206, 73, flock) +SYSCALL(207, 51, getsockname) +SYSCALL(208, 52, getpeername) // --- mounts --- SYSCALL(185, 165, mount) diff --git a/ports/alpine/world b/ports/alpine/world index fab5aa1..bb9a788 100644 --- a/ports/alpine/world +++ b/ports/alpine/world @@ -16,3 +16,13 @@ ncurses-terminfo-base # The PCI and USB ID databases: lspci, fastfetch and Xorg read them. hwdata-pci hwdata-usb +# X on the framebuffer, with startx. /root/.xserverrc and .xinitrc (from +# mkaroot.sh) start it with -dumbSched and run Window Maker in it. +xorg-server +xf86-video-fbdev +xf86-input-evdev +xinit +# The desktop: Window Maker, a terminal for it, and a font for both. +windowmaker +xterm +font-dejavu diff --git a/ports/alpine/world.lock b/ports/alpine/world.lock index 90005b1..fd28a61 100644 --- a/ports/alpine/world.lock +++ b/ports/alpine/world.lock @@ -1,12 +1,91 @@ # Generated by tools/posix/alpine-lock.sh from world. Do not edit; # change world and run `make alpine-lock`. # file sha256 repository +brotli-libs-1.2.1-r0.apk 62a7e37e43bbfc1f8525a00ad2125fd5897135dc93b849733884cb78ffd8e26c main busybox-1.37.0-r31.apk 0e626fa97cf937fda7bba4ed4dde8e2f2dbb5083ac34e02180cec0c4eba9351b main busybox-binsh-1.37.0-r31.apk 93f803ae5db6b729c0f66f4afba6e907ccc17d7d5f8b8c3d25bdb4636de2c48a main +encodings-1.1.0-r0.apk f7c0eb762638d0249b4fbbe07a8e4ed6aab8e86b74c239c40e47a36219777d52 main +eudev-libs-3.2.14-r6.apk 45fdbcf37c5c0e82067e98a818d3c1fd16d0261c62128dcb6b935b86b4ec6c86 main +font-alias-1.0.6-r0.apk 754e5c1d19c2c69d5a2c3c02c7340cad7df3c26e63789516372ea164b564b927 main +fontconfig-2.17.1-r1.apk 7634cf2fb1066ec97c8f41ec90ec9f036540e056276b7bf9a10dc4ec9e44d238 main +font-cursor-misc-1.0.4-r1.apk a1a0c0659a9a48e24b4cc4b35583f4e57da9d2a0ae2cc8adb9be1143b66911b4 main +font-dejavu-2.37-r6.apk 6e7609e7a1aaed947afe4ddcc02bbdeec571dac5ff4ea1c04af9b8480b7b9dcf main +font-misc-misc-1.1.3-r1.apk 2c48f2a8529ac63243a0bdd5e7ab2329e0a8b8f996ffe95faed8b79fb05b4a52 main +freetype-2.14.3-r0.apk 09d76fde675a9ca3cfeb83d72fb09cb2e9ccf79b2937daa05a1af9a37a6b7815 main +gmp-6.3.0-r4.apk 85323ef5bce8ff234736dc2d6592ea37aebdc6348a531e27430bdfe38364d256 main hwdata-pci-0.408-r0.apk fe61404e6d2bd1e04c021d53641c0ebce97911084d77994e43a288a1278686c1 main hwdata-usb-0.408-r0.apk 272d365cb05dd085dd8022154cf71e3f3b0aaddd41c1269eeaf939c9d38675c8 main +libbsd-0.12.2-r0.apk 407597c04b41dbfe8a05b35d45a3453e32fae7df84aed3a06ff48dd4df1338b6 main +libbz2-1.0.8-r6.apk 2737e2b23ed08d4911ee62cd0163b5f4bf0379cacf4dc35b23a00209584c98fd main libcap2-2.78-r0.apk 42886a7b4bcde26347c712925c8dc7db202b6029ef9b8c7ee81577ab7bdd8fad main +libdrm-2.4.134-r0.apk 584fd84ad5030040e613fa1816097cd1706ddd542100070852b0eab6e09e12ca main +libelf-0.195-r0.apk 78c5a1993342ab1bdcdce30639fd0937e8000f6d0d85cea171f573d31decd036 main +libepoxy-1.5.10-r1.apk 0be13b4b524fe64315e0663ab615a244d75d7da73130ac1c630cd6096841444e main +libevdev-1.13.6-r0.apk b38a27a4ac55701fd2eb0032977af2664b6fc23b2f9c2cc6e893ad4452fd82d5 community +libexpat-2.9.0-r0.apk 32b127f31e0a919110a18a7f0bb3780d8fb66ba4d8d61b2d301d633f0e319d0e main +libffi-3.5.2-r1.apk 6da1a19313a40c56cac756fb11e2c4d5267831e1c22fc8323f24c943c94da6e0 main +libfontenc-1.1.9-r0.apk ea7f6d097653572991da749ac0764c2568d8902a01eea4236e56b10b90a7b0e3 main +libgcc-15.2.0-r5.apk 393dcd32629f06d7d85409c272d142d0c082772d10b87ef55ee82f47de3be637 main +libice-1.1.2-r0.apk 08daa589d57149b52b00c666b4009212aa3af95021ebdb2a359555c9bcaa76e1 main +libjpeg-turbo-3.1.3-r0.apk 6299627dd0a9156a7463511e4915a6294dfb592d8b4bdf85e55265adf1b8ea04 main +libmd-1.2.0-r0.apk f6d68d072fcefc8edd4ffe3afc341ca391f8a9a209130628fb065d98b18a2792 main libncursesw-6.6_p20260516-r0.apk cf8caa8a88bc4ce9d9e395567fefaf9ef7fbd55c50ae6155b35c1b58f3755023 main +libpciaccess-0.19-r0.apk 03fdccd1e2a0825e7f7fabbac67212e05d271d31d2eff44cc92f707e69908ad4 main +libpng-1.6.59-r0.apk d16aeb0597f2bc43e0583ac9ff779388ec160c3e22568b640122e94ba55be6d7 main +libsharpyuv-1.6.0-r0.apk 34621ff53f43030d24d1224fe9cb53b1590ab614cbce6080005cde703e6c874a main +libsm-1.2.6-r0.apk dd7eff58a46fd60e4afee9c7c1c3508920b0e1d6560129e59aee82329f68afc8 main +libstdc++-15.2.0-r5.apk 14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6 main +libuuid-2.42.3-r1.apk 8306e5bb577696c9069fe1dfd9e1dcc39d2d481c6a1b0e707fd03c3e21aa6aa2 main +libwebp-1.6.0-r0.apk dc50c0280348330bb113079331c4a35d85e28351c7baa9268ee346aad8398288 main +libx11-1.8.14-r0.apk 016efc189c6d566061ffe206ed0780e32686c1a8c832a3917d35d2c66d61a40f main +libxau-1.0.12-r0.apk 3506cdba7ed916a6f814a010fbb127dbfdae4147107f64a15f5fa2651c5d8510 main +libxaw-1.0.16-r1.apk 8ac469cddeac40ff2e91c84cb02ba526d7c62e30a79a25c2e3b6e5269236801d main +libxcb-1.17.0-r2.apk 7935d5ad9c7137b7b3f1292d62f55f7e0cc5c919155218c3060ea46503b3f211 main +libxcvt-0.1.3-r0.apk 809b454981887c1d1d835c909c46a80650ca72a73a92268a2cf8a666ad82b6b7 community +libxdmcp-1.1.5-r1.apk 2a93f44f3f85c1eda9ed53691c35d0742b5fcd8e0057cd7380934025936cbf6f main +libxext-1.3.7-r0.apk 8afe1ee6ab8cb656e12f20dc5b7fb2d4103faee4a38298b94f26ab52e597cbc8 main +libxfont2-2.0.8-r0.apk f8bf87b9ce835cb307eb8a3d270e226c4437757478c52ba3ad384cecc2ac6fef community +libxft-2.3.9-r0.apk eba6fd85599b4fedf3acdd3ad3e2eec84ea97a5ba8f1ac570fa1ee37a7d81420 main +libxinerama-1.1.6-r0.apk 9f780efb3cf675b5253b73b84f278b40757fa7869c6fe70a9790a73f4f7e810f main +libxkbfile-1.2.0-r0.apk e575c6c314ad87e74887526aca3b1429f5c1d6290e66f2945d4053f387beea57 main +libxml2-2.13.9-r2.apk 4b2f986159c659f014b942fe8a5d70c67af475153ca826c3dd53389eea46a300 main +libxmu-1.3.1-r0.apk 5d0b06d1a4b38ac0e070e0dad5e61583e5e147e378ba50418c83e1be4b19986c main +libxpm-3.5.20-r0.apk 1ed312a00abac5a5840c1f3721921ea8183e664566c19763ed00aa9f35bb2178 main +libxrender-0.9.13-r0.apk 724aa6c6353e70869e0eb5aa5461e8fc0753b99db765de47a3ae238573dbdcf8 main +libxshmfence-1.3.3-r0.apk 53790552de7eea17a47aa4c082b06b735c545a0947c41128f97c7be95a7d3d92 main +libxt-1.3.1-r0.apk 08b167b05ac7dfb892421876cc2467214208b83b2960a9f49e84f9dbcf36e812 main +libxxf86vm-1.1.7-r0.apk 887a3c5438e1db624d07ac23d16ec08d21fb16de438d75ad2dd5637c958b4108 main +llvm22-libs-22.1.3-r1.apk dec15bdd29b6ec7fc22aee43bac153a22e6b132c599e5e5d9ccd0d159eae764f main +mcookie-2.42.3-r1.apk 64f86340991e2f283ce95b1bae21157da6849118cd9f1a2931de5c73d0f9fc3d main +mesa-26.1.6-r0.apk 8fb0e3d386f09df4f3e3643108b38197558465b2558e6d66d6a9c055adc8d1a9 main +mesa-egl-26.1.6-r0.apk ccdd25c8f3a069b018a39a5641e4f13ab7ceb511d5991e9f8fb18bd551172ffa main +mesa-gbm-26.1.6-r0.apk db6b00543e7e686f377a5c250036067742f914112839255e2e5edbbc241bfb0d main +mesa-gl-26.1.6-r0.apk 4d2d502a3938bd5dc4affec13545271571bb8bec40e7078ef01f34222bfb5d5d main +mesa-gles-26.1.6-r0.apk 2544a75e74778ab9ddcecfc8d9dc35375cb3a4ac3661e94a1156e2c528d7b59b main +mkfontscale-1.2.4-r0.apk 2385a7aad3971fcad9d862cb90937e643e633b14bf4b91f8b8ce3040a1468d20 main +mtdev-1.1.7-r0.apk 004f42bc67bc0eb573d64d802c5f0325f49013cf4aa7576d4b571399df46f3f7 community musl-1.2.6-r3.apk e142255d0fa397f14318996bc10378093dcc069dc3dd1b6818c8f28a789c21da main ncurses-terminfo-base-6.6_p20260516-r0.apk ce83be6d0bd10584a53c3a5868cc1595ce5f0f8e0b621f8f7f70b6144e521fc9 main +nettle-3.10.2-r0.apk 693c1a99b781cd17149916df30113ccd6bdddac130298c17b1286fd2ae438386 main +pixman-0.46.4-r0.apk 378550cbe348dda9b0733014375dba87a89f0077c5f3dbeba7dce8dbcdda707e main +pkgconf-2.5.1-r0.apk 20cc47b76b82e9424a35ecc4fdd7bd904ef49ada1684214910fae31bc0cab737 main +spirv-tools-1.4.341.0-r0.apk 6034c22c4f61fb76e517c1d5b11ef5c26c7154d2baeacec8b0a9dcca94ff9a93 main +tiff-4.7.2-r0.apk 757ce87ebe4923a6be9869d026e250958ca09696529c464781e8a91d2b81670b main +util-macros-1.20.2-r0.apk 7c129d085b646b13ccef3738e93038221aec840c1a6764279c5adc9950ba1bdc main +wayland-libs-client-1.25.0-r0.apk a728f7103217cafdc83ff6ad0d679c197dbbe280bd5788c8336259f7a39cb303 main +windowmaker-0.96.0-r0.apk 15b500d9afccf94a385e042b01ed9b1d709b1627d97597cea59bf63d0bab4f6b community +xauth-1.1.5-r0.apk 94e80e9687a1829a6fe95fb28d4b08a5531dab182cde8a58ff1b370b2cd293fb community +xf86-input-evdev-2.11.0-r0.apk 1f3fcd3e837b43ad5e32005e03095b547ddbfc25973f0511e9ff74bf342d2fe0 community +xf86-video-fbdev-0.5.0-r6.apk 6a8a1b16f82d8810896f00c00b356c8b8d923e9aa0ce64737ae8274ca82f507a community +xinit-1.4.4-r0.apk efb5dc1e7929824f5add7d5d51d0ecb34ecfd0a773dfca4f061adc8a353b234f community +xkbcomp-1.5.0-r0.apk 8bfbd3c8ce7180e8cf599d5a0b791993b61fa98f3a19c47aaf5fcbe30c12d0ba main +xkeyboard-config-2.47-r0.apk 8c8f9af2b78f8e955480aa88103e495d2637edac659a417075e877c88a41fed0 main +xmodmap-1.0.11-r1.apk 42489c8865d401e894b8da210f5c8e27f58c1d72c83250a505a65bdb8c45bec0 community +xorg-server-21.1.25-r0.apk 8130c341a5f047069cb35ac77a44d8bab6a2e3cb5a7d0745e3c5096b1909ecfb community +xorg-server-common-21.1.25-r0.apk bf5994707b4030f249ad44a123ad2686501c82d2002d73d0a641d9001caa37c3 community +xrdb-1.2.2-r0.apk 29649088693b5974dec3f928a5303e354f7b91e381b778006fe3011d003b1369 community +xterm-410-r0.apk eff11da325ffb1dcfa4c9902ef27d34cc27b76457e1fc8dad90f10944ecb33b8 community +xz-libs-5.8.4-r0.apk ab362dd515ead04c31a6c302163248da7aa4813e283c971e6dd3f789b453a1d9 main +zlib-1.3.2-r1.apk 63aeea03c15a2f9018f81805cfc8aa926bdf5cd68921f22149c2fbb5d0ee9f47 main zsh-5.9-r7.apk 22ec3c4de69e38437e351ba5a881f5d88e9b518a248c8aa06d36b148cf16ce32 main +zstd-libs-1.5.7-r2.apk 23c6065b0049b2406441564bcf0032515a43f78e80d76fcb85535a3803ef5d4e main diff --git a/ports/zstd/Makefile b/ports/zstd/Makefile new file mode 100644 index 0000000..33fca6b --- /dev/null +++ b/ports/zstd/Makefile @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# zstd -- the decompressor behind SCFF, the compressed live root. Dual +# BSD-3-Clause / GPL-2.0; see LICENSE and COPYING in the tarball and +# docs/licensing.md. +# +# Kernel code, like uACPI: nothing is installed into the sysroot. This +# directory fetches and unpacks a pinned release, and the top-level Makefile +# compiles the decoder half of lib/ straight into the kernel against +# kernel/scff/compat, which stands in for the libc headers it includes. The +# host side, tools/mkscff, links the system's libzstd. +# +# Chosen for what SCFF asks of a codec: compress as hard as the build likes, +# once, and decompress at memory speed on every first read of a block. + +include ../mk/port.mk + +ZSTD_VERSION := 1.5.7 +ZSTD_URL := https://github.com/facebook/zstd/releases/download/v$(ZSTD_VERSION)/zstd-$(ZSTD_VERSION).tar.gz +ZSTD_SHA256 := eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3 + +TARBALL := $(DISTDIR)/zstd-$(ZSTD_VERSION).tar.gz + +$(TARBALL): | $(DISTDIR) + curl -fsSL -o $@ $(ZSTD_URL) + sh $(SHA256CHECK) "$(ZSTD_SHA256)" "$@" + +download: $(TARBALL) + +$(SRCDIR): $(TARBALL) + mkdir -p $(PORTDIR) + cd $(PORTDIR) && tar xzf $(abspath $<) + mv $(PORTDIR)/zstd-$(ZSTD_VERSION) $@ + +# The kernel build depends on this directory existing, nothing more. +all: | $(SRCDIR) + +install: + +clean: + rm -rf $(PORTDIR) + +distclean: clean + rm -f $(TARBALL) diff --git a/shell.nix b/shell.nix index 0da3540..2a63eb7 100644 --- a/shell.nix +++ b/shell.nix @@ -16,6 +16,14 @@ pkgs.mkShell { name = "schos"; + # Off, all of it. Nix's compiler wrapper adds -fPIC, -fstack-protector, + # -D_FORTIFY_SOURCE and friends to every compile, which is right for Linux + # programs and wrong for everything here: the kernel is freestanding and + # linked at fixed addresses, and the UEFI loader targets PE/COFF, where clang + # refuses -fPIC outright ("unsupported option '-fPIC' for target + # 'x86_64-unknown-windows-msvc'") and `make` stops at BOOTX64.EFI. + hardeningDisable = [ "all" ]; + nativeBuildInputs = with pkgs; [ # The cross-compiler, and the reason this file exists. clang targets # bare-metal ELF, 16-bit code and PE/COFF from one install; lld-link is what diff --git a/tools/mkscff.c b/tools/mkscff.c new file mode 100644 index 0000000..64d34d1 --- /dev/null +++ b/tools/mkscff.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// mkscff: write a block image as SCFF, the SchOS Compressed File Format. +// +// mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N] +// +// The layout is include/fs/scff.h. Chunks are compressed on every core, since +// zstd's high levels are slow and the image is hundreds of megabytes, and each +// one is decompressed again and compared before it is written: an image the +// kernel cannot read is found here rather than at boot. +// +// Built against the zstd release the kernel's decoder comes from (ports/zstd), +// not the host's libzstd, so both sides are one version and nothing needs to +// be installed for it. + +#include +#include +#include +#include +#include +#include + +#include "fs/scff.h" +#include "zstd.h" + +static unsigned char *img; +static uint64_t img_bytes; +static uint32_t chunk_bytes = 128 * 1024; +static int level = 19; +static uint32_t nchunks; + +static unsigned char **out; // chunk i as stored, or 0 for an all-zero one +static size_t *outlen; +static uint32_t next_chunk; +static pthread_mutex_t next_lock = PTHREAD_MUTEX_INITIALIZER; + +static void +die(const char *what) +{ + fprintf(stderr, "mkscff: %s\n", what); + exit(1); +} + +static int +all_zero(const unsigned char *p, size_t n) +{ + for (size_t i = 0; i < n; i++) + if (p[i]) + return 0; + return 1; +} + +static void * +worker(void *arg) +{ + (void)arg; + ZSTD_CCtx *cc = ZSTD_createCCtx(); + ZSTD_DCtx *dc = ZSTD_createDCtx(); + size_t cap = ZSTD_compressBound(chunk_bytes); + unsigned char *buf = malloc(cap), *back = malloc(chunk_bytes); + + if (!cc || !dc || !buf || !back) + die("out of memory"); + ZSTD_CCtx_setParameter(cc, ZSTD_c_compressionLevel, level); + ZSTD_CCtx_setParameter(cc, ZSTD_c_checksumFlag, 1); + + for (;;) { + pthread_mutex_lock(&next_lock); + uint32_t i = next_chunk++; + pthread_mutex_unlock(&next_lock); + if (i >= nchunks) + break; + + const unsigned char *src = img + (uint64_t)i * chunk_bytes; + size_t n = img_bytes - (uint64_t)i * chunk_bytes; + if (n > chunk_bytes) + n = chunk_bytes; + + if (all_zero(src, n)) { + outlen[i] = 0; + continue; + } + size_t c = ZSTD_compress2(cc, buf, cap, src, n); + if (ZSTD_isError(c)) + die(ZSTD_getErrorName(c)); + if (c >= n) { // did not compress: stored as it is + out[i] = malloc(n); + memcpy(out[i], src, n); + outlen[i] = n; + continue; + } + size_t d = ZSTD_decompressDCtx(dc, back, chunk_bytes, buf, c); + if (ZSTD_isError(d) || d != n || memcmp(back, src, n) != 0) + die("a chunk did not decompress to itself"); + out[i] = malloc(c); + memcpy(out[i], buf, c); + outlen[i] = c; + } + ZSTD_freeCCtx(cc); + ZSTD_freeDCtx(dc); + free(buf); + free(back); + return 0; +} + +int +main(int argc, char **argv) +{ + const char *in = 0, *outpath = 0; + long threads = sysconf(_SC_NPROCESSORS_ONLN); + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--chunk") && i + 1 < argc) + chunk_bytes = (uint32_t)atoi(argv[++i]) * 1024; + else if (!strcmp(argv[i], "--level") && i + 1 < argc) + level = atoi(argv[++i]); + else if (!strcmp(argv[i], "--threads") && i + 1 < argc) + threads = atol(argv[++i]); + else if (!in) + in = argv[i]; + else if (!outpath) + outpath = argv[i]; + else + die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]"); + } + if (!in || !outpath) + die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]"); + if (chunk_bytes < 4096 || chunk_bytes > SCFF_CHUNK_MAX || + (chunk_bytes & (chunk_bytes - 1))) + die("--chunk must be a power of two from 4 to 1024 KB"); + if (threads < 1) + threads = 1; + + FILE *f = fopen(in, "rb"); + if (!f) + die(strerror(errno)); + fseek(f, 0, SEEK_END); + img_bytes = (uint64_t)ftell(f); + fseek(f, 0, SEEK_SET); + if (img_bytes == 0 || img_bytes % 4096) + die("the image is not a whole number of 4 KB blocks"); + if (!(img = malloc(img_bytes)) || fread(img, 1, img_bytes, f) != img_bytes) + die("cannot read the image"); + fclose(f); + + nchunks = (uint32_t)((img_bytes + chunk_bytes - 1) / chunk_bytes); + out = calloc(nchunks, sizeof(*out)); + outlen = calloc(nchunks, sizeof(*outlen)); + if (!out || !outlen) + die("out of memory"); + + pthread_t *t = calloc((size_t)threads, sizeof(*t)); + for (long i = 0; i < threads; i++) + pthread_create(&t[i], 0, worker, 0); + for (long i = 0; i < threads; i++) + pthread_join(t[i], 0); + + struct scff_header h; + memset(&h, 0, sizeof(h)); + memcpy(h.magic, SCFF_MAGIC, 4); + h.version = SCFF_VERSION; + h.codec = SCFF_CODEC_ZSTD; + h.chunk_bytes = chunk_bytes; + h.nchunks = nchunks; + h.image_bytes = img_bytes; + h.index_off = sizeof(h); + + uint64_t *index = malloc(((size_t)nchunks + 1) * sizeof(uint64_t)); + uint64_t pos = sizeof(h) + ((uint64_t)nchunks + 1) * sizeof(uint64_t); + for (uint32_t i = 0; i < nchunks; i++) { + index[i] = pos; + pos += outlen[i]; + } + index[nchunks] = pos; + h.stored_bytes = pos; + + if (!(f = fopen(outpath, "wb"))) + die(strerror(errno)); + fwrite(&h, sizeof(h), 1, f); + fwrite(index, sizeof(uint64_t), (size_t)nchunks + 1, f); + for (uint32_t i = 0; i < nchunks; i++) + if (outlen[i]) + fwrite(out[i], 1, outlen[i], f); + if (fclose(f) != 0) + die("cannot write the output"); + + printf("mkscff: %llu KB -> %llu KB (%.1f%%), %u chunks of %u KB, zstd -%d\n", + (unsigned long long)(img_bytes >> 10), (unsigned long long)(pos >> 10), + 100.0 * (double)pos / (double)img_bytes, nchunks, chunk_bytes >> 10, level); + return 0; +} diff --git a/tools/posix/mkaroot.sh b/tools/posix/mkaroot.sh index d3499cc..4d2e71e 100644 --- a/tools/posix/mkaroot.sh +++ b/tools/posix/mkaroot.sh @@ -97,6 +97,77 @@ mkdir -p "$P/etc/profile.d" grep -v '^export \(PATH\|HOME\|TERM\)=' "$P/etc/profile" > "$P/etc/profile.d/schos.sh" rm -f "$P/etc/profile" +# How startx starts the X server for root: Alpine's xserverrc, plus +# -dumbSched. Xorg reads input on a thread of its own, SchOS has no threads +# yet, and the server crashed on its way out the first time a client +# disconnected, in pthread_join on a thread that was never made. -dumbSched is +# Xorg's own switch for input on the main loop. The file is root's own rather +# than /etc/X11/xinit/xserverrc, which belongs to the xinit package. +mkdir -p "$P/root" +cat > "$P/root/.xserverrc" <<'XEOF' +#!/bin/sh +# The VT X runs on is the one startx was started from. startx works that out +# only when `uname -s` says Linux, and SchOS's says SchIFT. +vt=$(tty | sed -n 's|^/dev/tty\([0-9][0-9]*\)$|vt\1|p') +exec /usr/bin/X -nolisten tcp -dumbSched $vt "$@" +XEOF + +# What startx runs inside it: Window Maker, which world installs. Without this +# xinit falls back to twm and an xterm, neither of which is in the image. +cat > "$P/root/.xinitrc" <<'XEOF' +#!/bin/sh +exec wmaker +XEOF + +# X's input devices, named rather than found. Alpine's Xorg asks udev for +# them, SchOS has no udev, and X came up with no keyboard and no mouse: the +# pointer did not move, Ctrl+Alt+F-keys did nothing and nothing could end the +# session. SchOS has exactly two input nodes whatever the machine -- every +# keyboard on event0, every mouse on event1 (kernel/dev/input.c) -- so a +# fixed list is the whole answer. The file is SchOS's, in xorg.conf.d, which +# no Alpine package writes into. +mkdir -p "$P/etc/X11/xorg.conf.d" +cat > "$P/etc/X11/xorg.conf.d/10-schos.conf" <<'XEOF' +# SchOS has no udev, so X is told its input devices instead of finding them. +# event0 is every keyboard and event1 every mouse (kernel/dev/input.c). +Section "ServerFlags" + Option "AutoAddDevices" "false" + # Ctrl+Alt+Backspace ends X. + Option "DontZap" "false" +EndSection + +Section "InputDevice" + Identifier "keyboard" + Driver "evdev" + Option "Device" "/dev/input/event0" + Option "XkbRules" "evdev" + Option "XkbOptions" "terminate:ctrl_alt_bksp" +EndSection + +Section "InputDevice" + Identifier "mouse" + Driver "evdev" + Option "Device" "/dev/input/event1" +EndSection + +Section "Device" + Identifier "fb" + Driver "fbdev" +EndSection + +Section "Screen" + Identifier "screen" + Device "fb" +EndSection + +Section "ServerLayout" + Identifier "schos" + Screen "screen" + InputDevice "keyboard" "CoreKeyboard" + InputDevice "mouse" "CorePointer" +EndSection +XEOF + # Mount points and runtime directories are alpine-baselayout's; an empty # directory here only duplicates it. for d in dev proc sys run tmp srv; do @@ -110,6 +181,9 @@ sed -i 's|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|' /etc/passwd for s in /bin/zsh /bin/mksh; do grep -qx "$s" /etc/shells 2>/dev/null || echo "$s" >> /etc/shells done +# The machine's own name, which Alpine's hosts leaves out. Without it xauth +# cannot resolve "schos:0", and startx says "bad display name" four times. +grep -qw schos /etc/hosts || printf '127.0.1.1\tschos\n' >> /etc/hosts exit 0 EOF cp "$PKG/post-install" "$PKG/post-upgrade" @@ -157,7 +231,8 @@ ROOT_PI=$ROOT sh -c ' sed -i "s|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|" "$ROOT_PI/etc/passwd" for s in /bin/zsh /bin/mksh; do grep -qx "$s" "$ROOT_PI/etc/shells" 2>/dev/null || echo "$s" >> "$ROOT_PI/etc/shells" - done' + done + grep -qw schos "$ROOT_PI/etc/hosts" || printf "127.0.1.1\tschos\n" >> "$ROOT_PI/etc/hosts"' # The repository goes in too, so the installed system can reinstall and # upgrade schos-base, and Alpine's are listed for everything else. diff --git a/tools/posix/mkiso.sh b/tools/posix/mkiso.sh index 4cf408a..09c120a 100644 --- a/tools/posix/mkiso.sh +++ b/tools/posix/mkiso.sh @@ -3,11 +3,16 @@ # # tools/posix/mkiso.sh [output.iso] # -# The disc carries one El Torito boot entry and a real directory tree: +# The disc carries one El Torito boot entry and the loader and kernel as files: # # /boot/efiboot.img a FAT image holding the loader and the kernel # /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc -# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy +# /schos/kernel the same kernel, for chainloaders (below) +# +# The root filesystem is inside the kernel, as its ramdisk. The disc used to +# carry it a third time as an ISO 9660 tree, which nothing reads -- SchOS has +# no ISO 9660 driver -- and once Xorg was in the root that copy alone was +# 360 MB. # # --- Why only one entry ----------------------------------------------------- # @@ -69,7 +74,6 @@ cd "$repo" BUILD=${BUILD:-build} STAGING="$BUILD/iso" -SYSROOT=${SYSROOT:-sysroot} KERNEL="$BUILD/kernel/kernel" EFIAPP="$BUILD/uefi/BOOTX64.EFI" @@ -113,9 +117,6 @@ cp "$KERNEL" "$STAGING/schos/kernel" # this FAT image -- not the ISO 9660 tree around it. Without the kernel in here # the loader comes up and halts with EFI_NOT_FOUND, which is what it did. # -# The ISO 9660 tree still carries the sysroot for a running system to read; the -# loader's own two files live in here because that is the volume it is handed. -# # Sized from what actually goes in plus slack, rather than fixed: a hardcoded # size is a thing that breaks quietly the first time the kernel grows. EFI_IMG="$STAGING/boot/efiboot.img" @@ -145,15 +146,6 @@ if true; then mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel fi -# The sysroot, as an ordinary hierarchy. Copied rather than linked so that -# whatever ends up on the disc is a snapshot, not a view of a tree that may be -# rebuilt underneath it. -if [ -d "$SYSROOT" ]; then - for d in "$SYSROOT"/*; do - [ -e "$d" ] || continue - cp -a "$d" "$STAGING/" - done -fi # xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags # below unchanged -- xorriso's -as mkisofs mode exists precisely to be @@ -173,6 +165,11 @@ if command -v xorriso >/dev/null 2>&1; then # of type 0xEF is what every UEFI firmware looks for on a removable # disk, MBR rather than GPT because the oldest ones read that best. HYBRID="-append_partition 2 0xef $EFI_IMG" + # El Torito keeps its own copy of the image in the tree, the layout real + # machines have booted. Pointing it into the partition instead + # (--interval:appended_partition_2:all::, as Arch does) saves a copy, and + # an ASUS laptop stopped listing the stick the day that was tried. With + # the root compressed (SCFF) the copy is only the kernel's size. elif command -v mkisofs >/dev/null 2>&1; then MKISOFS="mkisofs" HYBRID="" diff --git a/tools/windows/bootcheck.ps1 b/tools/windows/bootcheck.ps1 index 32645cb..f94c304 100644 --- a/tools/windows/bootcheck.ps1 +++ b/tools/windows/bootcheck.ps1 @@ -495,8 +495,11 @@ try { Start-Sleep -Milliseconds 150 $t = Read-Log if ($null -eq $t -or $t.Length -le $Since) { continue } - # A prompt at the very end, with nothing typed after it. - if ($t.Substring($Since) -match '(?s)#[ ]?$') { return $true } + # A prompt at the very end, with nothing typed after it: mksh's `#`, + # or zsh's `»`, which root's shell has been since the root became + # Alpine's. zsh follows it with erase-to-end-of-line and turns + # bracketed paste on, so both escapes may trail the prompt. + if ($t.Substring($Since) -match '(?s)(#|\u00bb)(\e\[[0-9;]*m)*[ ]?(\e\[K)?(\e\[\?2004h)?$') { return $true } } return $false } @@ -629,6 +632,12 @@ if ($null -eq $text) { $text = '' } Write-Host "==> serial transcript ($($text.Length) bytes) -> $log" -ForegroundColor Cyan Write-Host $text +# Matched without terminal escapes. busybox's ls, which is the Alpine root's, +# colours what it lists -- `copy.txt` arrives as ESC[1;32mcopy.txt -- and zsh +# wraps every prompt in them, so a pattern anchored at a line start found +# nothing in output that was perfectly right. Shown raw above, matched clean. +$text = $text -replace '\e\[[0-9;?]*[A-Za-z]', '' -replace '\e[()][0-9A-Za-z]', '' -replace '[\x0e\x0f]', '' + $script:fail = $false function Expect($pattern, $what) { if ($script:text -match $pattern) { @@ -697,7 +706,10 @@ Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem' Expect 'init: ready' 'init came up on a SchFS root' Expect 'label:\s*schos' '/proc/schfs reports the volume label' Expect 'root_objectid:\s*16' '/proc/schfs reports the root object id' -Expect 'dir\s+bin' 'the root directory really contains bin/' +# /proc/schfs lists the first 16 entries, in the tree's order, and on the +# Alpine root bin/ is the seventeenth: any of the top-level directories proves +# the same thing. +Expect '(?m)^\s*dir\s+(bin|etc|usr)\s' 'the root directory really contains bin/, etc/ or usr/' # / is the mounted volume, so mounting that same device again must be # refused. Without this a kernel that had quietly fallen back to some other # root would still pass everything else here.