Merge pull request #1 from dualfroz/feat/pending-work-2026-09

feat: land pending September work
This commit is contained in:
dualfroz authored and GitHub committed 2026-09-14 03:26:50 +02:00
commit 9555fc4b88
176 files changed
+33949 -219

No files matched your search

+1
View File
@@ -3,3 +3,4 @@ os.iso
iso/boot/frostbyte.elf iso/boot/frostbyte.elf
*.o *.o
*.elf *.elf
iso/boot/initrd.img
+575 -8
View File
@@ -6,7 +6,7 @@ CFLAGS := -std=gnu11 -ffreestanding -nostdlib \
-fno-asynchronous-unwind-tables \ -fno-asynchronous-unwind-tables \
-mno-red-zone -mno-mmx -mno-sse -mno-sse2 \ -mno-red-zone -mno-mmx -mno-sse -mno-sse2 \
-mcmodel=kernel \ -mcmodel=kernel \
-Wall -Wextra -Iinclude -g -c -Wall -Wextra -Iinclude -MMD -MP -g -c
ASFLAGS := -ffreestanding -Iinclude -g -c ASFLAGS := -ffreestanding -Iinclude -g -c
@@ -16,7 +16,8 @@ UCFLAGS := -std=gnu11 -ffreestanding -nostdlib \
-fno-stack-protector -fno-pic -fno-pie \ -fno-stack-protector -fno-pic -fno-pie \
-fno-asynchronous-unwind-tables \ -fno-asynchronous-unwind-tables \
-mno-red-zone -mno-mmx -mno-sse -mno-sse2 \ -mno-red-zone -mno-mmx -mno-sse -mno-sse2 \
-Wall -Wextra -Iinclude -Iuser -g -c -mcmodel=large \
-Wall -Wextra -Iinclude -Iuser -MMD -MP -g -c
UASFLAGS := -ffreestanding -Iinclude -Iuser -g -c UASFLAGS := -ffreestanding -Iinclude -Iuser -g -c
@@ -33,7 +34,55 @@ OBJS := $(SOBJS) $(COBJS)
USERCOMMON := $(UBUILD)/crt.o $(UBUILD)/ulib.o USERCOMMON := $(UBUILD)/crt.o $(UBUILD)/ulib.o
USERPROGRAMS := $(UBUILD)/hello.elf $(UBUILD)/counter.elf \ USERPROGRAMS := $(UBUILD)/hello.elf $(UBUILD)/counter.elf \
$(UBUILD)/sbrktest.elf $(UBUILD)/fileread.elf $(UBUILD)/sbrktest.elf $(UBUILD)/fileread.elf \
$(UBUILD)/pipetest.elf $(UBUILD)/disktest.elf \
$(UBUILD)/fstest.elf $(UBUILD)/linktest.elf \
$(UBUILD)/spawntarget.elf $(UBUILD)/exectarget.elf \
$(UBUILD)/proctest.elf $(UBUILD)/argtest.elf \
$(UBUILD)/fifomaster.elf $(UBUILD)/fifowriter.elf \
$(UBUILD)/fiforeader.elf $(UBUILD)/killtest.elf \
$(UBUILD)/killvictim.elf $(UBUILD)/namedreader.elf \
$(UBUILD)/namedwriter.elf $(UBUILD)/consolereader1.elf \
$(UBUILD)/consolereader2.elf $(UBUILD)/sigtest.elf \
$(UBUILD)/sigtarget.elf $(UBUILD)/sigdefault.elf \
$(UBUILD)/jobtest.elf $(UBUILD)/jobtarget.elf \
$(UBUILD)/nesttest.elf $(UBUILD)/nesttarget.elf \
$(UBUILD)/eintrtest.elf $(UBUILD)/eintrtarget.elf \
$(UBUILD)/restarttest.elf $(UBUILD)/restarttarget.elf \
$(UBUILD)/masktest.elf $(UBUILD)/masktarget.elf \
$(UBUILD)/sigchldtest.elf $(UBUILD)/sigchldchild.elf \
$(UBUILD)/waitanytest.elf $(UBUILD)/waitanyfast.elf \
$(UBUILD)/waitanyslow.elf \
$(UBUILD)/stopchldtest.elf $(UBUILD)/stopchldtarget.elf \
$(UBUILD)/nodefertest.elf \
$(UBUILD)/alarmtest.elf $(UBUILD)/alarmdefault.elf \
$(UBUILD)/siginfotest.elf $(UBUILD)/siginfotarget.elf \
$(UBUILD)/pgroupfg.elf $(UBUILD)/pgroupbg.elf \
$(UBUILD)/jobctlfg.elf $(UBUILD)/jobctlbg.elf \
$(UBUILD)/polltest.elf $(UBUILD)/pollwriter.elf \
$(UBUILD)/pipesrc.elf $(UBUILD)/pipesink.elf \
$(UBUILD)/redirwriter.elf $(UBUILD)/redirreader.elf \
$(UBUILD)/stderrwriter.elf $(UBUILD)/stderrreader.elf \
$(UBUILD)/selfstoptest.elf $(UBUILD)/selfstoptarget.elf \
$(UBUILD)/herestest.elf \
$(UBUILD)/hcat.elf $(UBUILD)/hsink.elf \
$(UBUILD)/pipecat.elf \
$(UBUILD)/cwdtest.elf $(UBUILD)/cwdtarget.elf \
$(UBUILD)/envprint.elf \
$(UBUILD)/nicetest.elf $(UBUILD)/nicetarget.elf \
$(UBUILD)/date.elf \
$(UBUILD)/globcat.elf \
$(UBUILD)/timetest.elf \
$(UBUILD)/false.elf \
$(UBUILD)/argecho.elf \
$(UBUILD)/rmtest.elf \
$(UBUILD)/permtest.elf \
$(UBUILD)/identtest.elf \
$(UBUILD)/dirpermtest.elf \
$(UBUILD)/grouptest.elf \
$(UBUILD)/pidfdtest.elf \
$(UBUILD)/mmaptest.elf $(UBUILD)/mmapprotvictim.elf \
$(UBUILD)/mmapexecvictim.elf
FSFILES := fsroot/etc/motd.txt fsroot/etc/readme.txt fsroot/home/notes.txt FSFILES := fsroot/etc/motd.txt fsroot/etc/readme.txt fsroot/home/notes.txt
@@ -41,6 +90,8 @@ KERNEL := $(BUILD)/frostbyte.elf
INITRD := $(BUILD)/initrd.img INITRD := $(BUILD)/initrd.img
MKTAR := $(BUILD)/mktar MKTAR := $(BUILD)/mktar
ISO := os.iso ISO := os.iso
DISK := $(BUILD)/disk.img
DISKFLAGS := -drive file=$(DISK),format=raw,if=ide
.PHONY: all clean run runserial test .PHONY: all clean run runserial test
@@ -79,6 +130,430 @@ $(UBUILD)/sbrktest.elf: $(UBUILD)/sbrktest.o $(USERCOMMON) user/user.ld
$(UBUILD)/fileread.elf: $(UBUILD)/fileread.o $(USERCOMMON) user/user.ld $(UBUILD)/fileread.elf: $(UBUILD)/fileread.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x70000000 -o $@ $(UBUILD)/fileread.o $(USERCOMMON) $(LD) $(ULDFLAGS) --defsym USER_BASE=0x70000000 -o $@ $(UBUILD)/fileread.o $(USERCOMMON)
$(UBUILD)/pipetest.elf: $(UBUILD)/pipetest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x80000000 -o $@ $(UBUILD)/pipetest.o $(USERCOMMON)
$(UBUILD)/disktest.elf: $(UBUILD)/disktest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x90000000 -o $@ $(UBUILD)/disktest.o $(USERCOMMON)
$(UBUILD)/fstest.elf: $(UBUILD)/fstest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0xA0000000 -o $@ $(UBUILD)/fstest.o $(USERCOMMON)
$(UBUILD)/linktest.elf: $(UBUILD)/linktest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0xB0000000 -o $@ $(UBUILD)/linktest.o $(USERCOMMON)
# Note: 0xC0000000 is deliberately skipped -- kmain.c's runPagingTest()
# hardcodes a single supervisor-only diagnostic page at that exact address
# for the whole kernel's lifetime, which would silently collide with a
# user binary's first page (elfLoad's pagingMapUserRange only maps pages
# that are not already present, so it would find that one "already mapped"
# and leave it non-user-accessible, faulting the instant ring 3 tried to
# execute from it).
$(UBUILD)/spawntarget.elf: $(UBUILD)/spawntarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0xD0000000 -o $@ $(UBUILD)/spawntarget.o $(USERCOMMON)
# exectarget is never listed in kmain.c's boot-time spawnUser calls -- it is
# only ever reached via proctest.c's userExec, which derives its stack/brk
# purely from this ELF's own entry point (elfSlotStackTop/elfSlotBrkStart,
# elf.c/elf.h). Its USER_BASE still needs to be reserved here so it never
# collides with another binary's slot.
$(UBUILD)/exectarget.elf: $(UBUILD)/exectarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0xE0000000 -o $@ $(UBUILD)/exectarget.o $(USERCOMMON)
$(UBUILD)/proctest.elf: $(UBUILD)/proctest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0xF0000000 -o $@ $(UBUILD)/proctest.o $(USERCOMMON)
# Like exectarget, argtest is never in kmain.c's boot-time spawnUser list --
# only proctest.c's userSpawn (with a specific argv) ever launches it. Its
# slot picks up right where proctest's leaves off (0xF0000000 + elfSlotSize).
$(UBUILD)/argtest.elf: $(UBUILD)/argtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x100000000 -o $@ $(UBUILD)/argtest.o $(USERCOMMON)
# fifomaster IS in kmain.c's boot-time spawnUser list (it needs no argv of
# its own); fifowriter/fiforeader are only ever spawned by fifomaster, each
# with the shared pipe handle it created as their one argv.
$(UBUILD)/fifomaster.elf: $(UBUILD)/fifomaster.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x110000000 -o $@ $(UBUILD)/fifomaster.o $(USERCOMMON)
$(UBUILD)/fifowriter.elf: $(UBUILD)/fifowriter.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x120000000 -o $@ $(UBUILD)/fifowriter.o $(USERCOMMON)
$(UBUILD)/fiforeader.elf: $(UBUILD)/fiforeader.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x130000000 -o $@ $(UBUILD)/fiforeader.o $(USERCOMMON)
# killtest is in kmain.c's boot-time spawnUser list; killvictim is only ever
# spawned by killtest, and is designed to never return on its own (see
# killvictim.c) -- proving threadKill (sched.c) is the only way out.
$(UBUILD)/killtest.elf: $(UBUILD)/killtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x140000000 -o $@ $(UBUILD)/killtest.o $(USERCOMMON)
$(UBUILD)/killvictim.elf: $(UBUILD)/killvictim.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x150000000 -o $@ $(UBUILD)/killvictim.o $(USERCOMMON)
# namedreader/namedwriter are both in kmain.c's boot-time spawnUser list,
# with no parent-child relationship to each other -- they rendezvous purely
# through the vfsMkfifo'd path "/tmp/rendezvous", not a shared argv handle.
$(UBUILD)/namedreader.elf: $(UBUILD)/namedreader.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x160000000 -o $@ $(UBUILD)/namedreader.o $(USERCOMMON)
$(UBUILD)/namedwriter.elf: $(UBUILD)/namedwriter.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x170000000 -o $@ $(UBUILD)/namedwriter.o $(USERCOMMON)
# consolereader1/consolereader2 are both in kmain.c's boot-time spawnUser
# list, with no relationship to each other -- each simply reads its own fd 0,
# which keyboard.c's per-reader fan-out delivers a synthetic keystroke to
# independently (consoleInjectorThread, kmain.c).
$(UBUILD)/consolereader1.elf: $(UBUILD)/consolereader1.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x180000000 -o $@ $(UBUILD)/consolereader1.o $(USERCOMMON)
$(UBUILD)/consolereader2.elf: $(UBUILD)/consolereader2.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x190000000 -o $@ $(UBUILD)/consolereader2.o $(USERCOMMON)
# sigtest is spawned directly by kmain.c at boot; sigtarget/sigdefault are
# only ever spawned at runtime by sigtest itself (userSpawn), the same
# relationship killtest.elf/killvictim.elf already have -- each still needs
# its own reserved USER_BASE regardless of who spawns it, since elfLoad
# maps every ELF at whatever fixed address it was linked at and all of
# them share this one address space (see elf.h).
$(UBUILD)/sigtest.elf: $(UBUILD)/sigtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1A0000000 -o $@ $(UBUILD)/sigtest.o $(USERCOMMON)
$(UBUILD)/sigtarget.elf: $(UBUILD)/sigtarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1B0000000 -o $@ $(UBUILD)/sigtarget.o $(USERCOMMON)
$(UBUILD)/sigdefault.elf: $(UBUILD)/sigdefault.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1C0000000 -o $@ $(UBUILD)/sigdefault.o $(USERCOMMON)
# jobtest is spawned directly by kmain.c at boot; jobtarget is only ever
# spawned at runtime by jobtest itself (userSpawn), the same relationship
# sigtest.elf/sigtarget.elf above already have.
$(UBUILD)/jobtest.elf: $(UBUILD)/jobtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1D0000000 -o $@ $(UBUILD)/jobtest.o $(USERCOMMON)
$(UBUILD)/jobtarget.elf: $(UBUILD)/jobtarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1E0000000 -o $@ $(UBUILD)/jobtarget.o $(USERCOMMON)
# nesttest is spawned directly by kmain.c at boot; nesttarget is only ever
# spawned at runtime by nesttest itself (userSpawn), the same relationship
# sigtest.elf/sigtarget.elf and jobtest.elf/jobtarget.elf above already have.
$(UBUILD)/nesttest.elf: $(UBUILD)/nesttest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x1F0000000 -o $@ $(UBUILD)/nesttest.o $(USERCOMMON)
$(UBUILD)/nesttarget.elf: $(UBUILD)/nesttarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x200000000 -o $@ $(UBUILD)/nesttarget.o $(USERCOMMON)
# eintrtest is spawned directly by kmain.c at boot; eintrtarget is only ever
# spawned at runtime by eintrtest itself (userSpawn), the same relationship
# killtest.elf/killvictim.elf above already have.
$(UBUILD)/eintrtest.elf: $(UBUILD)/eintrtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x210000000 -o $@ $(UBUILD)/eintrtest.o $(USERCOMMON)
$(UBUILD)/eintrtarget.elf: $(UBUILD)/eintrtarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x220000000 -o $@ $(UBUILD)/eintrtarget.o $(USERCOMMON)
# restarttest is spawned directly by kmain.c at boot; restarttarget is only
# ever spawned at runtime by restarttest itself (userSpawn), the same
# relationship eintrtest.elf/eintrtarget.elf above already have.
$(UBUILD)/restarttest.elf: $(UBUILD)/restarttest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x230000000 -o $@ $(UBUILD)/restarttest.o $(USERCOMMON)
$(UBUILD)/restarttarget.elf: $(UBUILD)/restarttarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x240000000 -o $@ $(UBUILD)/restarttarget.o $(USERCOMMON)
# masktest is spawned directly by kmain.c at boot; masktarget is only ever
# spawned at runtime by masktest itself (userSpawn), the same relationship
# nesttest.elf/nesttarget.elf above already have.
$(UBUILD)/masktest.elf: $(UBUILD)/masktest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x250000000 -o $@ $(UBUILD)/masktest.o $(USERCOMMON)
$(UBUILD)/masktarget.elf: $(UBUILD)/masktarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x260000000 -o $@ $(UBUILD)/masktarget.o $(USERCOMMON)
# sigchldtest is spawned directly by kmain.c at boot; sigchldchild is only
# ever spawned at runtime by sigchldtest itself (userSpawn), the same
# relationship nesttest.elf/nesttarget.elf above already have.
$(UBUILD)/sigchldtest.elf: $(UBUILD)/sigchldtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x270000000 -o $@ $(UBUILD)/sigchldtest.o $(USERCOMMON)
$(UBUILD)/sigchldchild.elf: $(UBUILD)/sigchldchild.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x280000000 -o $@ $(UBUILD)/sigchldchild.o $(USERCOMMON)
# waitanytest is spawned directly by kmain.c at boot; waitanyfast and
# waitanyslow are only ever spawned at runtime by waitanytest itself
# (userSpawn), the same relationship nesttest.elf/nesttarget.elf above
# already have.
$(UBUILD)/waitanytest.elf: $(UBUILD)/waitanytest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x290000000 -o $@ $(UBUILD)/waitanytest.o $(USERCOMMON)
$(UBUILD)/waitanyfast.elf: $(UBUILD)/waitanyfast.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2A0000000 -o $@ $(UBUILD)/waitanyfast.o $(USERCOMMON)
$(UBUILD)/waitanyslow.elf: $(UBUILD)/waitanyslow.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2B0000000 -o $@ $(UBUILD)/waitanyslow.o $(USERCOMMON)
# stopchldtest is spawned directly by kmain.c at boot; stopchldtarget is
# only ever spawned at runtime by stopchldtest itself (userSpawn), the same
# relationship jobtest.elf/jobtarget.elf above already have.
$(UBUILD)/stopchldtest.elf: $(UBUILD)/stopchldtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2C0000000 -o $@ $(UBUILD)/stopchldtest.o $(USERCOMMON)
$(UBUILD)/stopchldtarget.elf: $(UBUILD)/stopchldtarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2D0000000 -o $@ $(UBUILD)/stopchldtarget.o $(USERCOMMON)
# nodefertest is spawned directly by kmain.c at boot; it is a single,
# self-contained process (self-raises signals against its own pid) with no
# spawned counterpart, unlike every other pair above.
$(UBUILD)/nodefertest.elf: $(UBUILD)/nodefertest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2E0000000 -o $@ $(UBUILD)/nodefertest.o $(USERCOMMON)
$(UBUILD)/alarmtest.elf: $(UBUILD)/alarmtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x2F0000000 -o $@ $(UBUILD)/alarmtest.o $(USERCOMMON)
$(UBUILD)/alarmdefault.elf: $(UBUILD)/alarmdefault.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x300000000 -o $@ $(UBUILD)/alarmdefault.o $(USERCOMMON)
$(UBUILD)/siginfotest.elf: $(UBUILD)/siginfotest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x310000000 -o $@ $(UBUILD)/siginfotest.o $(USERCOMMON)
$(UBUILD)/siginfotarget.elf: $(UBUILD)/siginfotarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x320000000 -o $@ $(UBUILD)/siginfotarget.o $(USERCOMMON)
# pgroupfg/pgroupbg are both spawned directly by kmain.c at boot, with no
# parent-child relationship to each other (unlike every *test/*target pair
# above) -- each simply becomes its own default process group leader
# (sched.c's initialPgid), the same way two independently boot-launched
# programs already do for consolereader1/consolereader2 above.
$(UBUILD)/pgroupfg.elf: $(UBUILD)/pgroupfg.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x330000000 -o $@ $(UBUILD)/pgroupfg.o $(USERCOMMON)
$(UBUILD)/pgroupbg.elf: $(UBUILD)/pgroupbg.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x340000000 -o $@ $(UBUILD)/pgroupbg.o $(USERCOMMON)
# jobctlfg/jobctlbg are never spawned by kmain.c at all -- unlike every
# other program in this list, they are loaded on demand, at runtime, when
# kmain.c's jobctlDriverThread types their names at the real interactive
# shell (shell.c), which resolves them through jobctl.c exactly like a real
# shell's $PATH lookup. They still need their own fixed link slot up front
# like any other binary (elf.h's elfLoad maps a binary at whatever address
# it was linked at, chosen here, not at load time).
$(UBUILD)/jobctlfg.elf: $(UBUILD)/jobctlfg.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x350000000 -o $@ $(UBUILD)/jobctlfg.o $(USERCOMMON)
$(UBUILD)/jobctlbg.elf: $(UBUILD)/jobctlbg.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x360000000 -o $@ $(UBUILD)/jobctlbg.o $(USERCOMMON)
$(UBUILD)/polltest.elf: $(UBUILD)/polltest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x370000000 -o $@ $(UBUILD)/polltest.o $(USERCOMMON)
$(UBUILD)/pollwriter.elf: $(UBUILD)/pollwriter.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x380000000 -o $@ $(UBUILD)/pollwriter.o $(USERCOMMON)
# pipesrc/pipesink are, like jobctlfg/jobctlbg above, never spawned by
# kmain.c directly -- loaded on demand when the real interactive shell
# processes a typed "pipesrc | pipesink" line (shell.c's new pipeline
# syntax, kmain.c's pipelineDriverThread).
$(UBUILD)/pipesrc.elf: $(UBUILD)/pipesrc.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x390000000 -o $@ $(UBUILD)/pipesrc.o $(USERCOMMON)
$(UBUILD)/pipesink.elf: $(UBUILD)/pipesink.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3A0000000 -o $@ $(UBUILD)/pipesink.o $(USERCOMMON)
# redirwriter/redirreader are, like pipesrc/pipesink above, never spawned by
# kmain.c directly -- loaded on demand when the real interactive shell
# processes typed "redirwriter > FILE" / "redirwriter >> FILE" /
# "redirreader < FILE" lines (shell.c's new file redirection syntax,
# kmain.c's redirDriverThread).
$(UBUILD)/redirwriter.elf: $(UBUILD)/redirwriter.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3B0000000 -o $@ $(UBUILD)/redirwriter.o $(USERCOMMON)
$(UBUILD)/redirreader.elf: $(UBUILD)/redirreader.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3C0000000 -o $@ $(UBUILD)/redirreader.o $(USERCOMMON)
# stderrwriter/stderrreader are, like redirwriter/redirreader above, never
# spawned by kmain.c directly -- loaded on demand when the real interactive
# shell processes typed "stderrwriter 2> FILE" / "stderrwriter 2>> FILE" /
# "stderrreader < FILE" lines (shell.c's new stderr redirection syntax,
# kmain.c's stderrDriverThread).
$(UBUILD)/stderrwriter.elf: $(UBUILD)/stderrwriter.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3D0000000 -o $@ $(UBUILD)/stderrwriter.o $(USERCOMMON)
$(UBUILD)/stderrreader.elf: $(UBUILD)/stderrreader.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3E0000000 -o $@ $(UBUILD)/stderrreader.o $(USERCOMMON)
# selfstoptest is spawned directly by kmain.c at boot; selfstoptarget is
# only ever spawned at runtime by selfstoptest itself (userSpawn), the same
# relationship jobtest.elf/jobtarget.elf above already have.
$(UBUILD)/selfstoptest.elf: $(UBUILD)/selfstoptest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x3F0000000 -o $@ $(UBUILD)/selfstoptest.o $(USERCOMMON)
$(UBUILD)/selfstoptarget.elf: $(UBUILD)/selfstoptarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x400000000 -o $@ $(UBUILD)/selfstoptarget.o $(USERCOMMON)
# herestest is, like redirreader/stderrreader above, never spawned by kmain.c
# directly -- loaded on demand when the real interactive shell processes a
# typed "herestest <<< TEXT" line (shell.c's new here-string redirection
# syntax, kmain.c's hereDriverThread). Unlike redirreader/stderrreader it has
# no writer counterpart binary at all -- the "writer" is the shell's own
# kernel thread, feeding the typed text straight into a pipe (jobctl.c's
# jobctlSpawnHereStringPiped), never another spawned process's stdout.
$(UBUILD)/herestest.elf: $(UBUILD)/herestest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x410000000 -o $@ $(UBUILD)/herestest.o $(USERCOMMON)
# hcat/hsink are, like pipesrc/pipesink above, never spawned by kmain.c
# directly -- loaded on demand when the real interactive shell processes a
# typed "hcat <<< TEXT | hsink" line (shell.c's new here-string-into-
# pipeline syntax, jobctl.c's jobctlSpawnPipedHereString, kmain.c's
# hereStringPipelineDriverThread).
$(UBUILD)/hcat.elf: $(UBUILD)/hcat.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x420000000 -o $@ $(UBUILD)/hcat.o $(USERCOMMON)
$(UBUILD)/hsink.elf: $(UBUILD)/hsink.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x430000000 -o $@ $(UBUILD)/hsink.o $(USERCOMMON)
# pipecat is, like pipesrc/pipesink above, never spawned by kmain.c directly
# -- loaded on demand when the real interactive shell processes a typed
# "pipesrc | pipecat | pipesink" line (shell.c's new N-stage pipeline
# syntax, kmain.c's threeStagePipelineDriverThread). It is the middle stage
# of that pipeline: unlike hcat/hsink above, both its fd 0 and fd 1 are real
# process-to-process pipes, never the console or a here-string.
$(UBUILD)/pipecat.elf: $(UBUILD)/pipecat.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x440000000 -o $@ $(UBUILD)/pipecat.o $(USERCOMMON)
# cwdtest is spawned directly by kmain.c at boot; cwdtarget is only ever
# spawned at runtime by cwdtest itself (userSpawn, no argv), the same
# relationship jobtest.elf/jobtarget.elf above already have -- except this
# pair rendezvous purely through the real per-process cwd the kernel itself
# hands cwdtarget at creation time (sched.c's initialCwd), not a shared
# pipe handle passed as an argv string.
$(UBUILD)/cwdtest.elf: $(UBUILD)/cwdtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x450000000 -o $@ $(UBUILD)/cwdtest.o $(USERCOMMON)
$(UBUILD)/cwdtarget.elf: $(UBUILD)/cwdtarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x460000000 -o $@ $(UBUILD)/cwdtarget.o $(USERCOMMON)
# envprint is, like jobctlfg/jobctlbg/hcat/pipecat above, never spawned by
# kmain.c directly -- loaded on demand when the real interactive shell
# processes a typed "envprint" line (shell.c's fallback /bin/<word> lookup,
# kmain.c's new envDriverThread).
$(UBUILD)/envprint.elf: $(UBUILD)/envprint.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x470000000 -o $@ $(UBUILD)/envprint.o $(USERCOMMON)
# nicetest is spawned directly by kmain.c at boot; nicetarget is only ever
# spawned at runtime by nicetest itself (userSpawn, no argv), the same
# *test / *target relationship cwdtest.elf/cwdtarget.elf above already
# have -- this pair rendezvous purely through sysNice/sysGetPriority
# (syscall.c), never a shared pipe handle or the filesystem.
$(UBUILD)/nicetest.elf: $(UBUILD)/nicetest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x480000000 -o $@ $(UBUILD)/nicetest.o $(USERCOMMON)
$(UBUILD)/nicetarget.elf: $(UBUILD)/nicetarget.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x490000000 -o $@ $(UBUILD)/nicetarget.o $(USERCOMMON)
# date is spawned directly by kmain.c at boot, fully synchronous like
# nicetest.elf above -- no shell rendezvous, no injected keystroke.
$(UBUILD)/date.elf: $(UBUILD)/date.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x4A0000000 -o $@ $(UBUILD)/date.o $(USERCOMMON)
# globcat is, like envprint/hcat/pipecat above, never spawned by kmain.c
# directly -- loaded on demand when the real interactive shell processes a
# typed "globcat /tmp/gd/*.txt" line (shell.c's new wildcard glob
# expansion, kmain.c's new globDriverThread). USER_BASE picks up well past
# the last-used slot (date.elf, plus its own boot-time spawnUser stack
# address) rather than the immediately-following one, to stay clear of any
# address this file's own boot-time spawnUser calls already reference.
$(UBUILD)/globcat.elf: $(UBUILD)/globcat.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x4C0000000 -o $@ $(UBUILD)/globcat.o $(USERCOMMON)
# timetest is spawned directly by kmain.c at boot, fully synchronous like
# nicetest.elf/date.elf above -- no shell rendezvous, no injected keystroke.
# USER_BASE is the slot right after globcat.elf's (the last one reserved);
# nothing else references any address in or past this slot.
$(UBUILD)/timetest.elf: $(UBUILD)/timetest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x4D0000000 -o $@ $(UBUILD)/timetest.o $(USERCOMMON)
# false is, like globcat above, never spawned by kmain.c directly --
# loaded on demand when the real interactive shell processes a typed
# `false && echo X || echo ...` line (shell.c's new `&&`/`||` conditional
# chaining, kmain.c's new andOrDriverThread). USER_BASE picks up the slot
# right after timetest.elf's (the last one reserved).
$(UBUILD)/false.elf: $(UBUILD)/false.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x4E0000000 -o $@ $(UBUILD)/false.o $(USERCOMMON)
# argecho is, like globcat/false above, never spawned by kmain.c directly --
# loaded on demand when the real interactive shell processes a typed
# `argecho $?` line (shell.c's new `$?` expansion, kmain.c's extended
# andOrDriverThread). USER_BASE picks up the slot right after false.elf's
# (the last one reserved).
$(UBUILD)/argecho.elf: $(UBUILD)/argecho.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x4F0000000 -o $@ $(UBUILD)/argecho.o $(USERCOMMON)
# rmtest is spawned directly by kmain.c at boot, fully synchronous like
# timetest.elf/date.elf/nicetest.elf above -- no shell rendezvous, no
# injected keystroke. USER_BASE is the slot right after argecho.elf's (the
# last one reserved); nothing else references any address in or past this
# slot.
$(UBUILD)/rmtest.elf: $(UBUILD)/rmtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x500000000 -o $@ $(UBUILD)/rmtest.o $(USERCOMMON)
# permtest is spawned directly by kmain.c at boot, fully synchronous like
# rmtest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after rmtest.elf's (the last one reserved); nothing
# else references any address in or past this slot.
$(UBUILD)/permtest.elf: $(UBUILD)/permtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x510000000 -o $@ $(UBUILD)/permtest.o $(USERCOMMON)
# identtest is spawned directly by kmain.c at boot, fully synchronous like
# permtest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after permtest.elf's (the last one reserved); nothing
# else references any address in or past this slot.
$(UBUILD)/identtest.elf: $(UBUILD)/identtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x520000000 -o $@ $(UBUILD)/identtest.o $(USERCOMMON)
# dirpermtest is spawned directly by kmain.c at boot, fully synchronous like
# identtest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after identtest.elf's (the last one reserved); nothing
# else references any address in or past this slot.
$(UBUILD)/dirpermtest.elf: $(UBUILD)/dirpermtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x530000000 -o $@ $(UBUILD)/dirpermtest.o $(USERCOMMON)
# grouptest is spawned directly by kmain.c at boot, fully synchronous like
# dirpermtest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after dirpermtest.elf's (the last one reserved); nothing
# else references any address in or past this slot.
$(UBUILD)/grouptest.elf: $(UBUILD)/grouptest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x540000000 -o $@ $(UBUILD)/grouptest.o $(USERCOMMON)
# pidfdtest is spawned directly by kmain.c at boot, fully synchronous like
# grouptest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after grouptest.elf's (the last one reserved, verified by
# scanning every USER_BASE in this Makefile, not just assumed to be "one past
# the highest" -- see kmain.c's own nicetest/date address-space inconsistency,
# left as a warning for exactly this kind of addition); nothing else
# references any address in or past this slot.
$(UBUILD)/pidfdtest.elf: $(UBUILD)/pidfdtest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x550000000 -o $@ $(UBUILD)/pidfdtest.o $(USERCOMMON)
# mmaptest is spawned directly by kmain.c at boot, fully synchronous like
# pidfdtest.elf above -- no shell rendezvous, no injected keystroke. USER_BASE
# is the slot right after pidfdtest.elf's (the last one reserved, verified by
# scanning every USER_BASE in this Makefile); nothing else references any
# address in or past this slot.
$(UBUILD)/mmaptest.elf: $(UBUILD)/mmaptest.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x560000000 -o $@ $(UBUILD)/mmaptest.o $(USERCOMMON)
# mmapprotvictim is only ever spawned at RUNTIME by mmaptest.elf itself
# (userSpawn), never directly by kmain.c -- same relationship killvictim.elf
# already has to killtest.elf. USER_BASE is the slot right after
# mmaptest.elf's own.
$(UBUILD)/mmapprotvictim.elf: $(UBUILD)/mmapprotvictim.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x570000000 -o $@ $(UBUILD)/mmapprotvictim.o $(USERCOMMON)
# mmapexecvictim is only ever spawned at RUNTIME by mmaptest.elf itself
# (userSpawn), exactly like mmapprotvictim.elf above -- never directly by
# kmain.c. USER_BASE is the slot right after mmapprotvictim.elf's own.
$(UBUILD)/mmapexecvictim.elf: $(UBUILD)/mmapexecvictim.o $(USERCOMMON) user/user.ld
$(LD) $(ULDFLAGS) --defsym USER_BASE=0x580000000 -o $@ $(UBUILD)/mmapexecvictim.o $(USERCOMMON)
$(MKTAR): tools/mktar.c | $(BUILD) $(MKTAR): tools/mktar.c | $(BUILD)
gcc -O2 -Wall -Wextra -o $@ $< gcc -O2 -Wall -Wextra -o $@ $<
@@ -89,6 +564,85 @@ $(INITRD): $(MKTAR) $(USERPROGRAMS) $(FSFILES)
--file bin/counter $(UBUILD)/counter.elf \ --file bin/counter $(UBUILD)/counter.elf \
--file bin/sbrktest $(UBUILD)/sbrktest.elf \ --file bin/sbrktest $(UBUILD)/sbrktest.elf \
--file bin/fileread $(UBUILD)/fileread.elf \ --file bin/fileread $(UBUILD)/fileread.elf \
--file bin/pipetest $(UBUILD)/pipetest.elf \
--file bin/disktest $(UBUILD)/disktest.elf \
--file bin/fstest $(UBUILD)/fstest.elf \
--file bin/linktest $(UBUILD)/linktest.elf \
--file bin/spawntarget $(UBUILD)/spawntarget.elf \
--file bin/exectarget $(UBUILD)/exectarget.elf \
--file bin/proctest $(UBUILD)/proctest.elf \
--file bin/argtest $(UBUILD)/argtest.elf \
--file bin/fifomaster $(UBUILD)/fifomaster.elf \
--file bin/fifowriter $(UBUILD)/fifowriter.elf \
--file bin/fiforeader $(UBUILD)/fiforeader.elf \
--file bin/killtest $(UBUILD)/killtest.elf \
--file bin/killvictim $(UBUILD)/killvictim.elf \
--file bin/namedreader $(UBUILD)/namedreader.elf \
--file bin/namedwriter $(UBUILD)/namedwriter.elf \
--file bin/consolereader1 $(UBUILD)/consolereader1.elf \
--file bin/consolereader2 $(UBUILD)/consolereader2.elf \
--file bin/sigtest $(UBUILD)/sigtest.elf \
--file bin/sigtarget $(UBUILD)/sigtarget.elf \
--file bin/sigdefault $(UBUILD)/sigdefault.elf \
--file bin/jobtest $(UBUILD)/jobtest.elf \
--file bin/jobtarget $(UBUILD)/jobtarget.elf \
--file bin/nesttest $(UBUILD)/nesttest.elf \
--file bin/nesttarget $(UBUILD)/nesttarget.elf \
--file bin/eintrtest $(UBUILD)/eintrtest.elf \
--file bin/eintrtarget $(UBUILD)/eintrtarget.elf \
--file bin/restarttest $(UBUILD)/restarttest.elf \
--file bin/restarttarget $(UBUILD)/restarttarget.elf \
--file bin/masktest $(UBUILD)/masktest.elf \
--file bin/masktarget $(UBUILD)/masktarget.elf \
--file bin/sigchldtest $(UBUILD)/sigchldtest.elf \
--file bin/sigchldchild $(UBUILD)/sigchldchild.elf \
--file bin/waitanytest $(UBUILD)/waitanytest.elf \
--file bin/waitanyfast $(UBUILD)/waitanyfast.elf \
--file bin/waitanyslow $(UBUILD)/waitanyslow.elf \
--file bin/stopchldtest $(UBUILD)/stopchldtest.elf \
--file bin/stopchldtarget $(UBUILD)/stopchldtarget.elf \
--file bin/nodefertest $(UBUILD)/nodefertest.elf \
--file bin/alarmtest $(UBUILD)/alarmtest.elf \
--file bin/alarmdefault $(UBUILD)/alarmdefault.elf \
--file bin/siginfotest $(UBUILD)/siginfotest.elf \
--file bin/siginfotarget $(UBUILD)/siginfotarget.elf \
--file bin/pgroupfg $(UBUILD)/pgroupfg.elf \
--file bin/pgroupbg $(UBUILD)/pgroupbg.elf \
--file bin/jobctlfg $(UBUILD)/jobctlfg.elf \
--file bin/jobctlbg $(UBUILD)/jobctlbg.elf \
--file bin/polltest $(UBUILD)/polltest.elf \
--file bin/pollwriter $(UBUILD)/pollwriter.elf \
--file bin/pipesrc $(UBUILD)/pipesrc.elf \
--file bin/pipesink $(UBUILD)/pipesink.elf \
--file bin/redirwriter $(UBUILD)/redirwriter.elf \
--file bin/redirreader $(UBUILD)/redirreader.elf \
--file bin/stderrwriter $(UBUILD)/stderrwriter.elf \
--file bin/stderrreader $(UBUILD)/stderrreader.elf \
--file bin/selfstoptest $(UBUILD)/selfstoptest.elf \
--file bin/selfstoptarget $(UBUILD)/selfstoptarget.elf \
--file bin/herestest $(UBUILD)/herestest.elf \
--file bin/hcat $(UBUILD)/hcat.elf \
--file bin/hsink $(UBUILD)/hsink.elf \
--file bin/pipecat $(UBUILD)/pipecat.elf \
--file bin/cwdtest $(UBUILD)/cwdtest.elf \
--file bin/cwdtarget $(UBUILD)/cwdtarget.elf \
--file bin/envprint $(UBUILD)/envprint.elf \
--file bin/nicetest $(UBUILD)/nicetest.elf \
--file bin/nicetarget $(UBUILD)/nicetarget.elf \
--file bin/date $(UBUILD)/date.elf \
--file bin/globcat $(UBUILD)/globcat.elf \
--file bin/timetest $(UBUILD)/timetest.elf \
--file bin/false $(UBUILD)/false.elf \
--file bin/argecho $(UBUILD)/argecho.elf \
--file bin/rmtest $(UBUILD)/rmtest.elf \
--file bin/permtest $(UBUILD)/permtest.elf \
--file bin/identtest $(UBUILD)/identtest.elf \
--file bin/dirpermtest $(UBUILD)/dirpermtest.elf \
--file bin/grouptest $(UBUILD)/grouptest.elf \
--file bin/pidfdtest $(UBUILD)/pidfdtest.elf \
--file bin/mmaptest $(UBUILD)/mmaptest.elf \
--file bin/mmapprotvictim $(UBUILD)/mmapprotvictim.elf \
--file bin/mmapexecvictim $(UBUILD)/mmapexecvictim.elf \
--file etc/motd.txt fsroot/etc/motd.txt \ --file etc/motd.txt fsroot/etc/motd.txt \
--file etc/readme.txt fsroot/etc/readme.txt \ --file etc/readme.txt fsroot/etc/readme.txt \
--file home/notes.txt fsroot/home/notes.txt --file home/notes.txt fsroot/home/notes.txt
@@ -98,14 +652,27 @@ $(ISO): $(KERNEL) $(INITRD) iso/boot/grub/grub.cfg
@cp $(INITRD) iso/boot/initrd.img @cp $(INITRD) iso/boot/initrd.img
grub-mkrescue -o $(ISO) iso 2>/dev/null grub-mkrescue -o $(ISO) iso 2>/dev/null
run: $(ISO) # Scratch disk backing /dev/disk0 (ata.c / vfs.c blockDev*). Zeroed, not
qemu-system-x86_64 -cdrom $(ISO) -serial stdio -m 128M # committed: a fresh 8 MiB raw image is exactly as good as any other for a
# driver that owns the whole device and formats nothing on it.
$(DISK): | $(BUILD)
dd if=/dev/zero of=$@ bs=1M count=8 status=none
runserial: $(ISO) run: $(ISO) $(DISK)
qemu-system-x86_64 -cdrom $(ISO) -serial stdio -display none -m 128M -no-reboot qemu-system-x86_64 -cdrom $(ISO) $(DISKFLAGS) -serial stdio -m 128M
test: $(ISO) runserial: $(ISO) $(DISK)
qemu-system-x86_64 -cdrom $(ISO) $(DISKFLAGS) -serial stdio -display none -m 128M -no-reboot
test: $(ISO) $(DISK)
sh tools/ci-test.sh sh tools/ci-test.sh
clean: clean:
rm -rf $(BUILD) $(ISO) iso/boot/frostbyte.elf iso/boot/initrd.img rm -rf $(BUILD) $(ISO) iso/boot/frostbyte.elf iso/boot/initrd.img
# Auto-generated header dependencies (-MMD -MP above): without these, editing
# a widely-included header (e.g. sched.h) would not force a rebuild of every
# .c file that includes it, silently linking objects compiled against
# different struct layouts of the same type -- exactly the kind of ABI
# mismatch that corrupts memory at runtime instead of failing to build.
-include $(wildcard $(BUILD)/*.d) $(wildcard $(UBUILD)/*.d)
+34
View File
@@ -0,0 +1,34 @@
#ifndef FROSTBYTE_ATA_H
#define FROSTBYTE_ATA_H
#include "types.h"
/*
* ATA PIO driver (primary bus, master drive only, LBA28 addressing).
*
* This talks to real IDE hardware registers (0x1F0-0x1F7 + the 0x3F6
* alternate-status/device-control port) using polling, not IRQ14: ataInit
* disables the drive's interrupt line (nIEN) up front, so every command is
* driven by busy-waiting on the status register. That keeps the driver
* usable from the in-kernel test harness, which runs before the scheduler
* and with interrupts disabled.
*
* All polling loops are bounded (ataPollTimeout iterations): a missing or
* wedged drive returns false/failure instead of hanging the kernel forever.
*/
#define ataSectorSize 512
bool ataInit(void);
bool ataPresent(void);
uint64 ataSectorCount(void);
const char* ataModelString(void);
/* count is a sector count in [0, 255]; 0 trivially succeeds without
touching hardware. Both fail (return false) if no drive was found by
ataInit, if the request runs past ataSectorCount(), or if the drive
reports an error/timeout mid-transfer. */
bool ataReadSectors(uint64 lba, uint32 count, void* buffer);
bool ataWriteSectors(uint64 lba, uint32 count, const void* buffer);
#endif
+37
View File
@@ -0,0 +1,37 @@
#ifndef FROSTBYTE_BMP_H
#define FROSTBYTE_BMP_H
#include "types.h"
/*
* Bitmap: general-purpose fixed-size bitmap over caller-supplied 64-bit
* word storage. No heap dependency (the pmm frame allocator uses its own
* private copy of this same idea and predates this generic version; this
* one is for any other subsystem that needs a bit-indexed set).
*/
typedef struct Bitmap {
uint64* words;
usize bitCount;
} Bitmap;
/* backingWords must hold at least (bitCount + 63) / 64 uint64 words. */
void bmpInit(Bitmap* b, uint64* backingWords, usize bitCount);
void bmpSet(Bitmap* b, usize index);
void bmpClear(Bitmap* b, usize index);
bool bmpTest(const Bitmap* b, usize index);
void bmpSetRange(Bitmap* b, usize start, usize count);
void bmpClearRange(Bitmap* b, usize start, usize count);
void bmpSetAll(Bitmap* b);
void bmpClearAll(Bitmap* b);
usize bmpCountSet(const Bitmap* b);
/* Returns the index of the first clear/set bit, or -1 if none exists. */
isize bmpFindFirstClear(const Bitmap* b);
isize bmpFindFirstSet(const Bitmap* b);
#endif
+56
View File
@@ -33,4 +33,60 @@ typedef struct Elf64ProgramHeader {
uint64 elfLoad(const void* image, uint64 imageSize); uint64 elfLoad(const void* image, uint64 imageSize);
/* Maps one ELF64 program header's p_flags (segmentFlags: standard PF_X=1/
PF_W=2/PF_R=4 bit values) onto the extra paging.h leaf-PTE bits
(pageWrite, pageNoExecute) elfLoad passes to pagingMapUserRangeExact for
that segment -- real per-segment W^X (a binary's own .text/.rodata ends
up read-only+executable, its .data/.bss read-write+non-executable)
instead of one blanket writable+executable range for the whole image.
Pure address-flag arithmetic, no paging/scheduler/VFS state touched, so
it is fully unit-testable (elfTest.c) the same way elfSlotBase and
friends already are. */
uint64 elfSegmentPagingFlags(uint32 segmentFlags);
/* Every userspace binary in this kernel is linked (Makefile --defsym
USER_BASE=...) at a fixed, elfSlotSize-aligned base of its own, since
there is one shared address space system-wide (no per-process page
tables/CR3) and no relocation support (-fno-pic): two binaries must
simply never overlap. kmain.c's boot-time spawnUser calls hardcode each
binary's stack/brk address following the same base+offset pattern by
hand; elfSlotStackTop/elfSlotBrkStart let syscall.c's exec()/spawn()
(a path resolved at runtime, not known at kmain.c's compile time)
derive the same addresses purely from the ELF's own entry point. */
#define elfSlotSize 0x10000000ull
#define elfSlotStackOffset 0x08000000ull
#define elfSlotBrkOffset 0x04000000ull
uint64 elfSlotBase(uint64 entry);
uint64 elfSlotStackTop(uint64 entry);
uint64 elfSlotBrkStart(uint64 entry);
/* Ceiling on argv entries any single exec()/spawn()/boot launch can pass
(syscall.c's copyUserArgv enforces the same bound while copying them out
of userspace). Chosen together with the 256-byte per-argument cap
syscall.c already uses for path-like strings so that
elfMaxArgs*256 + a few hundred bytes of pointers/argc never comes close
to execStackSize (syscall.c) or kmain.c's userStackSize -- both far
larger than any realistic argv list this kernel's own binaries pass. */
#define elfMaxArgs 16
/* Lays out a process's very first stack frame the way a real x86_64 SysV
process entry looks (used by user/crt.S's _start): from stackTop down,
the argument strings themselves, then (16-byte aligned per the ABI)
argc, argv[0..argc-1], and a NULL argv terminator -- *outRsp is where
_start should start executing with, pointing at argc.
argv/argc are already kernel-side data (copyUserArgv, doExec/doSpawn in
syscall.c, or a literal array for kmain.c's boot spawnUser) by the time
this runs; this function only ever writes into [stackTop-stackSize,
stackTop), which the caller must have already mapped (pagingMapUserRange)
-- same contract as elfLoad's segment writes.
Returns false, writing nothing, if argc is out of [0, elfMaxArgs] or the
strings plus the fixed-size argv/argc area do not fit in stackSize bytes
-- unreachable for every call site in this kernel today given the bound
above, but checked rather than assumed so a future caller with a bigger
argv (or a deliberately tiny stack in elfTest.c) fails cleanly instead of
silently writing before stackTop-stackSize. */
bool elfBuildInitialStack(uint64 stackTop, uint64 stackSize, const char* const* argv,
int argc, uint64* outRsp);
#endif #endif
+131
View File
@@ -14,13 +14,144 @@
#define fsTypeFile 1 #define fsTypeFile 1
#define fsTypeDir 2 #define fsTypeDir 2
#define fsTypeFifo 3
/* Real POSIX permission bits (chmod(2)/open(2)'s access check). This kernel
now has a real (if minimal) uid AND gid model -- sched.h's Thread.uid/
Thread.gid, vfs.c's vfsPermitted -- so all three (owner/group/other)
triads are genuinely stored AND enforced. A mode value like 0750 is
accepted and echoed back by vfsStat/userStat exactly as given. Enforced
at exactly two real choke points, both uid+gid-aware via vfsPermitted:
resolveOpen (vfs.c) refuses to open an existing file for writing without
the right write bit for the CALLING thread's identity, and
loadImageByPath/jobctlSpawn (syscall.c/jobctl.c) refuse to exec a file
without the right exec bit -- both real, both checked at open/spawn time
only, never re-checked against an fd/process already granted (matching
real Unix). Deliberately no uid-0-bypasses-everything superuser rule
like real Unix's root has -- see vfs.c's vfsPermitted doc comment for
why a from-scratch bypass would have silently broken every permission
test already relying on chmod denying write even to a file's own
creator (every boot-time process starts at uid 0). */
#define fsModeOwnerRead 0400u
#define fsModeOwnerWrite 0200u
#define fsModeOwnerExec 0100u
/* The "group" triad -- vfsPermitted's middle fallback, consulted once a
caller's uid fails to match a node's owner uid but its GID (sched.h's
Thread.gid) matches the node's own group gid (fs.h's FbStat.gid). Each
bit is its own owner bit shifted right by 3 (real Unix's own rwxrwxrwx
bit layout: owner at bits 8-6, group at bits 5-3, other at bits 2-0),
the same derivation fsModeOther* below already used for the next shift
down -- vfsPermitted computes both from wantOwnerBit rather than storing
six separate constants per permission. */
#define fsModeGroupRead 0040u
#define fsModeGroupWrite 0020u
#define fsModeGroupExec 0010u
/* The "other" (neither owner nor group) triad -- vfsPermitted's final
fallback once a caller's uid AND gid both fail to match. Numerically each
is its own owner bit shifted right by 6, matching the 6-bit gap between
the owner and other triads in a real Unix mode. */
#define fsModeOtherRead 0004u
#define fsModeOtherWrite 0002u
#define fsModeOtherExec 0001u
/* Defaults applied at creation time (never retroactive -- an existing
file's mode never changes on its own, only through chmod). Directory
mode bits are stored and reported by stat/ls -l but never enforced: this
kernel's existing per-node `writable` (tmpfs) / dir-emptiness (SFS) gates
already govern directory mutation, a second, redundant enforcement axis
was not worth the complexity for a case no test needs. */
#define fsModeDefaultFile 0644u
#define fsModeDefaultDir 0755u
#define fsModeDefaultExec 0755u
/* sysMmap's `prot` argument (ulib.h's userMmap) -- POSIX PROT_NONE/READ/
WRITE/EXEC by shape. mmapProtExec IS real and hardware-enforced
(mmapFaultHandler, mmap.c, maps every page of a Vma missing this bit
with paging.h's pageNoExecute): jumping into a mapping that never asked
for it takes a real, catchable SIGSEGV (isr.c's I/D-bit short-circuit in
tryResolveUserPageFault), never silently succeeds -- WHEN
paging.h's pagingNxSupported() is true (boot.S enables IA32_EFER.NXE
only after confirming CPUID.80000001H:EDX.NX; unsupported hardware
silently keeps every mapping executable rather than write a reserved PTE
bit, the same honest best-effort this codebase already applies
elsewhere). This mechanism is not limited to mmap: elf.c's own loader
now maps each ELF program header with the same paging.h flags, derived
from that segment's own p_flags (elfSegmentPagingFlags) -- a binary's
.text/.rodata comes up read-only+executable, its .data/.bss read-write+
non-executable (user/user.ld emits them as two separate PT_LOAD
segments precisely so they can get different flags), and the
fixed-size stack and brk heap (kmain.c/syscall.c/jobctl.c/sched.c) still
ask pagingMapUserRangeFlags for pageNoExecute explicitly the same way
they always have. mmapProtWrite IS also real and enforced
(mmapFaultHandler, mmap.c): a write fault against a mapping missing this
bit is refused and turns into a real SIGSEGV (isr.c), never silently
allowed -- the same real enforcement elf.c's own read-only .text/.rodata
segments now get too (user/elfwritevictim.c). */
#define mmapProtNone 0x0
#define mmapProtRead 0x1
#define mmapProtWrite 0x2
#define mmapProtExec 0x4
/* sysMmap's `flags` argument -- MAP_ANONYMOUS-equivalent: fd/offset are
ignored and every page is demand-zero-filled instead of read from a file
(mmap.c's mmapFaultHandler). There is no MAP_SHARED bit: every mapping
here behaves like MAP_PRIVATE (a file-backed page's own frame is this
mapping's alone -- writing to it, when mmapProtWrite allows that, never
writes back to the underlying file, exactly like a real MAP_PRIVATE
mapping's own copy-on-write semantics, just without ever actually
sharing the frame with the file cache to begin with, since this kernel
has none), so the file-backed default already gets MAP_PRIVATE's own
behavior without a second flag bit to ask for it. */
#define mmapAnonymous 0x1
#define fsNameMax 64 #define fsNameMax 64
/* Shared cap for a full path string (as opposed to fsNameMax's single
component): sched.h's Thread.cwd, vfs.h's vfsResolvePath/vfsGetcwd, and
every syscall.c path-copying stack buffer (sysOpen/sysMkdir/sysChdir/...)
all use this same constant, so a path that fits one always fits the
other -- shell.c's own longstanding local pathCapacity used the same 256
value before this constant existed. */
#define fsPathMax 256
typedef struct FbStat { typedef struct FbStat {
unsigned long size; unsigned long size;
unsigned int type; unsigned int type;
/* Real owner rwx permission bits (see fsModeOwner* above) for a tmpfs
or SFS node -- a synthetic 0666/0444 derived from `writable` before
chmod(2) existed, now the actual per-node value. Always 0666 for
/dev/disk0 (no real per-file mode concept for a raw block device,
an honest, never-enforced placeholder, unlike the file/dir case). */
unsigned int mode; unsigned int mode;
/* Real owner identity (POSIX uid): the sched.h Thread.uid value that
was current at creation time for a tmpfs or SFS node -- see vfs.c's
vfsUid/sfs.h's sfsUidOf, the same accessors vfsPermitted itself reads
to decide owner-vs-other access. Never retroactive at creation time,
like mode's own default, and never changed by chmod -- only a real
vfsChown (root-only, real Unix's _POSIX_CHOWN_RESTRICTED default) can
reassign it afterward. Always 0 ("root") for /dev/disk0 and the in-memory
tmpfs root itself, mirroring mtime/ctime's own "0 means never
tracked" placeholder immediately below for cases with no real
creation event of their own. */
unsigned int uid;
/* Real group identity (POSIX gid): the sched.h Thread.gid value that was
current at creation time for a tmpfs or SFS node -- see vfs.c's
vfsGid/sfs.h's sfsGidOf, the same accessors vfsPermitted itself reads
to decide the group branch of owner-vs-group-vs-other access. Never
retroactive at creation time, like uid's own default, and never
changed by chmod -- only vfsChgrp (owner may only set it to their OWN
current gid, root may set it to anything, see vfs.c's vfsChgrp doc
comment) can reassign it afterward. Always 0 for /dev/disk0 and the
in-memory tmpfs root itself, mirroring uid's own placeholder. */
unsigned int gid;
/* Real wall-clock timestamps (seconds since the Unix epoch, rtc.h's
rtcNow()) for an SFS ("/mnt") inode -- see sfs.h's sfsMtimeOf/
sfsCtimeOf. Always 0 for the in-memory tmpfs tree and /dev/disk0,
which have never tracked time (documented gap, not a faked value). */
unsigned long mtime;
unsigned long ctime;
} FbStat; } FbStat;
typedef struct FbDirent { typedef struct FbDirent {
+45
View File
@@ -0,0 +1,45 @@
#ifndef FROSTBYTE_HMAP_H
#define FROSTBYTE_HMAP_H
#include "types.h"
/*
* Hmap: a string-keyed hash map with chained buckets, backed by the kernel
* heap. Keys are copied into owned storage on hmapSet (the caller's key
* buffer need not outlive the call); values are stored as opaque void*
* (the map does not take ownership of pointed-to value memory).
*/
typedef struct HmapEntry {
char* key;
void* value;
struct HmapEntry* next;
} HmapEntry;
typedef struct Hmap {
HmapEntry** buckets;
usize bucketCount;
usize count;
} Hmap;
/* FNV-1a. Exposed so tests (and callers who want a custom bucket count
heuristic) can reason about it directly. */
uint64 hmapHashString(const char* key);
bool hmapInit(Hmap* h, usize bucketCount);
void hmapFree(Hmap* h);
/* Inserts key/value, or overwrites value if key is already present.
Returns false only on allocation failure. */
bool hmapSet(Hmap* h, const char* key, void* value);
void* hmapGet(const Hmap* h, const char* key);
bool hmapHas(const Hmap* h, const char* key);
bool hmapRemove(Hmap* h, const char* key);
usize hmapCount(const Hmap* h);
/* Visits every entry in unspecified order; stop early if visit returns
false. userData is passed through unchanged. */
void hmapForEach(const Hmap* h, bool (*visit)(const char* key, void* value, void* userData), void* userData);
#endif
+232
View File
@@ -0,0 +1,232 @@
#ifndef FROSTBYTE_JOBCTL_H
#define FROSTBYTE_JOBCTL_H
#include "types.h"
/*
* Generic "run a binary by path, with real job control" helpers, callable
* only from plain kernel-thread context (shell.c's shellRun, or a boot-time
* demo thread in kmain.c) -- never from inside a syscall's interrupt-gate
* handler, exactly like keyboardReaderReadBlocking/pipeReadBlocking's own
* restriction, since jobctlWaitForeground below parks with threadJoin's
* real halt() loop.
*
* Unlike syscall.c's doSpawn (sysSpawn's underlying implementation, used by
* one ring-3 process to spawn another), a job started here always becomes
* its OWN new process group leader (threadSetPgid(pid, pid)) regardless of
* the caller's own pgid -- matching a real shell's fork()+setpgid(child,
* child) convention for every new job/pipeline, not doSpawn's default of
* inheriting the parent's group (right for a process spawning a plain
* worker of its own, wrong for an interactive shell launching a new
* foreground/background command). argv is already kernel memory here (the
* caller IS the kernel, not a ring-3 process going through a syscall), so
* neither function copies or range-checks it the way copyUserArgv/
* userRangeOk (syscall.c) must for a user-supplied pointer.
*/
/* Loads `path` and spawns it as a new process group leader, hands it "the
terminal" (keyboardSetForegroundPgid, keyboard.h) immediately, and
returns its pid without waiting -- split from the actual wait
(jobctlWaitForeground below) so a caller can learn the pid the instant
the job exists, not only after it has already exited (shell.c's own
background job table needs the same immediacy from
jobctlRunBackground below, so this mirrors it instead of hiding the pid
inside a single combined call). Returns threadInvalidId, touching the
terminal not at all, if `path` cannot be loaded (missing from the VFS,
malformed ELF, or the thread table is full). */
uint32 jobctlSpawnForeground(const char* path, const char* const* argv, int argc);
/* Blocks (threadJoin) until the pid jobctlSpawnForeground just returned
exits, then reclaims the terminal for the CALLING thread's own group --
deliberately not "whatever pgid owned the terminal right before
jobctlSpawnForeground ran" (which could itself be some earlier, now-dead
job's stale id): a real shell always gets its own prompt's terminal back
when a foreground job quits, so a later Ctrl-C typed at the prompt
reaches the shell's NEXT job, not a group nothing will ever answer for
again. Returns the job's real exit code, same contract as threadJoin
itself. */
int64 jobctlWaitForeground(uint32 pid);
/* Loads `path` and spawns it as a new process group leader exactly like
jobctlSpawnForeground, but never touches the foreground pgid at all and
returns the new pid immediately without waiting -- a real shell's `&`
suffix. The caller is responsible for eventually reaping it (e.g.
shell.c's own background job table, reaped opportunistically via
threadJoinAnyChild). Returns false (touching *outPid not at all) on the
same load/spawn failures as jobctlSpawnForeground. */
bool jobctlRunBackground(const char* path, const char* const* argv, int argc,
uint32* outPid);
/* Generalizes jobctlSpawnForeground/jobctlRunBackground's shared "load and
spawn" step for shell.c's new `cmd1 | cmd2` pipeline syntax: `stdinHandle`/
`stdoutHandle` are raw pipe.h handles from vfsPipeCreateHandle (vfs.h),
baked into the new process's own fd 0/fd 1 (vfsPipeAttachInto) BEFORE it
ever runs a single instruction -- -1 for either one leaves that fd at its
normal default (the real console), exactly like an unpiped command's own
stdin/stdout. `joinPgid` is threadInvalidId for "become a new group
leader" (jobctlSpawn's existing default, what every other caller here
still wants) or an already-live pgid to join instead -- a pipeline's
second-and-later stage joins its first stage's own new group so a single
Ctrl-C reaches every stage at once, exactly like a real shell's pipeline
job. Already fully general for a MIDDLE stage of a longer pipeline too:
`stdinHandle`/`stdoutHandle` are independent, so a stage can have both a
piped stdin AND a piped stdout at once (shell.c's runPipelineStages does
exactly this for every interior stage of a 3+-stage pipeline) -- the only
things that ever needed generalizing for that were shell.c's own
parsing/orchestration and jobctlWaitPipelineStages below, never this
function. Never touches the terminal itself: shell.c's own pipeline
runner grants and reclaims it once, for the pipeline as a whole, via
jobctlWaitPipelineStages below -- not per stage. Returns threadInvalidId
on the same load/spawn failures as jobctlSpawnForeground. */
uint32 jobctlSpawnPiped(const char* path, const char* const* argv, int argc,
int stdinHandle, int stdoutHandle, uint32 joinPgid);
/* Blocks until every one of `pids[0..count-1]` (jobctlSpawnPiped's own
return values for a pipeline's stages, `pids[1..count-1]` already joined
to `pids[0]`'s group) has exited, then reclaims the terminal for the
CALLING thread's own group -- the same two-step contract
jobctlWaitForeground has for a single job, just for a whole N-stage
pipeline at once (2 <= count <= shellMaxPipelineStages, shell.h). Grants
the terminal to `pids[0]`'s group (which every other pid is already a
member of) before waiting, exactly like jobctlSpawnForeground already
does for a single job, so a Ctrl-C typed while the pipeline runs reaches
every stage. threadJoin's own real blocking semantics mean joining every
pid in this fixed order is correct regardless of which stage actually
exits first. Returns the LAST stage's exit code, matching a real
pipeline's own `$?` convention (the last command's status). */
int64 jobctlWaitPipelineStages(const uint32* pids, int count);
/* Installs `inPath`/`outPath`/`errPath` file redirections (vfsOpenInto,
vfs.h) into an ALREADY-SPAWNED child's fd 0/fd 1/fd 2 -- shell.c's new
`cmd [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR]` syntax. Any path may be
nullptr to leave that fd untouched (still whatever jobctlSpawn/
jobctlSpawnPiped already gave it -- the real console by default, or a
pipe end for a pipeline stage). `outAppend`/`errAppend` select `>>`/
`2>>` (oAppend, never truncates) over `>`/`2>` (oTrunc) when the
matching path is non-null; ignored otherwise. fd 0's default console
reader is released first, exactly like jobctlSpawnPiped's own stdin
override above, so redirecting it never leaks that reader slot; fd 1
and fd 2 have no such resource (their own default, fdConsole, owns
nothing -- neither is ever handed a reader by vfsFdInit). Exposed as
its own function (rather than folded into jobctlSpawnPiped's already-
long parameter list) so shell.c's `runPipeline` can layer a file
redirect onto exactly one end of a pipeline stage -- the other end
already wired to the shared pipe by jobctlSpawnPiped -- without a
combinatorial explosion of pipe-handle vs. path parameters on one call.
Returns false, leaving the child alive and running with whatever fds it
already had, on a bad path (missing without oCreate's implicit
create-on-write, unwritable parent, a fifo opened with the wrong mode,
exhausted pipe/inode resources) -- the caller decides whether that
warrants killing it (every caller in this kernel currently does, via
threadKill+threadJoin, exactly like runPipeline already does for a
failed second-stage spawn). */
bool jobctlApplyRedirects(uint32 pid, const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend);
/* Same as jobctlSpawnForeground, but installs `inPath`/`outPath`/`errPath`
file redirections (jobctlApplyRedirects above) into the child's fd 0/
fd 1/fd 2 BEFORE the terminal is ever granted to it -- shell.c's
`NAME [ARGS] [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR]`. On a redirect
failure the half-spawned child is killed and reaped before returning
threadInvalidId, exactly as if the load/spawn itself had failed: the
terminal must never be granted to a child whose own stdio could not be
wired up as asked. */
uint32 jobctlSpawnForegroundRedirected(const char* path, const char* const* argv, int argc,
const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend);
/* Same as jobctlRunBackground, but with the same file-redirection support
as jobctlSpawnForegroundRedirected above -- shell.c's
`NAME [ARGS] [< IN] [> OUT | >> OUT] [2> ERR | 2>> ERR] &`. Returns
false (touching *outPid not at all) on the same load/spawn failures as
jobctlRunBackground, or on a redirect failure (the half-spawned child is
killed and reaped first, same as the foreground case). */
bool jobctlRunBackgroundRedirected(const char* path, const char* const* argv, int argc,
const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend,
uint32* outPid);
/* Generalizes jobctlSpawnPiped for shell.c's new `cmd1 <<< TEXT | cmd2`
pipeline syntax, the same way jobctlSpawnPiped itself generalizes
jobctlSpawnForeground/jobctlRunBackground: spawns `path` with fd 0 wired
to a brand-new here-string pipe (`text` plus a trailing newline, written
and closed by THIS kernel thread before returning, exactly like
jobctlSpawnForegroundHereString below) instead of the console, AND
`stdoutHandle`/`joinPgid` passed straight through to jobctlSpawnPiped so
the spawned process can simultaneously be a pipeline's first stage --
-1/threadInvalidId for either one leaves that side at jobctlSpawnPiped's
own plain defaults (console stdout, new group leader). shell.c's
runPipeline uses this for a pipeline's LEFT stage when it was written
with `<<< TEXT` instead of a plain stdin; every other here-string caller
in this file (jobctlSpawnForegroundHereString/jobctlRunBackgroundHereString
below) is a thin wrapper passing -1/threadInvalidId, matching
jobctlSpawnForeground/jobctlRunBackground's own relationship to
jobctlSpawnPiped. Returns threadInvalidId on the same load/spawn
failures as jobctlSpawnPiped, or if the pipe table itself is exhausted
(vfsPipeCreateHandle, maxPipes slots all in use) -- both leave nothing
spawned. */
uint32 jobctlSpawnPipedHereString(const char* path, const char* const* argv, int argc,
const char* text, int stdoutHandle, uint32 joinPgid);
/* Same as jobctlSpawnForegroundRedirected, but for shell.c's new `NAME
[ARGS] <<< TEXT [> OUT | >> OUT] [2> ERR | 2>> ERR]` here-string syntax
instead of `< IN`: `text` (plus a trailing newline, matching a real
shell's own `<<<` behavior) is fed directly into the child's fd 0
through a real anonymous pipe (jobctlSpawnPipedHereString above) that
THIS kernel thread itself writes and closes -- unlike jobctlApplyRedirects'
inPath, there is no file opened at all, so `outPath`/`errPath` are still
applied exactly as jobctlApplyRedirects already does (fd 0 is simply
pre-wired to the here-string pipe instead of left at the console before
that call runs). The write end is closed (real EOF, not a lingering open
pipe) BEFORE this function ever returns, so by the time the child
actually gets a timeslice to read its own fd 0 it sees exactly `text`
followed by one newline followed by true end-of-file, with no
possibility of racing the shell thread for more data that will never
arrive. Returns threadInvalidId on the same load/spawn/redirect failures
as jobctlSpawnForegroundRedirected, plus a pipe-table-exhaustion failure
(vfsPipeCreateHandle, maxPipes slots all in use) -- both leave nothing
spawned. */
uint32 jobctlSpawnForegroundHereString(const char* path, const char* const* argv, int argc,
const char* text, const char* outPath, bool outAppend,
const char* errPath, bool errAppend);
/* Same as jobctlRunBackgroundRedirected, but with the same here-string
stdin support as jobctlSpawnForegroundHereString above -- shell.c's
`NAME [ARGS] <<< TEXT [> OUT | >> OUT] [2> ERR | 2>> ERR] &`. Returns
false (touching *outPid not at all) on the same load/spawn/redirect/
pipe-exhaustion failures as jobctlSpawnForegroundHereString. */
bool jobctlRunBackgroundHereString(const char* path, const char* const* argv, int argc,
const char* text, const char* outPath, bool outAppend,
const char* errPath, bool errAppend,
uint32* outPid);
/* Runs `path` to completion with its stdout wired to a real pipe THIS
kernel thread reads back as a string -- shell.c's new `$(cmd)` command
substitution, the mirror image of jobctlSpawnForegroundHereString's
"write a fixed string into the child's stdin" above. stdin is left at
its plain default (never redirected), and the terminal is never granted
to the child (unlike every foreground job above) -- command substitution
is not itself "the" foreground job, only a synchronous helper run while
building one. Joins the child FIRST, then drains its captured stdout --
not "read until EOF, then join": this kernel only ever releases a dead
thread's own fds (reclaimOpenFds, sched.c) from inside threadReap
(threadJoin's last step), never at threadExit/threadKill itself, so
reading first would block forever waiting for an end-of-file that only
the very threadJoin call coming after it could ever produce. Joining
first instead means `path`'s own combined stdout must fit within one
pipe buffer's worth (pipeBufferSize, pipe.h) without ever blocking on
free space while it runs -- the same margin jobctlSpawnPipedHereString
already relies on for a here-string write, applied here to a captured
read instead -- an honestly-scoped limit on how chatty a substituted
command may be, not a silent hang for anything that stays under it.
Copies at most outCap-1 captured bytes into outBuf (NUL-terminated,
truncating silently like every other fixed-capacity buffer in this
kernel), draining and discarding anything beyond that rather than
leaving it sitting in the pipe. *outStatus receives the child's real
exit code. Returns false, touching neither outBuf nor *outStatus, on
the same load/spawn/pipe-exhaustion failures as jobctlSpawnPiped/
vfsPipeCreateHandle. */
bool jobctlCaptureOutput(const char* path, const char* const* argv, int argc,
char* outBuf, int outCap, int64* outStatus);
#endif
+154 -2
View File
@@ -4,8 +4,160 @@
#include "types.h" #include "types.h"
void keyboardInit(void); void keyboardInit(void);
/* The real PS/2 IRQ1 handler (isr.c): reads the scancode off the hardware
port and hands it to keyboardHandleScancode below. */
void keyboardHandler(void); void keyboardHandler(void);
bool keyboardHasChar(void);
char keyboardReadChar(void); /*
* Per-reader console input queues: a fixed table of ring-buffer-backed byte
* queues (ring.h), each independently allocated and each fed a copy of
* every decoded keystroke by keyboardHandleScancode -- like several
* terminals plugged into one keyboard, every open reader sees every key.
* This replaces an earlier design with exactly one global buffer shared by
* every consumer (the kernel shell and every ring-3 process's fd 0 alike),
* which had two real problems: concurrent readers raced directly on the
* same head/tail indices with no locking, and its blocking wait
* (keyboardReadChar's raw "sti; hlt" loop) was invisible to the scheduler --
* a thread parked there could never be woken by schedWakeChannel, never
* charged CFS-lite vruntime while "running" it, and never recovered by
* threadKill the way every other blocking read/write in this kernel can be
* (pipe.h's pipeReadBlocking/pipeWriteBlocking, vfs.h's fdFlagBlocking
* pipe/fifo ends). Every reader here uses the exact same
* schedBlockCurrent/schedWakeChannel wait-channel contract as pipe.c, wired
* into vfs.c's fdConsole read path (vfsBlockingChannel) so syscall.c's
* sysRead restart-syscall trick works on fd 0 exactly like it already does
* on a pipe end.
*/
/* Bumped from 48 (this pool's very first, never-since-revisited size, set
when sched.h's maxThreads was still 32): vfs.c's vfsFdInit grabs one
reader automatically for EVERY thread ever created (kernel or user alike,
whether it ever reads fd 0 or not, its own doc comment already says so)
at CREATION time, which happens sequentially and entirely inside kmain()
for every boot-time thread, well before schedStart() ever runs -- so the
first 48 creation-order calls always succeeded and every one after them
always silently got consoleReaderInvalid, deterministically, regardless
of scheduling. shell.c's shellRun makes a SEPARATE, later, explicit
keyboardOpenReader() call once the shell thread actually gets a
timeslice (its own real input queue, distinct from its automatic-but-
unused fd 0 slot) -- that call can only succeed if some earlier thread
has ALREADY died and been reaped (reclaimOpenFds, sched.c) by then,
freeing a slot back up. maxThreads grew from 32 to 168 across dozens of
falas' own headroom bumps (sched.c's own history comment) while this
completely separate pool never grew to match, so by FALA 33 kmain.c was
creating far more than 48 threads before schedStart() ever ran --
shellRun's own call was surviving purely on the timing-dependent hope
that at least one of the first 48 automatic grabs belonged to an
already-exited thread by the time it ran, not on any real guarantee.
Adding one more automatically-grabbing kernel thread (hereDriverThread,
kmain.c) tipped this from "usually still works" to "shell has no input"
on a clean run -- caught live by this fala's own gate, not a review.
Sized to maxThreads (168) plus the same +8 headroom margin sched.c's own
bumps use, which is enough for every one of maxThreads' automatic
grabs to always succeed deterministically PLUS shellRun's own extra
explicit call on top, eliminating the race entirely rather than
continuing to depend on exit timing. Bumped again (176 -> 184) in lockstep
with sched.c's own maxThreads bump (168 -> 176, hcat/hsink/
hereStringPipelineDriverThread) -- the same fixed maxThreads+8 relationship,
not re-derived from scratch each time. Bumped again (184 -> 192) in
lockstep with sched.c's own maxThreads bump (176 -> 184, pipecat/
threeStagePipelineDriverThread), same relationship. Bumped again
(192 -> 200) in lockstep with sched.c's own maxThreads bump (184 -> 192,
cwdtest/cwdtarget), same relationship. Bumped again (200 -> 208) in
lockstep with sched.c's own maxThreads bump (192 -> 200, envprint/
envDriverThread), same relationship. Bumped again (208 -> 216) in
lockstep with sched.c's own maxThreads bump (200 -> 208, nicetest/
nicetarget), same relationship. Bumped again (216 -> 224) in lockstep
with sched.c's own maxThreads bump (208 -> 216, date), same
relationship. Bumped again (224 -> 232) in lockstep with sched.c's own
maxThreads bump (216 -> 224, timetest), same relationship. Bumped again
(232 -> 240) in lockstep with sched.c's own maxThreads bump (224 -> 232,
lsDriverThread), same relationship. Bumped again (240 -> 248) in
lockstep with sched.c's own maxThreads bump (232 -> 240,
andOrDriverThread), same relationship. Bumped again (248 -> 256) in
lockstep with sched.c's own maxThreads bump (240 -> 248, rmtest), same
relationship. Bumped again (256 -> 264) in lockstep with sched.c's own
maxThreads bump (248 -> 256, permtest plus its one transient runtime-
spawned child), same relationship. Bumped again (264 -> 272) in lockstep
with sched.c's own maxThreads bump (256 -> 264, dirpermtest), same
relationship. Bumped again (272 -> 280) in lockstep with sched.c's own
maxThreads bump (264 -> 272, grouptest plus its two transient runtime-
spawned children), same relationship. Bumped again (280 -> 288) in
lockstep with sched.c's own maxThreads bump (272 -> 280, pidfdtest plus
its one transient runtime-spawned killvictim), same relationship.
Bumped again (288 -> 296) in lockstep with sched.c's own maxThreads bump
(280 -> 288, mmaptest plus its one transient runtime-spawned
mmapprotvictim), same relationship. */
#define maxConsoleReaders 296
#define consoleReaderInvalid (-1)
/* Allocates a fresh, empty reader queue and returns its handle, or
consoleReaderInvalid if the table is full. Every thread gets one for its
own fd 0 at creation time (vfs.c's vfsFdInit) -- one keystroke delivered
by keyboardHandleScancode reaches every thread's queue independently, so
two unrelated processes both reading fd 0 each see their own copy rather
than racing for the same byte. */
int keyboardOpenReader(void);
/* Releases a reader's slot for reuse. No-op on an invalid handle. */
void keyboardCloseReader(int handle);
/* Non-blocking: pops up to length queued bytes into buffer, in the order
they were typed. Returns the number of bytes actually copied (which may
be less than length, or 0 only when length itself was 0), or -1 for an
invalid/closed handle or when the queue is currently empty (a live
keyboard has no end-of-file the way a pipe's write end closing does --
"nothing typed yet" and "try again later" are the same thing). */
int64 keyboardReaderRead(int handle, void* buffer, uint64 length);
/* Blocks (parks the calling thread and halts) until at least one byte is
available, then behaves exactly like keyboardReaderRead. Only valid from
plain kernel-thread context (see shell.c) -- calling this from inside a
syscall's interrupt-gate handler would freeze the whole machine, not just
the caller, the same hazard pipeReadBlocking's doc comment (pipe.h)
describes; a syscall handler must instead use vfsBlockingChannel (vfs.h)
with the restart-syscall pattern. Returns -1 immediately, without
blocking, for an invalid handle. */
int64 keyboardReaderReadBlocking(int handle, void* buffer, uint64 length);
/* The address schedBlockCurrent/schedWakeChannel use for this reader's wait
queue -- mirrors pipe.h's pipeChannel exactly, including returning
nullptr for an invalid handle so a syscall handler can tell "do not
block, this fd cannot ever produce more data" apart from "wait and
retry". */
const void* keyboardReaderChannel(int handle);
/* Non-consuming readiness peek for poll.h's vfsPollReady (vfs.c): true iff
keyboardReaderRead would return actual bytes right now, without popping
any of them off the ring. False for an invalid handle. */
bool keyboardReaderHasData(int handle);
/* The scancode-decoding half of keyboardHandler, split out so tests (and
the live boot demo in kmain.c, which has no automated way to drive a real
PS/2 device under headless QEMU) can inject a keystroke exactly the way
real hardware would, without needing to fake port I/O. Handles shift and
(now) left-Ctrl make/break codes, intercepts the real tty INTR/SUSP
combination (Ctrl-C/Ctrl-Z) as job-control signals to the foreground
process group instead of ordinary data (see the file comment in
keyboard.c), and otherwise broadcasts the decoded character to every open
reader queue, waking anyone blocked on one via schedWakeChannel. */
void keyboardHandleScancode(uint8 scancode);
/* Real tcsetpgrp(3): names the process group (sched.h's Thread.pgid) that
OWNS "the terminal" from this point on -- the only pgid keyboardHandleScancode's
Ctrl-C/Ctrl-Z interception (below) ever delivers sigInt/sigTstp to.
0 (the default, before anything ever calls this -- see keyboardInit) can
never match any live user thread's own pgid (every boot-spawned process
becomes its own new group leader, sched.c's initialPgid, so the lowest
possible real pgid is 1; only the permanent idle thread, which never
reads a keyboard, is ever pgid 0), so "nobody has claimed the foreground
yet" and "deliver to no one" are naturally the same state, with no extra
sentinel needed. */
void keyboardSetForegroundPgid(uint32 pgid);
/* The pgid keyboardSetForegroundPgid last recorded (0 if never called since
keyboardInit) -- real tcgetpgrp(3)'s counterpart, exposed to userspace
through sysGetFgPgrp/userGetForegroundPgid. */
uint32 keyboardForegroundPgid(void);
#endif #endif
+71
View File
@@ -0,0 +1,71 @@
#ifndef FROSTBYTE_KLIB_H
#define FROSTBYTE_KLIB_H
#include "types.h"
#include <stdarg.h>
/*
* klib: freestanding string/number utilities that build on top of the
* primitive mem-family and str-family routines in string.h. No libc, no
* heap dependency (callers pass their own buffers), safe to use before the
* heap is set up.
*/
/* ---- character classification -------------------------------------- */
bool isDigit(int c);
bool isAlpha(int c);
bool isAlnum(int c);
bool isSpace(int c);
bool isUpper(int c);
bool isLower(int c);
bool isHexDigit(int c);
int toUpper(int c);
int toLower(int c);
/* ---- string manipulation --------------------------------------------
Semantics match the C standard library equivalents (including the
footguns: strcpy/strcat assume a big-enough destination). */
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, usize count);
char* strcat(char* dest, const char* src);
char* strncat(char* dest, const char* src, usize count);
char* strchr(const char* str, int c);
char* strrchr(const char* str, int c);
char* strstr(const char* haystack, const char* needle);
usize strspn(const char* str, const char* accept);
usize strcspn(const char* str, const char* reject);
/* Reentrant tokenizer (no hidden static state, safe across threads).
Pass str on the first call, nullptr on subsequent calls; *savePtr
carries the position between calls (caller owns the storage). */
char* strTokR(char* str, const char* delim, char** savePtr);
/* ---- number parsing --------------------------------------------------- */
int32 atoi(const char* str);
int64 atol(const char* str);
/* base 0 means auto-detect: "0x"/"0X" prefix -> 16, leading "0" -> 8,
otherwise 10. base must be 0 or in [2, 36] otherwise. If endPtr is
non-null, *endPtr is set to the first unconsumed character. */
int64 strtol(const char* str, char** endPtr, int base);
uint64 strtoul(const char* str, char** endPtr, int base);
/* ---- formatted output into a caller-owned buffer ----------------------
Same contract as C99 snprintf: writes at most size-1 characters plus a
terminating nul (unless size == 0), and returns the number of characters
that *would* have been written given an unbounded buffer. Supports
%c %s %d %i %u %x %X %p %%, an 'l' length modifier for 64-bit args, and
a zero-padded/space-padded minimum field width. */
int snprintf(char* buf, usize size, const char* format, ...);
int vsnprintf(char* buf, usize size, const char* format, va_list args);
/* ---- generic sorting / searching --------------------------------------
Same contract as C's qsort/bsearch: cmp returns <0, 0, >0. */
void qsort(void* base, usize count, usize size, int (*cmp)(const void*, const void*));
void* bsearch(const void* key, const void* base, usize count, usize size,
int (*cmp)(const void*, const void*));
#endif
+82
View File
@@ -0,0 +1,82 @@
#ifndef FROSTBYTE_KTEST_H
#define FROSTBYTE_KTEST_H
#include "types.h"
/*
* In-kernel self-test harness (KUnit-style).
*
* A test is written as:
*
* #include "ktest.h"
*
* KTEST(mySubsystemBasics)
* {
* KT_EQ(t, 2 + 2, 4);
* KT_TRUE(t, somePredicate());
* }
*
* The KTEST macro both declares the test body and registers a pointer to it
* into the ".ktests" linker section, so no shared registry file has to be
* edited when a new test is added. ktestRunAll() (called once from kmain
* before the scheduler starts) iterates the section, runs every test, prints
* per-test results and a final machine-readable line:
*
* KTEST SUMMARY: <tests> tests, <checks> checks, <failures> failed
*
* The boot gate (tools/ci-test.sh) requires that line to report 0 failures,
* so a broken subsystem makes the gate fail instead of silently passing.
*
* Tests run with interrupts disabled, before the scheduler starts. They must
* be deterministic and must NOT depend on timer ticks, preemption or user
* threads. They may use kmalloc/kfree, the string helpers, pmm and any
* subsystem they explicitly initialise themselves.
*/
typedef struct KtestCtx {
const char* name;
uint64 checks;
uint64 failures;
} KtestCtx;
typedef void (*KtestFn)(KtestCtx* t);
typedef struct KtestEntry {
const char* name;
KtestFn fn;
} KtestEntry;
/* Runs every registered test, prints results, returns the number of failed
checks across all tests (0 means the whole battery is green). */
uint64 ktestRunAll(void);
/* Records one check. On failure it prints "FAIL file:line expr" and marks
the current test context as failed. Returns the boolean result so callers
can branch (e.g. skip a dereference after a null check failed). */
bool ktestCheck(KtestCtx* t, bool ok, const char* expr, const char* file, int line);
/* Null-safe string comparison used by KT_STREQ. */
bool ktestStrEq(const char* a, const char* b);
#define KTEST(testName) \
static void testName##KtestFn(KtestCtx* t); \
static const KtestEntry testName##KtestEntry = { #testName, testName##KtestFn }; \
__attribute__((section(".ktests"), used)) \
static const KtestEntry* const testName##KtestPtr = &testName##KtestEntry; \
static void testName##KtestFn(KtestCtx* t)
#define KT_TRUE(t, cond) ktestCheck((t), (cond) ? true : false, #cond, __FILE__, __LINE__)
#define KT_FALSE(t, cond) ktestCheck((t), (cond) ? false : true, "!(" #cond ")", __FILE__, __LINE__)
#define KT_EQ(t, a, b) ktestCheck((t), (int64)(a) == (int64)(b), #a " == " #b, __FILE__, __LINE__)
#define KT_NE(t, a, b) ktestCheck((t), (int64)(a) != (int64)(b), #a " != " #b, __FILE__, __LINE__)
#define KT_LT(t, a, b) ktestCheck((t), (int64)(a) < (int64)(b), #a " < " #b, __FILE__, __LINE__)
#define KT_LE(t, a, b) ktestCheck((t), (int64)(a) <= (int64)(b), #a " <= " #b, __FILE__, __LINE__)
#define KT_GT(t, a, b) ktestCheck((t), (int64)(a) > (int64)(b), #a " > " #b, __FILE__, __LINE__)
#define KT_GE(t, a, b) ktestCheck((t), (int64)(a) >= (int64)(b), #a " >= " #b, __FILE__, __LINE__)
#define KT_EQU(t, a, b) ktestCheck((t), (uint64)(a) == (uint64)(b), #a " ==u " #b, __FILE__, __LINE__)
#define KT_EQP(t, a, b) ktestCheck((t), (const void*)(a) == (const void*)(b), #a " ==p " #b, __FILE__, __LINE__)
#define KT_NULL(t, p) ktestCheck((t), (const void*)(p) == nullptr, #p " == null", __FILE__, __LINE__)
#define KT_NOTNULL(t, p) ktestCheck((t), (const void*)(p) != nullptr, #p " != null", __FILE__, __LINE__)
#define KT_STREQ(t, a, b) ktestCheck((t), ktestStrEq((a), (b)), #a " streq " #b, __FILE__, __LINE__)
#endif
+43
View File
@@ -0,0 +1,43 @@
#ifndef FROSTBYTE_LIST_H
#define FROSTBYTE_LIST_H
#include "types.h"
#include <stddef.h>
/*
* List: an intrusive circular doubly-linked list, Linux-kernel style. The
* list head and every node share the same ListNode type; a sentinel node
* (passed to listInit) acts as both head and tail so insert/remove never
* need a null check. Embed a ListNode in your own struct and use
* listEntry() to recover the containing struct pointer -- no allocation,
* no element copying, just pointer relinking.
*/
typedef struct ListNode {
struct ListNode* next;
struct ListNode* prev;
} ListNode;
#define listEntry(ptr, type, member) \
((type*)((uint8*)(ptr) - offsetof(type, member)))
#define listForEach(cursor, head) \
for ((cursor) = (head)->next; (cursor) != (head); (cursor) = (cursor)->next)
void listInit(ListNode* head);
bool listEmpty(const ListNode* head);
void listInsertAfter(ListNode* pos, ListNode* newNode);
void listInsertBefore(ListNode* pos, ListNode* newNode);
void listPushBack(ListNode* head, ListNode* newNode);
void listPushFront(ListNode* head, ListNode* newNode);
/* Unlinks node from whatever list it is in. Safe to call again on an
already-removed node (it becomes its own trivial one-element list). */
void listRemove(ListNode* node);
usize listLen(const ListNode* head);
ListNode* listFront(const ListNode* head);
ListNode* listBack(const ListNode* head);
#endif
+120
View File
@@ -0,0 +1,120 @@
#ifndef FROSTBYTE_MMAP_H
#define FROSTBYTE_MMAP_H
#include "types.h"
#include "fs.h"
struct Thread;
/* Cap on live mappings per thread. Small and fixed like every other
per-Thread table in this kernel (maxOpenFiles=16, sigMaxSignals=32) --
nothing here needs more than a handful of distinct mappings at once, and
a hard cap keeps Vma[] a plain fixed-size array embedded directly in
Thread (sched.h), exactly like FileDescriptor fds[maxOpenFiles] already
is, rather than something that needs its own allocator. */
#define maxVmasPerThread 8
/* One live mapping inside a single thread's own elfSlotSize slot (see
elf.h) -- mmap.c reserves [start, end) at mmapReserve time but allocates
no physical frames and reads no file data yet; mmapFaultHandler below
fills each page lazily, the first time a real #PF (isr.c) proves it was
actually touched. `fd` (meaningful only when `anonymous` is false) names
a slot in the OWNING thread's own fd table (vfs.h) -- this mapping is
only ever resolved for its own owning thread (mmapFaultHandler's own
contract), so there is no cross-thread fd ambiguity. Unlike a real
kernel's struct vm_area_struct, there is no backing inode reference
independent of that fd: closing `fd` before every page has been faulted
in leaves this mapping unable to fill the rest (mmapFaultHandler treats
a since-closed fd as a fault it cannot resolve, turning into a real
SIGSEGV rather than silently serving zeroed pages for wrong reasons) --
an honestly scoped limitation (real mmap(2) lets a mapping outlive the
fd that created it), not a hidden bug, chosen because holding a
reference to a file independent of any thread's fd table would need a
refcounted VfsNode/inode layer this kernel does not otherwise have. */
typedef struct Vma {
bool used;
uint64 start;
uint64 end;
uint32 prot;
bool anonymous;
int fd;
uint64 fileOffset;
} Vma;
/* Reserves [addr, addr+length) -- or, when addr is 0, a first-fit region
of `length` bytes the kernel picks itself, real mmap(NULL, ...)-style --
inside `thread`'s own mmap region ([userSlotBase+elfSlotStackOffset,
userSlotBase+elfSlotSize), see elf.h: the 128 MiB of every elfSlotSize
slot that sits above the small fixed-size stack and is never otherwise
touched by an ELF image or brk). No physical frame is allocated and no
byte is read yet -- see mmapFaultHandler for when that actually happens.
On success, writes the mapping's real base address to *outAddr (matters
when addr was 0) and returns true. Returns false, touching *outAddr not
at all, if: length is 0, addr or length is not frameSize-aligned, a
non-anonymous mapping's fileOffset is not frameSize-aligned, the
resulting range would fall outside the mmap region, it would overlap an
existing Vma of this same thread, no addr was given and no free region
of that size exists, or every one of maxVmasPerThread's own slots is
already in use. */
bool mmapReserve(struct Thread* thread, uint64 addr, uint64 length, uint32 prot,
bool anonymous, int fd, uint64 fileOffset, uint64* outAddr);
/* Real munmap(2): removes every Vma of `thread` covering any part of
[addr, addr+length), giving back to the pmm whatever physical frames
were actually faulted in for the removed range (pagingUnmapUserRange,
paging.h -- the same "only what is actually present costs anything"
contract that function already documents for itself). A length that only
overlaps one end of an existing Vma shrinks it in place; a length that
lands entirely inside one Vma's interior, touching neither end, splits
it into two (consuming one more Vma slot for the upper half) -- since
Vmas of the same thread never overlap each other (mmapReserve's own
refusal), at most one Vma can ever need splitting per call, so this
either finds the one free slot it needs or refuses the WHOLE call up
front, leaving every Vma of this thread completely untouched, rather
than leaving the caller's mapping table half-modified by a resource
failure it could not have predicted from addr/length alone. A range that
overlaps no Vma of this thread at all is a no-op returning true, real
munmap(2)'s own "unmapping already-unmapped memory is not an error"
behavior. Returns false only for a misaligned addr, a zero length, or
the above split-with-no-free-slot case. */
bool mmapRelease(struct Thread* thread, uint64 addr, uint64 length);
/* Looks up the (at most one, since a thread's own Vmas never overlap each
other) Vma of `thread` covering `addr`, or nullptr if none does. */
Vma* mmapFind(struct Thread* thread, uint64 addr);
/* The real page-fault resolver: called by isr.c for a ring-3 #PF whose
faulting address might belong to `thread`'s own mmap region. Looks up
the covering Vma (mmapFind); returns false, mapping nothing, if there is
none, or if `forWrite` is true but the Vma's own prot lacks
mmapProtWrite (isr.c turns either case into a real SIGSEGV, see its own
comment) or (mmapProtRead is missing on a read fault, refused the same
way). Otherwise allocates one physical frame, fills it -- zeroed for an
anonymous Vma; for a file-backed one, read from the Vma's own `fd`
starting at the file offset that corresponds to this page (whatever
vfsRead returns short of a full frameSize read, e.g. a page straddling
end-of-file, is left zero-padded, since the frame was already
zero-filled before the read) -- maps it at the containing page with
pageUser | (mmapProtWrite set ? pageWrite : 0), and returns true so
isr.c can simply retry the faulting instruction, exactly like any other
successfully resolved demand-paging fault. Only ever meaningful when
`thread` is the CURRENTLY RUNNING thread (it reads/restores that
thread's own fd offset through vfsRead/vfsLseek, which resolve fd
numbers against currentThread() -- true for both isr.c's real caller and
mmapTest.c's own KTEST coverage, which runs entirely on the idle thread,
itself always currentThread() while KTEST executes). */
bool mmapFaultHandler(struct Thread* thread, uint64 faultAddr, bool forWrite);
/* Zeroes every Vma of `thread` without touching any physical frame or
page-table entry -- callers that already reclaim frames some other way
(doExec's own pagingUnmapUserRange over the relevant range, syscall.c)
call this afterward purely to stop a later mmapReserve on the same
thread from ever finding a stale Vma left over from an image this
thread no longer runs. threadReap/threadDiscard (sched.c) do NOT need
to call this themselves: both already memset() the whole Thread struct
before a slot is handed back to allocSlot, which zeroes the embedded
Vma[] array as a side effect, exactly like every other per-thread table
in it. */
void mmapReleaseAll(struct Thread* thread);
#endif
+74
View File
@@ -7,8 +7,82 @@
#define pageWrite 0x2 #define pageWrite 0x2
#define pageUser 0x4 #define pageUser 0x4
/* Real x86_64 PTE bit 63 (XD/NX) -- refuses instruction fetch against a
page that has it set, enforced by the CPU itself once
pagingNxSupported() is true (boot.S enables IA32_EFER.NXE at boot,
gated on CPUID.80000001H:EDX.NX so this never sets a reserved bit on
hardware/hypervisors that predate it). pagingMap silently drops this
bit from the leaf entry it writes when NX is not supported -- the page
stays executable rather than risk a reserved-bit #PF, an honest
best-effort rather than a promise this bit is always enforced. */
#define pageNoExecute 0x8000000000000000ull
/* True once boot.S has confirmed both that the CPU reports
CPUID.80000001H:EDX.NX and that IA32_EFER.NXE was actually set --
callers that care whether pageNoExecute is real hardware enforcement
(as opposed to a bit pagingMap quietly ignored) check this. */
bool pagingNxSupported(void);
bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags); bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags);
uint64 pagingResolve(uint64 virtualAddr); uint64 pagingResolve(uint64 virtualAddr);
/* True if virtualAddr is currently mapped AND executable (present, and
either pageNoExecute was never asked for or the CPU cannot enforce it
-- see pagingNxSupported). False for an unmapped address or one mapped
with pageNoExecute successfully applied. Introspection only, used by
pagingTest.c/mmapTest.c to verify pagingMap/pagingMapUserRangeFlags
actually wrote the bit they were asked to, without needing a live
instruction-fetch fault to prove it indirectly. */
bool pagingIsExecutable(uint64 virtualAddr);
/* Same shape as pagingIsExecutable, for pageWrite instead of pageNoExecute:
true if virtualAddr is currently mapped AND writable, false for an
unmapped address or one mapped without pageWrite. Introspection only,
added alongside pagingMapUserRangeExact so pagingTest.c can prove a
range came back genuinely read-only (e.g. an ELF binary's own .text/
.rodata, elf.c's elfLoad) directly, without needing a live user-mode
write fault to prove it indirectly the way user/elfwritevictim.c's
SIGSEGV does. */
bool pagingIsWritable(uint64 virtualAddr);
bool pagingMapUserRange(uint64 virtualAddr, uint64 size); bool pagingMapUserRange(uint64 virtualAddr, uint64 size);
/* Same as pagingMapUserRange, plus extraFlags (pageNoExecute, currently
the only bit worth passing here) OR'd into every leaf PTE this call
creates -- pagingMapUserRange itself is a thin wrapper passing 0.
Callers that want a non-executable range (a stack, a brk-grown heap --
see kmain.c/syscall.c/jobctl.c's stack setup and sched.c's
currentThreadSbrk) pass pageNoExecute. A thin wrapper itself, over
pagingMapUserRangeExact below, that always forces pageWrite on --
every caller of this entry point (the ones just named, plus
pagingMapUserRange) wants a writable range and only ever varies whether
it is also executable. elf.c's own per-ELF-segment loader is the one
caller in this kernel that needs a range which may be READ-ONLY too (a
binary's own .text/.rodata), so it calls pagingMapUserRangeExact
directly instead. */
bool pagingMapUserRangeFlags(uint64 virtualAddr, uint64 size, uint64 extraFlags);
/* Lowest-level of the three pagingMapUserRange* entry points: flags is
the exact set of extra leaf PTE bits (pageWrite, pageNoExecute) OR'd
onto pageUser for every page in the range -- unlike
pagingMapUserRangeFlags, pageWrite is NOT forced on, so a caller that
omits it gets a genuinely read-only mapping. elf.c's elfLoad is the
only caller today: each ELF program header's own p_flags (via
elfSegmentPagingFlags) decides per-segment whether the range this call
maps is writable, executable, both, or (read-only, non-executable)
neither -- real per-segment W^X instead of one blanket writable+
executable range for a whole binary's image. */
bool pagingMapUserRangeExact(uint64 virtualAddr, uint64 size, uint64 flags);
/* Inverse of pagingMapUserRange: walks [virtualAddr, virtualAddr+size) one
frame at a time and, for every page that is actually present, gives its
physical frame back to the pmm and clears the PTE (present bit gone,
TLB invalidated). Pages that were never mapped -- including whole page-
table levels that were never allocated -- are silently skipped rather
than treated as an error: callers (threadReap/threadDiscard in sched.c,
doExec in syscall.c) always unmap a thread's whole elfSlotSize-sized
slot, most of which a typical small user program never touches. Safe to
call on a range that overlaps no mapping at all (a no-op). */
void pagingUnmapUserRange(uint64 virtualAddr, uint64 size);
#endif #endif
+67
View File
@@ -0,0 +1,67 @@
#ifndef FROSTBYTE_PIDFD_H
#define FROSTBYTE_PIDFD_H
#include "types.h"
#define maxPidfds 32
/*
* Real Linux precedent: pidfd_open(2) (Linux 5.3+) exists for exactly the
* ABA-style race sched.c's own allocSlot doc comment describes and
* deliberately leaves unfixed for plain pid arguments -- killtest.c's own
* comment already admits it ("killing it again must fail cleanly, not
* resurrect a slot that has since been reused") without ever guarding
* against it. A raw numeric pid can always be reassigned to a completely
* unrelated process once its original holder is reaped, so a pid cached
* from an earlier spawn/lookup is fundamentally unsafe to act on later
* with no way to tell "still the same process" apart from "coincidentally
* the same number now". Real Linux's own fix was not to make pid itself
* opaque -- every existing kill(2)/waitpid(2)/setpgid(2) caller still
* takes a raw pid, completely unchanged, forever -- but to add a SEPARATE,
* opt-in handle type that pins one specific incarnation of a slot instead.
* This file mirrors that shape exactly: pidfdOpen captures the target
* thread's CURRENT sched.c Thread.generation (the same "distinguish this
* specific occupant from any future occupant of the same numeric slot"
* idea pipe.c's own pipeIncarnation already uses for pipe handles), and
* every later pidfd operation re-checks it before ever touching the
* target -- if the original thread has since been reaped (its generation
* reset to 0 by threadReap/threadDiscard's memset) or the slot has been
* handed to a brand new, unrelated thread (a different, fresh nonzero
* generation), the pidfd cleanly reports "gone" (threadInvalidId) instead
* of silently operating on whichever live thread now happens to occupy
* that slot number. sched.c's raw pid-based threadKill/threadJoin/
* sigRaiseInfo/threadSetPgid/threadSetPriority are deliberately untouched
* by this file -- see allocSlot's own doc comment (sched.c) for why that
* remains a separate, much larger, deferred topic.
*/
/* Opens a new pidfd handle on thread `targetId`, pinning its CURRENT
generation (sched.h). Returns a small non-negative handle for
pidfdTarget/pidfdClose below, or -1 for id 0 (idle, never a valid
target), an out-of-range id, an already-unused slot, or a full pidfd
table (maxPidfds handles system-wide, mirroring pipe.c's own fixed-size
maxPipes table). Does not itself keep the target alive or pinned in any
way -- it can still exit, be killed, and be reaped by anyone (this
kernel's pids are never ownership-scoped, see sched.h's parentId doc
comment) while a pidfd on it is open; that is exactly the case
pidfdTarget below exists to detect. */
int pidfdOpen(uint32 targetId);
/* Returns the live target thread id this handle still safely refers to,
or threadInvalidId if the handle itself is invalid/closed OR the
original target has been reaped or recycled since pidfdOpen -- the one
check every pidfd-based operation needs, folded into a single call so
no caller can accidentally act on a stale target by forgetting a
separate validity check. A target that has exited but not yet been
reaped (threadDead) still returns its real id here -- exactly like a
raw pid, waiting on it should still be able to observe the real exit
status once, via whatever reaps it. */
uint32 pidfdTarget(int handle);
/* Frees a pidfd handle. No-op on an already-invalid handle. Never affects
the target thread itself in any way -- a pidfd, like a real Linux one,
is purely an observer/signaler, not a reference that keeps anything
alive. */
void pidfdClose(int handle);
#endif
+134
View File
@@ -0,0 +1,134 @@
#ifndef FROSTBYTE_PIPE_H
#define FROSTBYTE_PIPE_H
#include "types.h"
#define maxPipes 32
#define pipeBufferSize 256
/*
* Anonymous pipes: a fixed table of ring-buffer-backed byte queues, each
* shared between one read end and one write end. The raw pipeRead/pipeWrite
* primitives are always non-blocking, matching real O_NONBLOCK semantics:
*
* pipeRead:
* >0 bytes actually read
* 0 end-of-file: buffer drained AND the write end is closed
* -1 no data right now but the write end is still open (retry later)
*
* pipeWrite:
* >0 bytes actually written
* 0 length was 0
* -1 broken pipe (no read end left), or no free space right now
* (retry later)
*
* pipeReadBlocking/pipeWriteBlocking are built on top: they park the calling
* kernel thread on the pipe's wait channel (sched.h) instead of returning
* "try again", and are woken as soon as the other end changes the condition
* (data arrives, space frees up, or either end closes). They only make sense
* from kernel-thread context (see the producer/consumer demo in kmain.c) -
* a single process's own fd table cannot hand one pipe end to a second,
* independent execution context by itself.
*
* pipeCreateDetached + pipeAddReader/pipeAddWriter (below) close that gap
* without needing fork(): a handle from pipeCreateDetached starts with
* NEITHER end attached (unlike pipeCreate, which assumes its caller
* immediately owns both), so it can be handed to two entirely separate,
* independently spawned processes -- typically as a decimal string in argv
* (see user/fifomaster.c) -- each of which attaches its own end via
* vfsPipeAttach (vfs.h). syscall.c's sysRead/sysWrite give a real blocking
* read()/write() to whichever of those two ends is waiting, using the same
* restart-the-syscall trick sysWait already uses for threadJoin.
*/
int pipeCreate(void);
/* Like pipeCreate, but neither end starts "attached": readers and writers
both begin at 0, and pipeRead/pipeWrite treat "nobody has attached that
end YET" as "try again" rather than the true EOF/broken-pipe result they
report once an end that WAS attached goes away (see everHadReader/
everHadWriter, pipe.c) -- otherwise a writer that attaches before any
reader does would see an immediately, permanently "broken" pipe, and a
reader that attaches first would see immediate spurious EOF. */
int pipeCreateDetached(void);
/* Registers a new, independent reader/writer on an existing (typically
pipeCreateDetached'd) handle -- vfsPipeAttach (vfs.h) calls these when a
process attaches its end. No-op on an invalid handle. */
void pipeAddReader(int handle);
void pipeAddWriter(int handle);
/* True if a write to this pipe can never succeed again: either the handle
is not valid, or a reader was attached at some point and every attached
reader has since gone away. False for a pipeCreateDetached handle that
simply has not had its reader attach yet -- that is "try again", not
broken. Lets syscall.c's sysWrite tell "keep retrying" apart from "give
up and return -1" without reaching into pipe.c's own Pipe struct. */
bool pipeWriteBroken(int handle);
/* The address schedBlockCurrent/schedWakeChannel use for this pipe's wait
queue -- the same one pipeReadBlocking/pipeWriteBlocking already block
on. Exposed so a syscall handler (syscall.c) can park on it via the
restart-the-syscall pattern instead of calling halt() directly (which,
run from inside an interrupt-gate syscall handler with IF=0, would freeze
the whole machine rather than just the calling thread -- see sysWait's
own doc comment in syscall.c for the same hazard). Returns nullptr for
an invalid handle. */
const void* pipeChannel(int handle);
bool pipeValid(int handle);
/* Opaque token identifying THIS specific pipe living at `handle`'s slot,
distinct from any other pipe that has occupied (or will ever occupy) the
same numeric handle before or after -- this fixed-size table reuses a
slot number the moment both its ends are gone (pipeCloseRead/
pipeCloseWrite's pipeReleaseIfUnused), so pipeValid(handle) alone cannot
tell a caller that cached a handle across such a release "the same pipe I
bound to earlier" apart from "a different, unrelated pipe that happens to
have been assigned the same number since" (see vfs.c's SFS-backed named
fifo bindings, sfsFifoHandleFor, the motivating caller). Always non-zero
for any handle pipeCreate/pipeCreateDetached has ever handed out; 0 for
an invalid handle, which can never equal a real incarnation, so a naive
"compare and treat 0 as impossible" check is always safe. */
uint32 pipeIncarnation(int handle);
int64 pipeRead(int handle, void* buffer, uint64 length);
int64 pipeWrite(int handle, const void* buffer, uint64 length);
/* Non-consuming readiness peeks for poll.h's vfsPollReady (vfs.c): report
exactly the same two conditions pipeRead/pipeWrite's own success paths
check (data queued or true EOF; free space in the ring), without moving
a single byte or touching readers/writers bookkeeping. False for an
invalid handle either way. */
bool pipeHasData(int handle);
bool pipeHasSpace(int handle);
/* Blocks (parks the calling thread and halts) until data is available or
true end-of-file is reached. Returns exactly what pipeRead would once the
wait is over; -1 only for an invalid handle (never blocks in that case). */
int64 pipeReadBlocking(int handle, void* buffer, uint64 length);
/* Blocks until free space is available. Returns exactly what pipeWrite would
once the wait is over; returns -1 immediately without blocking for an
invalid handle or a broken pipe (no reader left), since neither condition
can ever resolve by waiting. */
int64 pipeWriteBlocking(int handle, const void* buffer, uint64 length);
void pipeCloseRead(int handle);
void pipeCloseWrite(int handle);
/* Frees a pipeCreateDetached handle that nobody has ever attached a reader
or writer to (readers==0 and writers==0) -- the one case pipeCloseRead/
pipeCloseWrite's own release-when-both-zero check (pipeReleaseIfUnused)
can never reach on its own, since neither is ever called for an end that
was never attached in the first place. Needed by a named pipe (vfs.h's
vfsMkfifo/vfsUnlink) created but never opened by anyone: without this, its
underlying handle would sit "used" in the fixed-size pipe table forever
once the vfs node naming it is removed. No-op (and reports false) if the
handle is invalid or still has a reader or writer attached -- unlinking a
named pipe that IS currently open must leave it alone, matching real
unlink-while-open semantics (existing fds keep working off the raw handle
number, not the vfs node). */
bool pipeDestroyIfUnattached(int handle);
#endif
+34
View File
@@ -0,0 +1,34 @@
#ifndef FROSTBYTE_POLL_H
#define FROSTBYTE_POLL_H
#include "types.h"
/*
* Real poll(2): waits for one or more file descriptors to become ready
* without picking, ahead of time, which single one to block on the way
* userRead/userWrite's own retry-the-syscall path already does for exactly
* one fd at a time (vfs.h's vfsBlockingChannel). Bit values match real
* Linux <poll.h> (POLLIN=0x1, POLLOUT=0x4, POLLERR=0x8, POLLHUP=0x10,
* POLLNVAL=0x20) even though nothing here reads a Linux binary's struct --
* there is no reason to invent different numbers for the same concept.
*/
#define pollIn 0x1
#define pollOut 0x4
#define pollErr 0x8
#define pollHup 0x10
#define pollNval 0x20
typedef struct PollFd {
int32 fd;
int16 events;
int16 revents;
} PollFd;
/* Bounded the same way maxOpenFiles (vfs.h) bounds one process's own fd
table -- sysPoll (syscall.c) copies the caller's array into a fixed
kernel-stack buffer of exactly this size, so a request for more entries
than any one process could ever have open fds for is simply rejected
rather than silently truncated. */
#define maxPollFds 16
#endif
+62
View File
@@ -0,0 +1,62 @@
#ifndef FROSTBYTE_PRIOQ_H
#define FROSTBYTE_PRIOQ_H
#include "types.h"
#include "rbt.h"
/*
* Prioq: an ephemeral min-vruntime priority queue built on top of the
* intrusive red-black tree (rbt.h) -- the "run queue" half of a CFS-lite
* (Completely Fair Scheduler, weighted-fair) scheduler. The caller owns the
* PrioQNode storage (typically embedded in a scheduler's thread struct,
* next to the vruntime counter it mirrors) and is expected to rebuild the
* queue from scratch every time a scheduling decision is needed: insert
* every currently-eligible candidate, pop the minimum, discard the rest.
* That "ephemeral" usage pattern (rather than a queue that persists node
* membership across calls) sidesteps a real hazard for a preemptive kernel
* scheduler: a thread can become eligible again from code paths other than
* the scheduler itself (e.g. a wait-queue wakeup), and keeping a
* long-lived queue in sync with every one of those paths is exactly the
* kind of dual-source-of-truth bug this design avoids by never trying.
*
* Ordering key is (vruntime, id): id is a mandatory, unique tiebreaker
* because rbtInsert silently rejects a node whose key compares equal to one
* already present (no duplicates in the underlying tree) -- two candidates
* that happen to share a vruntime would otherwise cause one of them to be
* dropped from the queue entirely rather than merely deprioritised.
*/
typedef struct PrioQNode {
RbtNode node;
uint64 vruntime;
uint32 id;
} PrioQNode;
typedef struct PrioQueue {
RbTree tree;
} PrioQueue;
void prioqInit(PrioQueue* q);
/* (Re)inserts n into the queue, keyed by (vruntime, id). n's storage is
owned by the caller and may be reused across unrelated queue lifetimes --
every field the tree needs is reset unconditionally by the underlying
rbtInsert. id must be unique among nodes simultaneously in the same
queue. */
void prioqInsert(PrioQueue* q, PrioQNode* n, uint64 vruntime, uint32 id);
/* Returns the minimum-vruntime node without removing it, or nullptr if the
queue is empty. */
PrioQNode* prioqPeekMin(const PrioQueue* q);
/* Removes and returns the minimum-vruntime node, or nullptr if the queue is
empty. */
PrioQNode* prioqPopMin(PrioQueue* q);
/* Removes an arbitrary node that is currently a member of q. */
void prioqRemove(PrioQueue* q, PrioQNode* n);
usize prioqCount(const PrioQueue* q);
bool prioqIsEmpty(const PrioQueue* q);
#endif
+63
View File
@@ -0,0 +1,63 @@
#ifndef FROSTBYTE_RBT_H
#define FROSTBYTE_RBT_H
#include "types.h"
/*
* Rbt: an intrusive red-black tree, Linux-kernel style. Embed an RbtNode in
* your own struct (next to a key field) and use rbtEntry() (mirrors
* listEntry from list.h) to recover the containing struct. No allocation,
* no element copying -- the tree only ever relinks the nodes you give it.
*
* Ordering is supplied by the caller as plain function pointers rather than
* baked into the node, so the same node type can be indexed by different
* keys in different trees (compare two full nodes for rbtInsert; compare a
* raw key against a node for rbtFind, same convention as bsearch).
*/
typedef struct RbtNode {
struct RbtNode* left;
struct RbtNode* right;
struct RbtNode* parent;
bool red;
} RbtNode;
#define rbtEntry(ptr, type, member) \
((type*)((uint8*)(ptr) - __builtin_offsetof(type, member)))
/* Returns <0, 0, >0 as a compares below/equal/above b. Equal means "already
present" -- rbtInsert rejects the new node in that case (no duplicates;
encode a tiebreaker into the comparison if you need a multiset). */
typedef int (*RbtCompareFn)(const RbtNode* a, const RbtNode* b);
/* Same convention as bsearch: <0 if key sorts before node, >0 if after. */
typedef int (*RbtLookupFn)(const void* key, const RbtNode* node);
typedef struct RbTree {
RbtNode* root;
usize count;
} RbTree;
void rbtInit(RbTree* t);
/* Links node into the tree according to compare. Returns false (node left
untouched) if a node comparing equal is already present. */
bool rbtInsert(RbTree* t, RbtNode* node, RbtCompareFn compare);
/* Unlinks node from the tree. node must currently be a member of t. */
void rbtRemove(RbTree* t, RbtNode* node);
RbtNode* rbtFind(const RbTree* t, const void* key, RbtLookupFn lookup);
RbtNode* rbtFirst(const RbTree* t); /* leftmost (minimum) */
RbtNode* rbtLast(const RbTree* t); /* rightmost (maximum) */
/* In-order successor/predecessor of node. nullptr past either end. Walking
rbtFirst()..rbtNext()..nullptr visits every node in ascending order. */
RbtNode* rbtNext(const RbtNode* node);
RbtNode* rbtPrev(const RbtNode* node);
usize rbtCount(const RbTree* t);
bool rbtIsEmpty(const RbTree* t);
#endif
+39
View File
@@ -0,0 +1,39 @@
#ifndef FROSTBYTE_RING_H
#define FROSTBYTE_RING_H
#include "types.h"
/*
* RingBuffer: a fixed-capacity byte circular buffer over a caller-supplied
* backing array. No heap dependency, so it is safe to use for early
* interrupt-driven queues (keyboard scancodes, serial RX) that need to
* exist before the kernel heap is initialised.
*/
typedef struct RingBuffer {
uint8* data;
usize capacity;
usize head; /* next byte to read */
usize count; /* bytes currently queued */
} RingBuffer;
void ringInit(RingBuffer* r, uint8* backingBuffer, usize capacity);
void ringClear(RingBuffer* r);
bool ringIsEmpty(const RingBuffer* r);
bool ringIsFull(const RingBuffer* r);
usize ringCount(const RingBuffer* r);
usize ringCapacity(const RingBuffer* r);
usize ringFreeSpace(const RingBuffer* r);
/* Single-byte push/pop. Push returns false (buffer unchanged) when full;
pop returns false (out untouched) when empty. */
bool ringPush(RingBuffer* r, uint8 byte);
bool ringPop(RingBuffer* r, uint8* out);
/* Bulk transfer. Writes/reads as many bytes as fit/are available (may be
fewer than requested) and returns the count actually transferred. */
usize ringWrite(RingBuffer* r, const uint8* data, usize len);
usize ringRead(RingBuffer* r, uint8* data, usize len);
#endif
+65
View File
@@ -0,0 +1,65 @@
#ifndef FROSTBYTE_RTC_H
#define FROSTBYTE_RTC_H
#include "types.h"
/*
* CMOS real-time clock driver (MC146818-compatible, ports 0x70/0x71) --
* the first source of real wall-clock date/time this kernel has ever had.
* Every other notion of "time" in frostbyte (pit.h's tick counter,
* threadSleep, userAlarm) is relative to boot, not to the calendar; this
* is the piece that lets a process ask "what day is it" at all.
*
* Honest limitation, same spirit as syscall.c's own documented fork()-less
* design: the CMOS century register's index is not standardized (it is
* only discoverable via the ACPI FADT table, which this kernel does not
* parse), so rtcReadDateTime hardcodes the century to 20 -- correct for
* any date from 2000 through 2099, which covers every real boot this
* kernel will ever see, but not a documented general-purpose solution.
*/
typedef struct RtcDateTime {
int64 year; /* full year, e.g. 2026 */
int64 month; /* 1-12 */
int64 day; /* 1-31 */
int64 hour; /* 0-23, already normalized out of 12-hour/PM if needed */
int64 minute; /* 0-59 */
int64 second; /* 0-59 */
} RtcDateTime;
/* Converts a single CMOS BCD byte (e.g. 0x59) to its binary value (59).
Exposed on its own, like ata.c's ataModelString, because it is a real
piece of the conversion pipeline worth testing in isolation from any
hardware access. */
uint8 rtcBcdToBinary(uint8 bcd);
/* Reads the current date/time off the CMOS RTC. Polls Status Register A's
Update-In-Progress bit (bounded, like ata.c's ataPollTimeout) before
each of two back-to-back raw reads and only accepts the result once both
raw reads agree byte-for-byte, which rules out a read torn mid-tick by
the RTC's own once-a-second update. Returns false only if the two raw
reads never agreed within the bounded retry budget (never hangs). */
bool rtcReadDateTime(RtcDateTime* out);
/* Civil calendar date/time -> seconds since the Unix epoch
(1970-01-01T00:00:00Z), via Howard Hinnant's days_from_civil algorithm
(proleptic Gregorian, correct for any year the RtcDateTime fields above
can represent). Pure arithmetic, no hardware access -- deterministic and
testable without a real clock. */
int64 rtcEpochSeconds(const RtcDateTime* dt);
/* rtcReadDateTime + rtcEpochSeconds in one call, the usual entry point for
anything that just wants "now" as a plain integer (sysTime, syscall.c).
Returns 0 if the underlying read failed. */
int64 rtcNow(void);
/* Seconds since the Unix epoch -> civil calendar date/time, the exact
inverse of rtcEpochSeconds above (Howard Hinnant's civil_from_days,
proleptic Gregorian, same algorithm family as daysFromCivil in rtc.c).
Correct for any epochSeconds value representable in int64, including
negative ones (dates before 1970). Pure arithmetic, no hardware access
-- the piece sfs.h's on-disk mtime/ctime (raw epoch integers) needs to
become a human-readable date, e.g. for a shell `ls -l`. */
void rtcCivilFromEpoch(int64 epochSeconds, RtcDateTime* out);
#endif
+706 -2
View File
@@ -3,13 +3,78 @@
#include "types.h" #include "types.h"
#include "vfs.h" #include "vfs.h"
#include "prioq.h"
#include "isr.h"
#include "signalnum.h"
#include "mmap.h"
/* Default scheduling weight (xv6/CFS-style "nice" niceWeight -- higher runs
more often, not longer). threadSetPriority scales relative to this. */
#define schedDefaultWeight 1024u
/* Returned by threadCreate/threadCreateUser when the thread table is full
(see threadCapacity) -- never a valid thread id, since id 0 is always the
permanent idle thread. Callers must check for this before using the
result to index into anything thread-related. */
#define threadInvalidId 0xFFFFFFFFu
/* Capacity of Thread.name, including the terminator. Large enough for every
name this kernel currently produces (kernel thread literals, "/bin/..."
paths passed to threadCreateUser by spawnUser/doSpawn) with room to
spare; longer names are truncated, never overflowed. */
#define threadNameMax 32
/* Capacity of Thread.env below: envMaxVars is how many "NAME=VALUE" slots
a process may hold at once, envEntryMax is the max size in bytes of a
single slot INCLUDING both the '=' separator and the nul terminator
(so "NAME=VALUE\0" must fit in envEntryMax bytes). Generous enough for
anything this kernel's own shell/demos actually set (see user/envprint.c)
with room to spare, the same "small fixed table, not a growable heap
allocation" choice Thread.cwd already made for a single path. */
#define envMaxVars 8
#define envEntryMax 64
/* One bit per signal number in Thread.sigPending/sigBlocked, and the size
of Thread.sigHandlers[] below -- see signal.h for the actual signal
subsystem this supports. Defined here, not signal.h, purely to break the
include cycle (signal.h includes sched.h for the Thread type). */
#define sigMaxSignals 32
/* Depth of the per-thread nested-handler context stack (Thread.sigSavedRegs/
sigSavedBlocked below) -- how many signal handlers may genuinely interrupt
each other (handler A running, a second signal arrives and is not blocked
by A's own sa_mask, so handler B runs nested inside A, then A resumes once
B returns) before sigConsumePending (signal.c) refuses to deliver a new
one and simply leaves it pending. A real kernel bounds this too (rlimit
on the signal-frame stack it pushes into user memory); this one has no
user-memory signal frame to overflow at all (see signal.h's file comment
-- the saved context lives in the Thread struct itself), so the bound
exists purely to keep that struct's size fixed and to fail a runaway
signal-storm safely (further deliveries just wait) instead of unboundedly
growing kernel memory per thread. 4 is generous headroom for anything
this kernel's own handlers actually do (none nest more than one deep
today -- see user/nesttarget.c) without being large enough to make
Thread noticeably bigger than sigMaxSignals-worth of handlers already
makes it. */
#define sigMaxNestDepth 4
typedef enum { typedef enum {
threadUnused = 0, threadUnused = 0,
threadRunnable = 1, threadRunnable = 1,
threadRunning = 2, threadRunning = 2,
threadSleeping = 3, threadSleeping = 3,
threadDead = 4 threadDead = 4,
threadBlocked = 5,
/* Job-control stop (signal.h's SIGSTOP/SIGTSTP): excluded from
schedule()'s ready-queue rebuild exactly like Sleeping/Blocked
already are (only threadRunnable is ever inserted), so a stopped
thread simply never gets picked until threadContinue moves it back
to threadRunnable. Distinct from threadBlocked because it is NOT
waiting on a wait-channel condition -- schedWakeChannel must never
treat it as a match (see schedWakeChannel), only threadContinue
(an explicit SIGCONT, not "the resource became available") ever
moves a thread out of this state. */
threadStopped = 6
} ThreadState; } ThreadState;
typedef void (*ThreadEntry)(void); typedef void (*ThreadEntry)(void);
@@ -19,24 +84,663 @@ typedef struct Thread {
uint64 stackBase; uint64 stackBase;
uint64 kernelStackTop; uint64 kernelStackTop;
uint64 sleepUntil; uint64 sleepUntil;
/* Absolute pitTicks() deadline for a pending alarm(2)-style SIGALRM
(0 == no alarm pending), set by threadSetAlarm and consulted once
per timer tick by schedule() -- the exact same shape as sleepUntil
above, checked in the exact same loop, except expiry raises a real
signal (sigRaise, signal.h) against the owning thread instead of
just flipping its own ThreadState back to Runnable. Deliberately a
plain deadline, not a wait-channel/ThreadState change: unlike
threadSleep, a thread with a pending alarm keeps running (or
sleeping, or blocked on something unrelated) exactly as it already
was right up until the deadline, exactly like real alarm()'s timer
ticking down in the background regardless of what the process is
doing. */
uint64 alarmAt;
/* poll.h's sysPoll (syscall.c) uses the same restart-the-syscall park
pattern sysRead/sysWrite/sysWait already use, but a poll call can be
woken by any one of several unrelated fds, not one -- so instead of
parking on a single fd's own wait channel (vfsBlockingChannel),
sysPoll blocks every waiting thread on ONE shared channel
(schedPollChannel below) and simply re-checks its whole fd list from
scratch on every wake, spurious or not, exactly like the retry loop
every OTHER restartable syscall already has. pollActive marks "this
thread is mid a not-yet-satisfied sysPoll call" (so a retry entry
can tell itself apart from a genuinely fresh call); pollDeadline is
the absolute pitTicks() deadline schedule()'s own expiry scan forces
a wake at if no fd ever becomes ready first (0 == wait forever,
meaningful only while pollActive is true -- the same "0 reserved"
convention alarmAt above already uses, but here 0 is a real,
legal, requested state (a timeout of -1) rather than "nothing
pending"). */
bool pollActive;
uint64 pollDeadline;
uint64 brk; uint64 brk;
uint64 vruntime;
uint64 exitCode;
uint32 weight;
uint32 id; uint32 id;
/* Id of whichever thread was actually running (currentIndex, sched.c)
at the moment this thread was created -- threadCreate/
threadCreateUser both set this unconditionally, so it is always
meaningful, not just for real userspace parent/child pairs: a
boot-time spawnUser (kmain.c) call happens before schedStart() ever
runs, so every process kmain.c spawns directly gets the permanent
idle thread (id 0) as its parentId, which is deliberately never a
valid sigRaise target (see signal.c's targetId==0 rejection) --
only a RUNTIME sysSpawn (syscall.c's doSpawn, called from inside a
live thread's own syscall) ever produces a parentId that is itself
a live, signalable process. Consulted by threadExit/threadKill
below to deliver a real SIGCHLD to whoever spawned this thread the
instant it actually dies, matching real wait()/SIGCHLD semantics --
NOT the same thing as "whoever later calls threadJoin/sysWait on
this id", which can be anyone that knows the pid, not just the
spawner (this kernel's honest fork()-less design already documents
that distinction, see doSpawn's own file comment in syscall.c). */
uint32 parentId;
/* Distinguishes THIS specific occupant of slot `id` from any other
thread that has occupied (or will ever occupy) the same numeric slot
before or after -- the exact same "handle number alone is not enough
once a slot is recycled" idea pipe.c's own Pipe.incarnation already
solves for pipe handles (see pipeIncarnation's own doc comment,
pipe.h). Assigned once, at creation time, from a single monotonic
counter (sched.c's nextThreadGeneration, never reused, mirroring
pipe.c's nextPipeIncarnation exactly) -- threadReap/threadDiscard's
memset resets this back to 0 when the slot is freed, and the next
threadCreate/threadCreateUser to land on that slot gets a brand new,
never-before-seen value, so 0 here always means "this exact slot is
currently unoccupied," never a live thread's own generation. Exists
purely so pidfd.h's pidfdOpen/pidfdTarget can tell "the same thread I
opened a handle on earlier" apart from "a different, unrelated thread
that has since been handed the same slot number" -- allocSlot's own
doc comment (sched.c) documents the real, previously-unguarded race
this closes for any caller that opts into pidfd.h instead of a raw
pid (kill/wait/sigSend/setpgid/nice below all still take a raw pid,
completely unchanged, exactly as that same comment explains why). */
uint32 generation;
/* Process group id (POSIX pgid): the scope real job control and real
kill(2)/sigSend's pid<0 convention actually target, not a single
thread (signalnum.h's sigInt/sigTstp arriving from keyboard.c's real
Ctrl-C/Ctrl-Z tracking are the live proof -- see keyboard.c's
keyboardHandleScancode and signal.c's sigRaiseGroup). Set once, at
creation time, by threadCreate/threadCreateUser's own initialPgid
helper (sched.c): a thread created while the idle thread (id 0) is
"current" -- i.e. one of kmain.c's boot-time spawnUser/threadCreate
calls, the only time that can ever be true, see parentId's own
comment just above -- becomes its own new group leader (pgid == its
own id), exactly like a real init process placing each service it
starts into its own job; a thread created by a genuinely running
parent (any runtime sysSpawn, syscall.c's doSpawn) instead INHERITS
that parent's pgid, matching real fork()'s own default (a child
stays in its parent's process group until something explicitly
calls setpgid). threadSetPgid (below) is the only thing that ever
changes it again after creation -- the sysSetpgid syscall's
userspace-facing half of real setpgid(2). */
uint32 pgid;
/* Real POSIX credential (uid(2)/setuid(2)): the identity vfs.c's
vfsPermitted consults, alongside a file's own owner uid and mode, to
decide owner-vs-other access at exactly the two enforcement choke
points fs.h's fsModeOwner and fsModeOther bits already gate
(resolveOpen's write check, loadImageByPath/jobctlSpawn's exec check) -- see
vfsPermitted's own doc comment for the full owner/other algorithm
(deliberately no uid-0-bypasses-everything superuser rule -- see that
same comment for why). Same creation-time default/copy split as pgid
immediately above (sched.c's new initialUid, mirroring initialPgid's
own "is the idle thread currently running" test): a boot-time thread
(kmain.c's spawnUser/threadCreate calls, idle currently "current")
starts as uid 0 ("root" in name only here, not in VFS privilege --
see fs.h's fsModeOwner* doc comment), exactly like a real init
process; a genuine runtime child (syscall.c's doSpawn) instead
INHERITS its parent's uid, matching real fork()'s own default before
any setuid ever runs. currentThreadSetUid (below) is the only thing
that ever changes it again after creation -- and, matching real
setuid(2)'s own privilege rule, only ever lets uid 0 become anyone;
any other uid may only "become" itself (a no-op), so dropping root is
real and permanent for the lifetime of this thread, exactly like a
real daemon that setuid()s away from root at startup and can never
regain it. */
uint32 uid;
/* Real POSIX group credential (getgid(2)/setgid(2)): the group identity
vfs.c's vfsPermitted consults, alongside a file's own group gid, as
the MIDDLE fallback between the owner and other checks fs.h's
fsModeGroup* bits gate. Same creation-time default/copy split as uid
immediately above (sched.c's new initialGid, mirroring initialUid's
own shape exactly): a boot-time thread starts at gid 0, a genuine
runtime child (syscall.c's doSpawn) INHERITS its parent's gid.
currentThreadSetGid (below) is the only thing that ever changes it
again -- unlike currentThreadSetUid, the privilege gate for THIS
call is the calling thread's own uid (currentThreadUid() == 0), not
its gid, matching real setgid(2): a privileged (root) PROCESS may
setgid to any group; an unprivileged one may only pass its own
current gid back in (a no-op), so dropping to a group is real and
permanent for the lifetime of a non-root thread, same shape as uid's
own one-way drop. */
uint32 gid;
/* Real per-process working directory (POSIX chdir(2)/getcwd(2)): always
a fully resolved absolute path (vfs.h's vfsChdir resolves a relative
argument against the CALLING thread's own cwd before ever storing
here, so this field itself never needs re-resolving against
anything). Set once, at creation time, by threadCreate/
threadCreateUser's own initialCwd helper (sched.c) -- a thread
created while the idle thread (id 0) is "current" (kmain.c's
boot-time spawns, see parentId's own comment above) starts at "/",
exactly like a real init process; a thread created by a genuinely
running parent (any runtime sysSpawn, syscall.c's doSpawn) instead
INHERITS a byte-for-byte COPY of that parent's cwd, matching real
fork()'s own copy-not-share semantics -- the two are independent
buffers from that point on, so a child's later chdir() can never
reach back and change its parent's, and vice versa. Before this
field existed, the only cwd concept anywhere in this kernel was a
single static local in shell.c shared by nothing else -- a spawned
user process calling open()/mkdir()/etc with a relative path had
never actually worked, since vfs.c's lookupNode/sfsLookup always
resolved relative to the filesystem root, never to any notion of
"current directory" (see vfs.c's vfsResolvePath, the only thing that
reads this field). */
char cwd[fsPathMax];
/* Real per-process environment variables (POSIX getenv(3)/setenv(3)):
up to envMaxVars "NAME=VALUE\0" strings, each at most envEntryMax-1
bytes, plus envCount tracking how many slots are actually in use.
Same creation-time default/copy split as Thread.cwd immediately
above (sched.c's new initialEnv, mirroring initialCwd's own shape):
a boot-time thread (idle is currently running) starts with an EMPTY
table, like a real init process before anything ever calls
setenv(); a genuine runtime child (syscall.c's doSpawn) instead
inherits a byte-for-byte COPY of its parent's table, so a child's
later setenv() can never reach back and change its parent's, and
vice versa -- the exact same independent-buffer guarantee cwd
already gives. */
char env[envMaxVars][envEntryMax];
int envCount;
ThreadState state; ThreadState state;
const char* name; /* Set once, permanently, the first time schedule() ever actually
switches execution to this thread. Distinguishes "freshly created,
never scheduled even once" (state threadRunnable, everRan false) from
"was running and got preempted, still Runnable" (everRan true) -- the
two look identical by state alone, but only the former is safe for
threadDiscard to tear down (see threadDiscard). */
bool everRan;
/* Fixed buffer, not a pointer: threadCreateUser/doSpawn (syscall.c) may
be called with a name that lives in a caller's stack-local buffer
(a path just copied in from userspace), which does not outlive the
call. thread->name must remain valid for the thread's entire life
(shell.c's `ps` listing and the exit log both read it long after
creation), so the bytes are always copied in, never aliased -- see
threadSetName/copyThreadName in sched.c. */
char name[threadNameMax];
const void* waitChannel;
/* Base virtual address of this thread's elfSlotSize-sized user-memory
slot (image + stack + brk-heap all live within
[userSlotBase, userSlotBase+elfSlotSize), see elf.h) -- 0 for kernel
threads (threadCreate), which own no user pages to give back.
Set from the entry point by threadCreateUser at spawn time and
updated by exec() (syscall.c doExec) when a thread's image moves to
a different slot. threadReap/threadDiscard pass this to
pagingUnmapUserRange so a dead or rolled-back process's physical
frames are actually reclaimed instead of leaking forever. */
uint64 userSlotBase;
PrioQNode schedNode;
FileDescriptor fds[maxOpenFiles]; FileDescriptor fds[maxOpenFiles];
/* mmap.h's own per-thread mapping table -- real demand-paged mmap(2)/
munmap(2), resolved lazily by isr.c's #PF handler through
mmapFaultHandler rather than eagerly mapped like pagingMapUserRange's
image/stack/brk pages. Lives inside the same [userSlotBase,
userSlotBase+elfSlotSize) slot as everything else this thread owns
(see mmap.h's own mmapReserve doc comment for exactly which sub-range),
so threadReap/threadDiscard's existing whole-slot
pagingUnmapUserRange already reclaims every frame a mapping here ever
faulted in with zero changes of their own -- this array only ever
needs its OWN bookkeeping cleared (mmapReleaseAll), which the
memset() both of those already do to the whole struct handles for
free. */
Vma vmas[maxVmasPerThread];
/* Signal state (signal.h/signal.c). Explicitly zeroed by sigResetState
from threadCreate/threadCreateUser below, the same way every other
field above is explicitly reset there too -- threadReap/threadDiscard
already memset() this whole struct before any slot is ever handed
back to allocSlot, so this is belt-and-suspenders, not a fix for a
leak that could otherwise happen (unlike keyboard.c's console-reader
table or pipe.c's refcounts, which live OUTSIDE Thread and genuinely
needed their own explicit reclaim -- see reclaimOpenFds). */
uint64 sigPending;
uint64 sigBlocked;
uint64 sigHandlers[sigMaxSignals];
/* Per-signal sa_mask (signal.h/signal.c): the ADDITIONAL signals to
block, alongside the one being delivered, for as long as
sigHandlers[signum]'s handler is running -- set by sigSetHandler,
consulted only by sigConsumePending's custom-handler branch. Index is
the signal being handled, not the signals it blocks (mirrors
sigHandlers[] being indexed by signal number, not by handler). */
uint64 sigHandlerMasks[sigMaxSignals];
/* Per-signal sa_flags (signal.h/signal.c) -- today only sigFlagRestart
(signalnum.h) is a defined bit, stored here exactly like
sigHandlerMasks is: set by sigSetHandler, indexed by the signal being
handled, consulted only by sigConsumePending when it is about to
decide whether an interrupted retryable syscall (see
sigInRetryableSyscall below) resumes transparently or returns
errInterrupted. */
uint64 sigHandlerFlags[sigMaxSignals];
/* SigInfo (signalnum.h) attached to whichever instance of signum is
CURRENTLY pending (index == signal number, same convention as
sigHandlers/sigHandlerMasks/sigHandlerFlags above) -- set by
sigRaiseInfo (signal.c) the moment sigPending's bit for it is set,
read (and copied into the shared user-visible page) by
sigConsumePending only if that signal's handler was registered with
sigFlagSigInfo. Like sigPending itself, this does not queue: raising
the same signum again before it is ever delivered overwrites this
with the newer detail, matching real Unix's own non-queuing standard
(non-realtime) signals, where only the most recent instance's info is
ever actually observable once more than one arrives uncollected. */
SigInfo sigPendingInfo[sigMaxSignals];
/* True exactly while this thread is parked mid the restart-syscall
pattern sysRead/sysWrite/sysWait/sysPoll (syscall.c) use to block: rip
has already been rewound by the interrupted `int $0x80`'s 2 bytes so
re-entering ring 3 re-executes the syscall from scratch. Set by
syscall.c immediately before each of those four calls parks and
reschedules; cleared unconditionally at the top of every
syscallDispatch call (a fresh dispatch, whether this is the retry
itself or a completely unrelated syscall, is never "still blocked" by
definition) AND by sigConsumePending the moment it actually acts on a
pending signal for this thread, win or lose -- a signal delivered
while this is false (ordinary running code, a sleep, or a nested
signal arriving inside an already-running handler) never touches
rip/rax at all, exactly as if sigFlagRestart's whole mechanism did
not exist, since there is no interrupted syscall retry to redirect. */
bool sigInRetryableSyscall;
/* How many handler contexts deep this thread currently is (0 = running
ordinary code, not inside any handler) -- sigSavedRegs[0..depth-1]/
sigSavedBlocked[0..depth-1] are the LIFO stack of interrupted contexts
sigOnSigReturn pops from, one per still-active handler, exactly like a
real call stack of signal frames. sigConsumePending refuses to push a
new one once this reaches sigMaxNestDepth. */
uint32 sigHandlerDepth;
uint64 sigSavedBlocked[sigMaxNestDepth];
Registers sigSavedRegs[sigMaxNestDepth];
} Thread; } Thread;
void schedInit(void); void schedInit(void);
/* Allocates a slot from the fixed-size thread table (reusing the lowest
free slot left behind by threadReap before ever growing the high-water
mark) and initializes a new thread in it. Returns threadInvalidId,
touching nothing else, if the table is completely full (threadCapacity
threads already live) -- callers MUST check for this instead of assuming
the id is always valid. */
uint32 threadCreate(const char* name, ThreadEntry entry); uint32 threadCreate(const char* name, ThreadEntry entry);
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop); uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
uint64 schedule(uint64 currentRsp); uint64 schedule(uint64 currentRsp);
void schedStart(void); void schedStart(void);
/* Total number of thread-table slots (used and free). A fixed compile-time
constant, exposed so callers (and tests) can reason about exhaustion
without hardcoding the internal capacity. */
uint32 threadCapacity(void);
/* Releases thread `id`'s resources (kernel stack) and resets its slot back
to threadUnused so a later threadCreate/threadCreateUser can reuse it.
Only valid on a thread whose state is threadDead (i.e. it has already
called threadExit) -- reaping anything else (including the permanent
idle thread, a still-live thread, or an id that was never created / was
already reaped) is rejected and returns false with no effect. */
bool threadReap(uint32 id);
/* Tears down a thread that was created but has never actually been
scheduled (state threadRunnable and everRan false) -- e.g. rolling back a
spawn that failed a later setup step after threadCreate/threadCreateUser
already succeeded. Unlike threadReap this does not require the thread to
have exited: it requires the opposite, that it has not yet run even
once, since anything that has run may hold state (a partially unwound
stack, in-flight I/O) that is not safe to discard out from under it.
Returns false, changing nothing, if the thread has ever run, is not
currently Runnable (already sleeping/blocked/dead/unused), or id is out
of range. */
bool threadDiscard(uint32 id);
/* Blocks the calling thread (parking with schedBlockCurrent + halt(), the
same pattern pipeReadBlocking/pipeWriteBlocking use) until thread `id`
has called threadExit, then returns its exit code and reaps it (frees
the slot for reuse) in one step -- the same "wait for a child, get its
status, reclaim it" contract as a real wait()/join(). Returns -1
immediately, without blocking, for an invalid id (0, out of range, or a
slot that is not currently a live thread) rather than parking forever on
something that will never wake it. Joining the same id a second time (or
from two different callers) is not supported: the first joiner reaps the
slot, so a second joiner sees "no such thread" and gets -1. */
int64 threadJoin(uint32 id);
/* Non-blocking scan for waitpid(-1)-style "reap whichever child happens to
already be dead, without caring which specific pid" (sysWait's waitAnyPid
case, syscall.c/syscall.h; user/waitanytest.c is the live demo). Unlike
threadJoin above this never calls halt() itself -- it can be (and is)
invoked from inside the interrupt-context syscall handler, where parking
directly would wedge the whole machine (see sysWait's own comment on why
its specific-pid path does its own retry-syscall dance rather than just
calling threadJoin unconditionally); the caller is responsible for
actually blocking when this returns waitScanNoneDead.
Scans every live slot for one whose parentId is `parentId`:
- none exist at all (dead or alive) -> waitScanNoChildren, the real
waitpid(-1) ECHILD case -- there is nothing to ever wait for, so the
caller must not block.
- at least one exists but none are threadDead yet -> waitScanNoneDead.
- one is already threadDead -> waitScanFoundDead, with its own id and
exit code written to *outId and *outCode; it is already fully reaped
(threadReap already ran) by the time this returns, the same contract
threadJoin(id) has for an already-dead target. Picks whichever
matching dead slot has the lowest index if more than one child
happens to be dead at once -- a real waitpid(-1) makes no promise
about which one it returns in that case either. */
typedef enum { waitScanNoChildren, waitScanNoneDead, waitScanFoundDead } WaitScanResult;
WaitScanResult threadJoinAnyChild(uint32 parentId, uint32* outId, int64* outCode);
/* Terminates the CALLING thread with the given exit code (sysExit's own
underlying implementation, syscall.c, and isr.c's uncaught-fatal-signal
path). Also raises a real sigChld (signal.h/signal.c) against
Thread.parentId above -- asynchronous notification, on top of (not a
replacement for) threadJoin/sysWait's synchronous "block until this pid
is Dead" contract: a parent with a sigChld handler installed and
genuinely blocked elsewhere (see user/sigchldtest.c, which spawns
user/sigchldchild.c) is woken through the same early-wake path
threadWakeForSignal already gives any other signal, rather than only
finding out the next time it happens to call sysWait. Harmless no-op on
the sigRaise side if parentId is 0 (the idle thread, boot-time spawns'
universal parent, see parentId's own comment) or already dead/unused
(an orphan whose spawner is itself gone) -- sigRaise's own target
validity check (signal.c) is what makes that safe, not anything special
here. Also wakes the parent's OWN slot as a wait channel (on top of, not
instead of, this slot's own channel below) -- the parking spot sysWait's
waitAnyPid path (syscall.c) uses via schedBlockCurrent(currentThread()),
so a parent genuinely blocked in userWaitAny wakes the instant ANY of
its children exits, not just one it named specifically. */
void threadExit(uint64 code); void threadExit(uint64 code);
/* Exit code threadKill assigns its victim -- named after SIGKILL (9), the
nearest real-world equivalent, though this kernel has no signal delivery
at all: threadKill is a hard, unconditional termination, not something a
target thread can catch, mask, or clean up after (see threadKill). */
#define threadKilledExitCode 9u
/* Unconditionally terminates thread `id` from OUTSIDE it -- the only way to
ever get back a process that is genuinely stuck (e.g. blocked forever in
a syscall-level pipe read with no writer ever coming, now that
sysRead/sysWrite can really block, see pipe.h/vfs.h). Works on a thread
in any of Runnable/Sleeping/Blocked: its state is simply set straight to
threadDead with exitCode threadKilledExitCode, exactly as if it had
called threadExit(threadKilledExitCode) itself, and whoever is (or later
becomes) threadJoin()ing it wakes up and reaps it normally -- there is no
"half-dead" state and no cleanup callback, since a mid-syscall thread has
no user-mode cleanup path to run anyway. Returns false, changing nothing,
for id 0 (idle), an out-of-range id, an already-Dead/never-created slot,
or (implicitly, since only the CPU's one currently-executing thread is
ever threadRunning) the caller's own id -- a syscall handler always runs
as its OWN thread, which is Running, not Runnable/Sleeping/Blocked, at
the moment it would try to kill itself, so self-kill is naturally
rejected without a special-case check. Real termination (sysExit) is
still how a thread should ever end itself. Also raises sigChld against
the victim's parentId, exactly like threadExit does -- a killed child
is still a dead child as far as its parent's SIGCHLD is concerned, the
same way real kill(2)ing a child still generates SIGCHLD to its real
parent. Also wakes the victim's parent's OWN slot as a wait channel,
exactly like threadExit does -- a parent parked in userWaitAny does not
care whether a child exited on its own or was killed, same as real
waitpid(-1) does not distinguish the two either. */
bool threadKill(uint32 id);
/* Job control (signal.h): unconditionally moves thread `id` -- Runnable,
Running (including the caller itself, unlike threadKill which naturally
excludes self-targeting), Sleeping, or Blocked -- into threadStopped.
Idempotent if `id` is already Stopped (matches real kill(pid, SIGSTOP)
succeeding again on an already-stopped process). Safe on a
Sleeping/Blocked thread for the same reason threadKill already is: every
such thread is parked mid-retry-loop right after its own halt() (see
pipeReadBlocking/pipeWriteBlocking, or the restart-syscall pattern
syscall.c's sysRead/sysWrite/sysWait use), so simply not being picked by
schedule() again until threadContinue moves it back is enough -- no
wait-channel state is lost, since that retry loop re-checks its own
condition from scratch every time it runs at all. Returns false,
changing nothing, for id 0 (idle -- stopping it would wedge the whole
machine the instant nothing else was runnable), an out-of-range id, or
an already-Dead/never-created slot. */
bool threadStop(uint32 id);
/* Job control (signal.h): moves thread `id` from threadStopped back to
threadRunnable. Returns false, changing nothing, if `id` is out of
range or not currently Stopped (matches real SIGCONT delivered to a
process that was never stopped -- a harmless no-op at the process-state
level; signal.c's sigRaise still delivers the signal itself/runs a
handler regardless of this return value). */
bool threadContinue(uint32 id);
/* Real Unix's interruptible sleep/EINTR: forces thread `id` from
threadSleeping or threadBlocked straight to threadRunnable, early --
before its sleep timer expires or its wait-channel condition is ever
satisfied -- so the NEXT schedule() eventually resumes it, letting
isr.c's deliverPendingSignals (which only ever runs at a return-to-ring3
point) actually reach a thread that would otherwise never naturally
produce one. Called only by signal.c's sigRaise, and only for a signal it
has already determined would genuinely be delivered (not blocked, not
ignored) if this thread resumed right now -- callers must not use this to
wake a thread for an unrelated reason. Deliberately excludes
threadStopped (only threadContinue, an explicit SIGCONT, may ever move a
Stopped thread -- the same asymmetry schedWakeChannel already has, for
the same reason: job control's "stopped means stopped" must survive an
ordinary signal arriving too). Returns false, changing nothing, for an
out-of-range id or any state other than Sleeping/Blocked (Runnable/
Running need no forcing; Stopped/Dead/Unused have nothing to wake, or
must not be woken this way at all). */
bool threadWakeForSignal(uint32 id);
void threadSleep(uint64 milliseconds); void threadSleep(uint64 milliseconds);
void threadSetBrk(uint32 id, uint64 brk); void threadSetBrk(uint32 id, uint64 brk);
uint64 currentThreadSbrk(int64 increment); uint64 currentThreadSbrk(int64 increment);
/* Real alarm(2): schedules a real SIGALRM (signalnum.h's sigAlrm) against
the CALLING thread only, `seconds` real seconds from now (converted to
pitTicks() ticks via pitFrequency(), the same conversion threadSleep
already uses for milliseconds) -- never another thread, exactly like
POSIX alarm() has no target-pid argument at all. `seconds` == 0 cancels
any alarm already pending for this thread instead of scheduling a new
one, matching real alarm(0). Returns the number of whole seconds that
remained on a PREVIOUSLY pending alarm for this thread (rounded up, so a
deadline even one tick in the future still reports at least 1, exactly
like real alarm() never reports "0 remaining" for one that has not
actually fired yet), or 0 if none was pending -- the caller's own
previous alarm is silently replaced (or cleared) either way, matching
real alarm()'s "at most one pending alarm per process" contract. Actual
delivery happens later, from schedule() (sched.c), once pitTicks()
reaches the deadline this call records. */
uint64 threadSetAlarm(uint64 seconds);
/* Weighted-fair scheduling (CFS-lite): every thread starts at
schedDefaultWeight. A thread with weight W accrues virtual runtime
schedDefaultWeight/W times as fast as a default-weight thread each time
it runs, so it is picked from the ready queue proportionally more often
-- not given a longer timeslice, since this scheduler's granularity is
already one timer tick. Returns false (no-op) for the same targets
threadSetPgid already refuses -- the idle thread (id 0), an out-of-range
id, or a slot that is not currently a live thread (Dead or Unused) --
plus a zero weight, which used to be silently ignored (kmain.c-only
caller never triggered it) before this became reachable from userspace
through sysNice (syscall.c). Was a kernel-internal helper only used
once, at boot, for workerA/workerB's own priority demo (kmain.c) --
still is, but ALSO now the mechanism sysNice's syscall.c handler calls,
so its own validation had to catch up to threadSetPgid's, not stay at
the older, weaker "just check threadTotal" bound that let a userspace
nice() call quietly reweight thread 0 (idle) or a slot no thread has
ever occupied. */
bool threadSetPriority(uint32 id, uint32 weight);
/* Returns thread `id`'s current scheduling weight, or 0 (never a real
weight -- threadSetPriority above always rejects weight 0, and every
thread starts at schedDefaultWeight) for an out-of-range id or a slot
that has never been a live thread (Unused) -- same "0 doubles as the
error return, since it is never a valid one" shape threadPgid below
has with threadInvalidId. A Dead-but-not-yet-reaped thread still
reports its last real weight, exactly like threadPgid still reports
its last real pgid for the same state -- see that function's own
comment for why. */
uint32 threadPriority(uint32 id);
uint64 threadVruntime(uint32 id);
/* Returns thread `id`'s process group id (Thread.pgid above), or
threadInvalidId for an out-of-range id or a slot that is not currently a
live thread -- the same "invalid id sentinel doubles as an error return"
convention threadInvalidId already has for allocation failure, safe here
because a real pgid is always some other live thread's own id (never
0xFFFFFFFF, since ids never reach that high, see threadCapacity). */
uint32 threadPgid(uint32 id);
/* Real setpgid(2)'s underlying mechanism: unconditionally sets thread
`id`'s pgid to `pgid`, whatever value the caller resolved "0 means self"/
"0 means use pid itself as the new group id" down to (that resolution is
the SYSCALL layer's job, syscall.c's sysSetpgid -- mirroring how
threadSetBrk/threadSetPriority/threadSetAlarm above already take a fully
resolved value, not a raw syscall argument). Deliberately does not
validate that `pgid` names any other real thread at all: this kernel has
no session concept to check membership against (real setpgid(2) would
reject joining a group outside the caller's own session), so -- like
several other omissions this codebase documents honestly rather than
fakes -- any uint32 is accepted, exactly as if every process were in one
giant session. Returns false, changing nothing, for id 0 (idle -- it has
no job-control identity to reassign) or an out-of-range/Dead/Unused id. */
bool threadSetPgid(uint32 id, uint32 pgid);
/* Real kill(-pgid, SIGKILL)-shaped bulk termination: calls threadKill on
every currently live thread (Runnable/Sleeping/Blocked/Stopped, per
threadKill's own accepted set) whose pgid equals `pgid`, one at a time,
through the ordinary single-target threadKill above -- so its own
per-target SIGCHLD-to-parent notification and wait-channel wakes already
apply per victim exactly as they would for a plain, non-group kill,
rather than this function duplicating any of that. A pgid whose only
member is the CALLER's own currently-Running thread naturally kills
nobody (threadKill excludes Running, i.e. self, by construction, the
same "self-kill via plain kill(2) is not really different" reasoning
userKill's own doc comment already gives) without needing a special
case here. Returns false, killing nobody, if no currently live thread
has this pgid at all -- the real ESRCH case for kill(-pgid, ...) with no
matching group. */
bool threadKillGroup(uint32 pgid);
/* Real getcwd(2)/chdir(2)'s underlying per-thread state (Thread.cwd above):
always the CALLING thread's own directory, never another pid's -- unlike
threadSetPgid, there is no `id` parameter here at all, since POSIX's
chdir() has no cross-process form the way setpgid(2) does for a child
still between fork() and exec(). currentThreadSetCwd does no validation
of its own (mirrors threadSetPgid's own "that is the syscall layer's
job" split) -- vfs.c's vfsChdir is what actually confirms `path` names a
real, already-resolved-to-absolute directory before ever calling this;
a `path` longer than fsPathMax-1 is rejected here (false, unchanged)
since it could never round-trip back out through currentThreadCwd(). */
const char* currentThreadCwd(void);
bool currentThreadSetCwd(const char* path);
/* Real getuid(2)/setuid(2)'s underlying per-thread state (Thread.uid
above): always the CALLING thread's own identity, mirroring
currentThreadCwd's own "no id parameter, no cross-process form" shape
exactly (POSIX setuid() has no cross-process form either -- a process can
only ever change its OWN credential). currentThreadSetUid enforces the
real privilege rule itself (this kernel's syscall layer does no
additional validation of its own, unlike currentThreadSetCwd's split):
uid 0 ("root") may become anyone; any other uid may only pass its own
current value back in (a no-op that still reports success, matching real
setuid(getuid())), and any other target is rejected, state unchanged.
There is deliberately no way back to uid 0 once dropped, short of
already being uid 0 at the time of the call -- the same one-way
privilege-drop shape a real setuid daemon relies on. */
uint32 currentThreadUid(void);
bool currentThreadSetUid(uint32 newUid);
/* Real getgid(2)/setgid(2)'s underlying per-thread state (Thread.gid above)
-- same "no id parameter, no cross-process form" shape as currentThreadUid
immediately above. currentThreadSetGid's privilege gate is the CALLING
thread's own currentThreadUid() (== 0), not its gid -- matching real
setgid(2), whose privilege check is CAP_SETGID (effectively "is this
process root"), never based on the gid being changed itself. A
non-privileged thread may only pass its own current gid back in (a
no-op success, matching real setgid(getgid())); any other target is
rejected, state unchanged. */
uint32 currentThreadGid(void);
bool currentThreadSetGid(uint32 newGid);
/* Real getenv(3)/setenv(3)'s underlying per-thread state (Thread.env
above): always the CALLING thread's own table, mirroring
currentThreadCwd/currentThreadSetCwd's own "no id parameter, no
cross-process form" shape exactly (POSIX setenv() has no cross-process
form either). currentThreadGetEnv returns a pointer straight into the
matching slot's own VALUE half (never a copy) or nullptr if `name` has
no entry -- that pointer stays valid only until the next
currentThreadSetEnv call on the SAME name, which may overwrite the slot
in place. currentThreadSetEnv overwrites an existing entry with a
matching `name` in place, or appends a new one if a slot is still free
under envMaxVars; returns false, table unchanged, if `name`+'='+`value`
would not fit in envEntryMax bytes, or if `name` is new and every slot
is already taken. */
const char* currentThreadGetEnv(const char* name);
bool currentThreadSetEnv(const char* name, const char* value);
/* Real unsetenv(3): removes the calling thread's own entry named `name`,
if any, compacting the table (memmove of every later slot down by one)
so envCount and currentThreadEnvEntry's own index space never develop a
hole -- unlike currentThreadSetEnv's in-place overwrite, this changes
WHICH index every later entry lives at. Returns true if an entry was
actually removed, false (table unchanged) if `name` had no entry --
matching real unsetenv(3), which is a no-op success either way, but the
bool lets a caller (shell.c's `unset` builtin) tell the difference if it
ever wants to. */
bool currentThreadUnsetEnv(const char* name);
/* Iterates the calling thread's own environment table for listing
(shell.c's `env` builtin): currentThreadEnvCount() is how many of the
envMaxVars slots are actually in use; currentThreadEnvEntry(index)
returns the raw "NAME=VALUE" string stored at that slot, or nullptr for
an out-of-range index. */
int currentThreadEnvCount(void);
const char* currentThreadEnvEntry(int index);
/* Copies up to threadNameMax-1 bytes of `name` into thread `id`'s own name
buffer (truncating, always nul-terminated). Used by exec() (syscall.c) so
a process's visible identity (shell `ps`, the exit log) reflects the new
image after it replaces the old one -- the same way a real process's
name changes across execve(). Invalid id is a silent no-op. */
void threadSetName(uint32 id, const char* name);
/* Base of thread `id`'s user-memory slot (see Thread.userSlotBase above),
or 0 for an out-of-range id or a kernel thread that owns no user slot.
Used by exec() (syscall.c) to find the OLD slot to reclaim before it
overwrites its own image with a new one. */
uint64 threadUserSlotBase(uint32 id);
/* Records which elfSlotSize-sized slot thread `id` currently occupies.
Only exec() (syscall.c) needs this directly -- threadCreateUser sets it
itself from the entry point it is given. Invalid id is a silent no-op. */
void threadSetUserSlotBase(uint32 id, uint64 base);
/* Wait-queue primitives ("sleep on a channel", xv6-style). A channel is any
stable address the caller controls (e.g. the address of the resource being
waited on) - it is never dereferenced, only compared for identity.
schedBlockCurrent marks the calling thread as blocked on a channel; it does
NOT itself give up the CPU (this scheduler is timer-preemptive, not
cooperative - callers park with halt() the same way threadSleep callers
already do, see kmain.c). schedWakeChannel moves every thread blocked on a
matching channel back to runnable; it is always safe to call even when no
thread is waiting (no-op) and must be called by whichever side of a
producer/consumer relationship changes the condition the other side is
waiting on. */
void schedBlockCurrent(const void* channel);
void schedWakeChannel(const void* channel);
/* The one wait channel every not-yet-satisfied sysPoll call blocks on (see
Thread.pollActive's own comment above) -- any code path that can make a
previously-not-ready fd become ready (pipe.c's pipeWrite/pipeRead
freeing space, pipeCloseRead/pipeCloseWrite breaking a pipe,
keyboard.c's keyboardHandleScancode enqueueing a keystroke) calls
schedWakeChannel(schedPollChannel()) in addition to whatever
fd-specific channel it already wakes, so every blocked poller gets a
chance to re-check its own fd list. Always returns the same address for
the life of the kernel -- not tied to any particular fd or pipe, unlike
pipeChannel/keyboardReaderChannel. */
const void* schedPollChannel(void);
uint32 threadCount(void); uint32 threadCount(void);
Thread* threadAt(uint32 index); Thread* threadAt(uint32 index);
Thread* currentThread(void); Thread* currentThread(void);
+226
View File
@@ -0,0 +1,226 @@
#ifndef FROSTBYTE_SFS_H
#define FROSTBYTE_SFS_H
#include "types.h"
/*
* SFS ("simple filesystem"): a real, persistent, inode-based filesystem
* living on the ATA block device (ata.h), starting at LBA sfsBaseLba. Unlike
* the VFS tmpfs tree (vfs.c), which is pure kmalloc state that evaporates on
* reboot, SFS state (superblock, bitmaps, inode table, file data) is fully
* on disk: sfsUnmount()+sfsMount() drops every in-memory structure and
* reconstructs it by reading the disk, the same way a real filesystem driver
* would after a remount.
*
* Block size equals the sector size (512 bytes), so every filesystem block
* is exactly one ATA sector transfer. sfsBaseLba (100) and the reserved
* tail (16 sectors) keep this filesystem's LBA range clear of the raw
* sector ranges ataTest.c/disktest.c already poke directly, so the two
* layers never fight over the same bytes even though they share one disk.
*/
#define sfsBlockSize 512
#define sfsBaseLba 100
#define sfsReserveEnd 16
#define sfsMaxNodes 512
#define sfsRootInode 1
#define sfsNameMax 55
#define sfsMaxSymlinkHops 8
#define sfsSymlinkTargetMax 255
#define sfsTypeFree 0
#define sfsTypeFile 1
#define sfsTypeDir 2
#define sfsTypeSymlink 3
#define sfsTypeFifo 4
/* Formats the SFS region from scratch: fresh superblock, zeroed bitmaps and
inode table, empty root directory. Destroys any existing SFS content in
that LBA range. Returns false if no drive is present or the drive is too
small to hold the fixed-size metadata. */
bool sfsFormat(void);
/* Mounts the existing on-disk filesystem, auto-formatting on first use (bad
magic). Loads the inode/block bitmaps into memory; everything else (inode
table, file data, directory entries) is read from disk on demand. */
bool sfsMount(void);
/* Flushes bitmaps/superblock to disk and frees in-memory state. A later
sfsMount() reconstructs everything purely from what is on disk. */
void sfsUnmount(void);
bool sfsMounted(void);
/* Total bytes usable for file data (excludes reserved metadata blocks). */
uint64 sfsCapacityBytes(void);
/* Data blocks not currently allocated to any inode (bitmap free count).
Mainly useful for tests to prove truncate/remove actually gives blocks
back rather than merely detaching them from the inode. */
uint64 sfsFreeDataBlocks(void);
/* path is slash-separated and relative to the SFS root ("" or "/" is the
root itself). Returns the inode number (>=1) or -1 if not found. Note:
"." and ".." are not special-cased (deliberate scope limit: paths are
always resolved from the root down, no relative-to-self hops).
A symlink encountered anywhere along the path - including the final
component - is transparently followed to its target and re-resolved from
the root (real open()/stat() semantics), up to sfsMaxSymlinkHops hops;
beyond that (a cycle, or a chain that is simply too long) resolution fails
with -1 instead of recursing forever. Use sfsLookupNoFollow to inspect a
symlink itself rather than what it points at. */
int32 sfsLookup(const char* path);
/* Same resolution as sfsLookup, except a symlink at the FINAL path component
is returned as itself instead of being followed (the lstat()/readlink()
convention). Symlinks at any earlier component are still followed, since
there is no other way to keep descending through them. */
int32 sfsLookupNoFollow(const char* path);
int32 sfsCreateFile(const char* path);
int32 sfsCreateDir(const char* path);
/* Creates a persistent named-pipe entry at `path`: a real, on-disk directory
entry of type sfsTypeFifo that survives sfsUnmount()+sfsMount() (and a
real reboot, unlike the VFS tmpfs-tree fifos vfsMkfifo already supports
under e.g. "/tmp") exactly the way a real filesystem's mkfifo(2) special
file does. The inode carries no data blocks -- sfsReadAt/sfsWriteAt
already refuse anything that is not sfsTypeFile/Dir/Symlink, so a fifo
inode simply has size 0 forever, matching real Unix (mkfifo persists the
NAME and TYPE of the rendezvous point, never any buffered bytes: those
only ever exist in memory while some process holds the pipe open, and
are gone the moment every process does, reboot or not). vfs.c is
responsible for the actually-live half of a fifo (attaching real
pipe.h byte-queue handles to opens of this path, see vfsOpen) --
sfs.c itself stays entirely ignorant of pipe.h, exactly as it already is
of every other higher-layer concern. Returns the new inode number, or -1
on the same failures as sfsCreateFile/sfsCreateDir (parent missing/not a
writable dir, name already taken, tables full). */
int32 sfsCreateFifo(const char* path);
/* Monotonically increasing counter, bumped exactly once every time
sfsFormat() actually wipes the on-disk filesystem (whether called
directly, e.g. every sfsTest.c test's own clean-slate setup, or
implicitly by sfsMount() the first time it sees a bad magic) -- never
bumped by a plain sfsMount()/sfsUnmount() cycle that finds a valid,
unchanged filesystem, since inode numbers keep meaning the same thing
across that kind of remount. Exists so a higher layer that caches
something keyed by inode number across calls (vfs.c's SFS fifo binding
table, see vfsOpen/vfsMkfifo/vfsUnlink in vfs.c) can tell "this cached
entry still refers to the same on-disk fifo I bound it to" apart from
"the whole filesystem underneath it was wiped and reformatted since,
inode 7 might be a completely different file/dir/fifo/nothing now" --
without sfs.c ever having to know that cache exists. */
uint64 sfsGeneration(void);
/* Creates a symbolic link at `path` whose target is the literal string
`target` - never validated against anything, so a symlink may point at a
path that does not exist yet (or ever), exactly like a real filesystem.
Returns the new inode number, or -1 on failure (parent missing, name
already taken, target too long, or the inode/block tables are full). */
int32 sfsCreateSymlink(const char* path, const char* target);
/* Reads the target string of a symlink inode into buf, null-terminated.
Fails (returns false, buf left untouched) if inodeNum is not a symlink or
if the stored target does not fit in cap bytes including the terminator -
never silently truncates. */
bool sfsReadlink(int32 inodeNum, char* buf, uint64 cap);
/* Creates a second directory entry (newPath) for the same inode an existing
file or symlink resolves to (existingPath is followed through symlinks,
same as open()/stat() - the new name refers to whatever it ultimately
points at, not the symlink itself). Directories cannot be hardlinked
(matches POSIX link(2): would make the directory tree no longer a tree).
Returns the shared inode number, or -1 on failure (existingPath missing,
a directory, newPath's parent missing, or newPath already taken). */
int32 sfsLink(const char* existingPath, const char* newPath);
/* Number of directory entries currently pointing at this inode. 0 for an
invalid/unmounted lookup, otherwise always >=1 for anything sfsLookup can
still reach. */
uint32 sfsLinkCountOf(int32 inodeNum);
/* Removes a file, or a directory only if it is empty. A hardlinked file's
data and inode survive until its link count reaches zero -- only the
named directory entry is guaranteed to disappear immediately. */
bool sfsRemove(const char* path);
uint64 sfsSizeOf(int32 inodeNum);
uint32 sfsTypeOf(int32 inodeNum);
/* Real wall-clock timestamps (rtc.h's rtcNow(), seconds since the Unix
epoch), stored on disk in the inode itself so they survive
sfsUnmount()+sfsMount() like everything else about the inode. mtime is
last content modification (sfsWriteAt, sfsTruncate); ctime is last inode
metadata change (mtime changes are also metadata changes, so every mtime
update bumps ctime too, plus link-count changes from sfsLink/sfsRemove
bump ctime alone, matching real inode semantics). Both are stamped at
creation, so a freshly created node has mtime == ctime. Return 0 for an
invalid/unmounted lookup, same convention as sfsSizeOf/sfsTypeOf. */
uint64 sfsMtimeOf(int32 inodeNum);
uint64 sfsCtimeOf(int32 inodeNum);
/* Real owner rwx permission bits (fs.h's fsModeOwner*), stored on disk in
the inode like every other field above -- survives sfsUnmount()+
sfsMount(). Stamped at creation with a type-based default (createNode:
fsModeDefaultDir for a directory, fsModeDefaultFile for anything else)
and changed only by sfsChmod. sfsModeOf returns 0 for an invalid/
unmounted lookup, same convention as sfsSizeOf/sfsTypeOf. sfsChmod
overwrites the whole value (no symbolic +x-style merge, matching real
chmod(2)'s own numeric-mode form) and bumps ctime like any other
metadata-only mutation (sfsLink's linkCount bump does the same) --
returns false for an invalid/unmounted inodeNum, true otherwise. */
uint32 sfsModeOf(int32 inodeNum);
bool sfsChmod(int32 inodeNum, uint32 mode);
/* Real owner identity (fs.h's FbStat.uid), stored on disk like every other
inode field above -- survives sfsUnmount()+sfsMount(). Stamped once, at
creation (createNode: sched.h's currentThreadUid(), the CALLER's real
identity at the moment of the create/mkdir/symlink/mkfifo call), and
afterward only ever changed by sfsChown (below). sfsUidOf returns 0 for
an invalid/unmounted lookup, same convention as sfsModeOf. */
uint32 sfsUidOf(int32 inodeNum);
/* Real chown(2)'s on-disk half: overwrites the inode's owner uid outright
and bumps ctime like sfsChmod's own metadata-only mutation does.
Deliberately has NO privilege check of its own -- vfsChown (vfs.c) is the
one real choke point that enforces the actual chown(2) privilege rule
(root-only, real Unix's _POSIX_CHOWN_RESTRICTED default: not even a
file's own owner may give it away), exactly like sfsChmod leaves
ownership-vs-caller policy to vfsChmod. Returns false for an invalid/
unmounted inodeNum, same convention as sfsChmod. */
bool sfsChown(int32 inodeNum, uint32 newUid);
/* Real group identity (fs.h's FbStat.gid), stored on disk like uid above --
survives sfsUnmount()+sfsMount(). Stamped once, at creation (createNode:
sched.h's currentThreadGid()), and afterward only ever changed by
sfsChgrp (below). sfsGidOf returns 0 for an invalid/unmounted lookup,
same convention as sfsUidOf. */
uint32 sfsGidOf(int32 inodeNum);
/* Real chgrp(2)'s on-disk half: overwrites the inode's group gid outright
and bumps ctime, mirroring sfsChown's own shape exactly. Deliberately has
NO privilege check of its own -- vfsChgrp (vfs.c) is the real choke point
(owner may set only their own current gid, root may set any gid). Returns
false for an invalid/unmounted inodeNum, same convention as sfsChown. */
bool sfsChgrp(int32 inodeNum, uint32 newGid);
/* Grows or shrinks a file in place, freeing/allocating data blocks as
needed. */
bool sfsTruncate(int32 inodeNum, uint64 newSize);
/* Reads/writes inode-relative byte ranges. Writing past the current end of
file zero-fills the gap first (no sparse holes), then writes the payload,
growing the file and allocating blocks (direct, then single indirect,
then double indirect, then triple indirect) as needed. Returns bytes
transferred, or -1 on error (inode not a file, or the file has hit the
direct+indirect+double+triple addressing limit, roughly 1 GiB). */
int64 sfsReadAt(int32 inodeNum, uint64 offset, void* buffer, uint64 length);
int64 sfsWriteAt(int32 inodeNum, uint64 offset, const void* buffer, uint64 length);
/* Lists directory entries by index (0, 1, 2, ...). Returns false past the
last entry. */
bool sfsReaddirAt(int32 dirInode, uint32 index, char* nameOut, uint32 nameCap, uint32* typeOut);
#endif
+270
View File
@@ -1,6 +1,276 @@
#ifndef FROSTBYTE_SHELL_H #ifndef FROSTBYTE_SHELL_H
#define FROSTBYTE_SHELL_H #define FROSTBYTE_SHELL_H
#include "types.h"
void shellRun(void); void shellRun(void);
/* Mirrors elf.h's elfMaxArgs (kept as shell.c's own named copy rather than
including elf.h just for one constant, the same "each caller of this
pattern owns its own named copy" choice jobctl.c/kmain.c/syscall.c
already made for their own shared "one slot's worth of stack" constant)
-- an argv built with more tokens than this could never be handed to
elfBuildInitialStack anyway. */
#define shellMaxArgs 16
/* Upper bound on how many `|`-separated stages shellSplitPipelineStages
below (and shell.c's runPipelineStages) will ever build at once -- a
generous cap for anything a human could plausibly type on one
lineCapacity-bounded (128 bytes, shell.c) command line, not a claim that
real shells have such a limit. Named analogously to shellMaxArgs (a
per-command cap) but for the orthogonal "how many commands are chained
together" axis. */
#define shellMaxPipelineStages 4
/* Splits `text` in place (each run of spaces becomes a NUL terminator,
like strtok but reentrant -- no hidden static state) into at most
maxTokens pointers into that same buffer, returning how many it found.
Takes an explicit capacity rather than hardcoding shellMaxArgs
internally so a caller that already consumed some of its own argv[]
array (runProgram, shell.c: argv[0] is always the program name itself,
not something shellTokenize ever produces) can correctly pass only the
REMAINING room, not the full array size -- passing the full array size
there would let this function write one past the end of it. Exposed
here (rather than kept static in shell.c) so shellTest.c (ktest.h) can
exercise its edge cases -- leading/trailing/repeated spaces, empty
input, hitting the capacity -- as pure string manipulation with no VFS
or scheduler involved, safe to run from KTEST's pre-scheduler
context. */
int shellTokenize(char* text, char* tokens[], int maxTokens);
/* Splits `line` in place at every top-level '|' into up to `maxStages`
NUL-terminated stages, written into `stages[0..result-1]` -- shell.c's
`cmd1 [args] | cmd2 [args] | ... | cmdN [args]` pipeline syntax, now a
REAL N-stage chain rather than only ever the first two commands (jobctl.h's
jobctlSpawnPiped already took independent stdin/stdout pipe handles per
spawn from the day it was written, so a middle stage with BOTH ends piped
at once needed no new kernel primitive -- only this function and
shell.c's runPipelineStages were ever the limiting factor).
Whitespace directly touching each '|' is trimmed from both sides of it
(so "a | b | c" and "a|b|c" produce the same stages) -- shellTokenize's
own token splitter only ever strips LEADING spaces from whatever it is
handed, never trailing ones, so every stage but the last has its trailing
spaces trimmed here first.
Returns 0 (leaving `line` and `stages` completely untouched) if there is
no '|' anywhere in the line at all, so the caller (execute()) knows to
fall back to running a single command exactly as before. Returns -1
(also leaving everything untouched) if the line contains MORE than
`maxStages` stages -- a real shell has no such limit, but this kernel's
per-pipeline arrays (shell.c's runPipelineStages, jobctl.c's
jobctlWaitPipelineStages) are fixed-size, so a caller must be able to
tell "malformed count" apart from "no pipe found" to report the right
error instead of silently truncating a longer chain. Otherwise returns
the real stage count (2..maxStages). Exposed here (rather than kept
static in shell.c) for the same pre-scheduler, no-VFS KTEST coverage
shellTokenize already gets (shellTest.c). */
int shellSplitPipelineStages(char* line, char* stages[], int maxStages);
/* Upper bound on how many `&&`/`||`-separated segments shellSplitAndOr
below (and shell.c's execute()) will ever build at once -- the exact
same "generous cap for one lineCapacity-bounded command line, not a
claim real shells have such a limit" reasoning shellMaxPipelineStages
above already gives, on the orthogonal "how many conditionally-run
commands are chained together" axis. */
#define shellMaxAndOrSegments 4
/* The operator immediately BEFORE segments[i+1] in shellSplitAndOr's own
output (there is no operator before segments[0], so andOrOps[] below
only ever has count-1 meaningful entries) -- real shell `&&`
(short-circuit: run the next command only if the previous one
succeeded, exit status 0) vs `||` (run the next command only if the
previous one FAILED, exit status != 0). */
typedef enum {
shellAndOrAnd = 1,
shellAndOrOr = 2
} ShellAndOrOp;
/* Splits `line` in place at every top-level `&&`/`||` into up to
`maxSegments` NUL-terminated segments, written into
`segments[0..result-1]`, with `andOrOps[0..result-2]` recording which
operator sat between each adjacent pair -- shell.c's new
`cmd1 && cmd2 || cmd3 && ...` conditional-chaining syntax, run BEFORE
pipeline splitting (`|` binds tighter than `&&`/`||`, exactly like a
real shell: `a | b && c` means "(a | b) && c", so each of THIS
function's own segments is independently handed to
shellSplitPipelineStages afterward, never the other way around).
Whitespace directly touching each operator is trimmed from both sides
of it, the same "a&&b and a && b produce the same segments" convention
shellSplitPipelineStages already established for `|`. A lone `&`
(shell.c's own trailing background marker, checked much later after
this function has already run) is never mistaken for half of `&&`,
since this function only ever matches the exact two-character sequence
-- a single `&` anywhere else in the line is simply left inside
whichever segment it falls in, untouched.
Returns 0 (leaving `line` and `segments` completely untouched) if there
is no top-level `&&`/`||` anywhere in the line, so the caller knows to
run it as a single command-or-pipeline exactly as before. Returns -1
(also leaving everything untouched) if the line contains MORE than
`maxSegments` segments, the same "malformed count, do not silently
truncate" shape shellSplitPipelineStages already uses for its own
analogous limit. Otherwise returns the real segment count
(2..maxSegments). Exposed here (rather than kept static in shell.c) for
the same pre-scheduler, no-VFS KTEST coverage shellSplitPipelineStages
already gets (shellTest.c). */
int shellSplitAndOr(char* line, char* segments[], ShellAndOrOp andOrOps[], int maxSegments);
/* Result of shellExtractRedirects below -- nullptr inPath/outPath/errPath/
hereString means "no such redirection was found, leave that fd at its
normal default". */
typedef struct ShellRedirect {
const char* inPath;
const char* outPath;
bool appendOut;
const char* errPath;
bool appendErr;
/* `<<<` TEXT -- shell.c's new here-string syntax. Unlike inPath (a file
to open), this is the literal next token's own text, fed to the
child's fd 0 through a real anonymous pipe the SHELL itself writes
into (jobctl.h's jobctlSpawnForegroundHereString/
jobctlRunBackgroundHereString), never opened as a path. Mutually
exclusive with inPath at the SAME fd 0 -- see shellExtractRedirects'
own doc comment below for the last-one-wins rule between the two. */
const char* hereString;
} ShellRedirect;
/* Scans an already-tokenized argv (shellTokenize's own output shape) for
`<`, `<<<`, `>`, `>>`, `2>`, and `2>>` tokens -- shell.c's new
`cmd [ARGS] [< IN | <<< TEXT] [> OUT | >> OUT] [2> ERR | 2>> ERR]`
redirection syntax -- and removes each one together with the token
immediately after it (a filename for every operator except `<<<`, whose
own following token is the literal here-string text instead), compacting
the remaining tokens down to a real argv with no gaps (exactly what
jobctl.h's spawn functions need to hand a program, which must never see
its own redirection operators as if they were its own arguments).
Writes the compacted count back through `argc` and fills `out` with
whichever paths/text were found (nullptr for whichever operator never
appeared). Returns false, leaving `argv`/`*argc` completely untouched, if
`<`, `<<<`, `>`, `>>`, `2>`, or `2>>` appears as the LAST token with no
filename/text after it -- the same "malformed input, caller falls back to
reporting an error" shape shellSplitPipelineStages uses for "no pipe
found" (though this one look more like an error since every OTHER token
still gets scanned first, unlike shellSplitPipelineStages's single
up-front counting pass). A later `>`/`>>` overwrites an earlier one (and vice versa)
rather than erroring, the same "last one wins" a real shell gives
`cmd > a > b` -- only ever ONE is meaningful since a single fd 1 can only
be redirected once; `2>`/`2>>` follow the exact same last-one-wins rule
independently for fd 2, since the two operator families target different
fds and never conflict with each other. `<` and `<<<` follow the SAME
last-one-wins rule for fd 0 across the two of them together (whichever
appears later wins, clearing the other field) -- a real shell gives fd 0
to whichever of `< file`/`<<< text` was written last too, since both
target the exact same single fd. Exposed here (rather than kept static in
shell.c) for the same pre-scheduler, no-VFS KTEST coverage
shellTokenize/shellSplitPipelineStages already get (shellTest.c). */
bool shellExtractRedirects(char** argv, int* argc, ShellRedirect* out);
/* Recognizes shell.c's `$NAME` unquoted-parameter-expansion syntax (the
glue that actually resolves it against a real environment, shell.c's
new shellExpandArgv, is not pure -- it reads sched.h's Thread.env via
currentThreadGetEnv, off limits for KTEST's pre-scheduler context -- so
only the SYNTAX half lives here, the same "pure string logic here, the
thread/VFS-touching glue stays static in shell.c" split shellTokenize/
shellSplitPipelineStages/shellExtractRedirects above already use).
`token` must be exactly '$' followed by one or more characters drawn
from [A-Za-z0-9_] (real shell variable-name syntax) and nothing else;
on a match, copies just the name (without the leading '$') into
`nameOut` and returns true. Returns false, `nameOut` untouched, for a
bare "$", a name that does not fit `nameCap`, or anything not starting
with '$' at all -- callers treat "false" as "not a variable reference,
leave this token exactly as typed". */
bool shellParseVarRef(const char* token, char* nameOut, int nameCap);
/* Recognizes shell.c's `$?` special parameter -- real POSIX shells' own
most-recently-completed command status, distinct from any `$NAME`
environment variable (shellParseVarRef above deliberately rejects it:
'?' is not in its own [A-Za-z0-9_] name-character set, so the two
token classes can never collide). `token` must be EXACTLY the two
characters "$?" and nothing else -- same "whole token or nothing"
contract shellParseVarRef already gives `$NAME`, so `$?x` or `x$?` are
both rejected, left for the caller to treat as an ordinary literal
token. The glue that resolves a match into an actual decimal status
string (shell.c's shellExpandToken, which reads shell.c's own
file-scope `lastStatus`) is not pure, so only this syntax check lives
here -- the same "pure logic here, thread/state-touching glue stays
static in shell.c" split shellParseVarRef already established. */
bool shellIsStatusVarToken(const char* token);
/* Classic shell wildcard matcher: '*' matches any run of characters
(including none), '?' matches exactly one character, anything else must
match `name` literally. Pure string logic (no VFS/scheduler involved),
iterative rather than recursive (a single backtrack point remembering
the most recent '*' and where in `name` it started matching from) --
the same shape a real libc fnmatch(3) uses, and the same "pure logic
here, thread/VFS-touching glue stays static in shell.c" split
shellTokenize/shellSplitPipelineStages/shellExtractRedirects/
shellParseVarRef above already established. Exposed here for
shellTest.c's KTEST coverage. */
bool shellGlobMatch(const char* pattern, const char* name);
/* True if `token` contains at least one glob metacharacter ('*' or '?') --
the same "is this token special" gate shellParseVarRef gives '$', used by
shell.c's shellExpandGlobs to decide whether a token needs real directory
listing + matching at all, or can pass through completely untouched (the
common case for most argv words, which have no wildcard in them at
all). */
bool shellTokenHasGlob(const char* token);
/* Splits `token` at its LAST '/' into `dirOut` (everything up to and
including that '/', or an empty string if `token` has no '/' at all --
meaning "the current directory", exactly like a bare `*.txt` in a real
shell) and `patternOut` (everything after it, the actual glob pattern to
match each directory entry's own name against via shellGlobMatch above).
Only this FINAL path component may itself contain glob syntax -- `dirOut`
is never re-scanned for `*`/`?` by anything in shell.c, an
honestly-scoped limitation (e.g. `/tmp/gd/ *.txt` is supported,
`/tmp/ * /file.txt` is not) matching this file's existing pattern of
documenting what its syntax does NOT attempt (see runPipelineStages' own
"no stage may be a builtin" and shellExtractRedirects' own redirect-target
scope limits). Truncates silently (like every other fixed-capacity buffer
in this shell) if either half does not fit its own capacity. Exposed here
for the same pre-scheduler, no-VFS KTEST coverage every other pure
function in this file already gets (shellTest.c). */
void shellSplitGlobToken(const char* token, char* dirOut, int dirCap, char* patternOut, int patternCap);
/* Recognizes shell.c's `$(cmd [args])` command-substitution syntax --
locates the FIRST `$(` anywhere in `line` and its matching `)` (no
nesting: the very next `)` after the opening `$(`, an honestly-scoped
limitation matching shellSplitGlobToken's own "only the final path
component" scope note), and copies the text between them (the command
to run, still completely unparsed) into `cmdOut`, truncated to `cmdCap`
like every other fixed-capacity buffer in this file.
Returns true with `*startOut` set to -1 (cmdOut/`*endOut` left
untouched) if `line` contains no `$(` at all -- the caller's signal to
stop looking, exactly like shellSplitPipelineStages' own "0 means
nothing found" convention but spelled with a sentinel index instead of a
sentinel count, since a real match also needs to report WHERE in the
line it sits. On a real match, `*startOut`/`*endOut` are the byte
offsets of the leading `$` and one-past the matching `)` respectively,
so a caller can splice `line[0..*startOut)` + (whatever `cmdOut`
evaluates to) + `line[*endOut..]` into a new line the same way
shellExtractRedirects' caller already reassembles argv around removed
tokens. Returns false, leaving everything untouched, if a `$(` is found
with no matching `)` anywhere after it -- the same "malformed input,
caller reports an error" shape shellExtractRedirects already uses for an
operator with no filename. Exposed here (rather than kept static in
shell.c) for the same pre-scheduler, no-VFS KTEST coverage every other
pure syntax function in this file already gets (shellTest.c) -- the glue
that actually RUNS `cmdOut` and splices its captured output back in
(shell.c's shellExpandCommandSubst) is not pure, so only the syntax half
lives here, the same split shellParseVarRef/shellIsStatusVarToken above
already established for `$NAME`/`$?`. */
bool shellFindCommandSubst(const char* line, int* startOut, int* endOut, char* cmdOut, int cmdCap);
/* The pid most recently spawned by the shell's own `NAME [ARGS] [&]`
fallback (shell.c's runProgram) -- foreground or background alike, set
the instant jobctlSpawnForeground/jobctlRunBackground (jobctl.h) hand
one back, never only after a foreground job has already exited -- or
threadInvalidId if the shell has never run anything yet. Lets a
boot-time live demo (kmain.c) that drives the real interactive shell
through injected keystrokes (there being no automated way to drive a
real PS/2 keyboard under headless QEMU) find out what pid it just asked
the shell to run, so it can act on that specific job (e.g. proving a
backgrounded one stays alive across an injected Ctrl-C the same way
user/pgroupbg.c's boot-time-spawned counterpart already does). */
uint32 shellLastSpawnedPid(void);
#endif #endif
+368
View File
@@ -0,0 +1,368 @@
#ifndef FROSTBYTE_SIGNAL_H
#define FROSTBYTE_SIGNAL_H
#include "types.h"
#include "isr.h"
#include "sched.h"
#include "signalnum.h"
/*
* Real (catchable) signal delivery on top of the single-address-space,
* ring0/ring3 kernel: a thread can install a handler for a signal number,
* mask/unmask signals, and have another thread (or itself) raise one --
* delivery happens the moment the target thread next returns to ring3
* through the interrupt-return path (isr.c's deliverPendingSignals), by
* rewriting its saved trapframe to enter a small shared trampoline page
* (sigInstallTrampoline below) instead of resuming where it was
* interrupted. The trampoline calls the handler, then traps back in via
* sysSigReturn, which restores the exact pre-signal register snapshot this
* kernel keeps in the Thread struct itself (sigSavedRegs) -- no signal
* frame is ever pushed onto the user stack, since there is no need to: the
* saved context already lives in kernel memory, unlike a real kernel with
* per-process address spaces where the handler must run in a stack the
* kernel does not otherwise own a copy of.
*
* Job control (SIGSTOP/SIGCONT/SIGTSTP) is real, on top of a genuine
* threadStopped ThreadState (sched.h): SIGSTOP, like SIGKILL, bypasses the
* pending/blocked/handler machinery entirely -- sigRaise delegates it
* straight to threadStop (sched.c), so it cannot be caught, blocked, or
* ignored, matching real Unix. SIGCONT is NOT bypassed the same way: it
* still sets its pending bit and runs through the normal scan (so an
* installed handler for it genuinely fires), but sigRaise ALSO
* unconditionally calls threadContinue as a side effect, independent of
* sigBlocked -- the resume itself is not something a handler or a mask can
* defer, only whether a handler additionally runs is. SIGTSTP is the
* catchable half of job control: its real-world default disposition is
* Stop rather than Terminate (sigDefaultIsStop), reported as
* sigOutcomeStop for isr.c to act on the same way it already acts on
* sigOutcomeTerminate, but -- unlike SIGSTOP -- an installed handler for it
* runs instead of stopping, and it can be blocked/ignored like any other
* catchable signal.
*
* Per-signal sa_mask (POSIX sigaction's sa_mask, not sigprocmask's whole-
* process mask): sigSetHandler takes a `mask` alongside the handler address,
* stored in Thread.sigHandlerMasks[signum] (sched.h) -- while that signum's
* handler is running, sigBlocked is the union of whatever was already
* blocked, this mask, and (unless sigFlagNoDefer -- signalnum.h -- is set
* for it) the signal itself, self-blocked like real Unix's implicit
* default. A DIFFERENT, unmasked signal arriving while a handler runs is
* therefore genuinely deliverable, not blanket-blocked the way an earlier
* version of this file (see git history) used to: sigConsumePending is
* gated by Thread.sigHandlerDepth against sigMaxNestDepth (sched.h), not by
* a single "one handler at a time" bool, so a second handler can run
* NESTED inside the first (its own context pushed onto the small
* sigSavedRegs/sigSavedBlocked stack) and the first resumes, via
* sigOnSigReturn popping that same stack, exactly where it left off once
* the nested one returns. user/nesttarget.c is the live ring-3 proof for
* two DIFFERENT signals; user/nodefertest.c is the same mechanism proving
* the SAME signal number can nest into its own still-active handler when
* registered with sigFlagNoDefer (and, by contrast, genuinely cannot
* without it); sigTest.c drives the stack directly for fully deterministic
* versions of both.
*
* Early wake of a Sleeping/Blocked target (real Unix's interruptible
* sleep/EINTR): sigRaise calls sched.c's threadWakeForSignal for any
* signal signal.c's sigWouldDeliver determines would genuinely be
* delivered (not blocked, not ignored) -- forcing the target straight to
* threadRunnable so the NEXT schedule() eventually resumes it, reaching a
* return-to-ring3 point (isr.c's deliverPendingSignals) it might otherwise
* NEVER naturally reach again (e.g. user/eintrtarget.c's read, which no
* writer will ever satisfy). A blocked or ignored signal leaves a
* Sleeping/Blocked target's wait completely undisturbed, exactly like real
* Unix -- sigWouldDeliver is checked first. threadWakeForSignal itself
* excludes threadStopped on purpose: job control's "stopped means stopped"
* must survive an ordinary signal arriving too, so only threadContinue (an
* explicit SIGCONT) may ever move a Stopped thread, the same asymmetry
* schedWakeChannel already has for the same reason.
*
* SA_RESTART / EINTR (signalnum.h's sigFlagRestart, sched.h's
* Thread.sigInRetryableSyscall): being woken early (or simply signaled
* while already Blocked) answers "does the target wake up at all", not
* "what does the interrupted syscall return once its handler is done" --
* that second question is what sa_flags settles. Before this existed,
* every retryable syscall (sysRead/sysWrite/sysWait, syscall.c) restarted
* unconditionally once a handler returned, since sysSigReturn always
* restores the exact rewound rip sigConsumePending captured. Now
* sigConsumePending checks, at the moment it is about to push a custom
* handler's context, whether the thread is genuinely mid one of those three
* (sigInRetryableSyscall) and the signal being delivered lacks
* sigFlagRestart for its own handler; if so, it rewrites the ABOUT-TO-BE-
* SAVED context in place (rip past the interrupted `int $0x80`, rax to
* errInterrupted) before pushing it, so sigOnSigReturn's later, byte-for-
* byte-faithful restore resumes the caller having genuinely observed an
* interrupted syscall, not a silently retried one -- real EINTR, not an
* approximation of it. user/restarttarget.c is the live ring-3 proof of
* both halves (with and without the flag); sigTest.c drives
* sigConsumePending directly for the deterministic one.
*
* A thread self-targeting SIGSTOP through sysSigSend (syscall.c) is
* reasoned about and exercised by
* schedTest.c's state-machine coverage of threadStop accepting
* threadRunning, but -- unlike the external-stop/continue scenario
* user/jobtest.c demonstrates live -- has no live ring-3 demo of its own
* this session: proving it would need a second, independent process to
* race-freely learn "the target has now actually stopped" before sending
* SIGCONT, which is exactly the kind of guess-free rendezvous this file's
* OWN sigtarget.c comment already had to solve once for a different race,
* and doing it again here was judged not worth the added live-boot
* surface for a self-stop path whose actual mechanism (threadStop, called
* on the current thread) is not otherwise unusual. SIGKILL is intentionally
* NOT routed through this pending/blocked machinery at all: sigRaise
* delegates it straight to the existing threadKill (sched.c), the one
* unconditional, unmaskable, uncatchable termination path this kernel
* already had (now also accepting threadStopped, since a real
* kill(pid, SIGKILL) kills a stopped process too) -- exactly like
* real kill(pid, SIGKILL) never goes through a handler either.
*/
/* Signal numbers, handler sentinels, sigSetFailed and the sigMask* "how"
values all live in signalnum.h, shared verbatim with userspace (see that
header for why it is split out). */
/* sigMaxSignals (one bit per signal number in Thread.sigPending/
sigBlocked, and the size of Thread.sigHandlers[]) is defined in sched.h,
next to the Thread struct that actually needs it as an array bound --
signal.h cannot define it itself without sched.h including signal.h
right back, which would be circular. */
/* Every signal bit set -- a plain "block literally everything" convenience
constant for callers/tests that want to exercise sigProcMask's own
sigKill/sigStop stripping, or job control's "resume happens regardless of
sigBlocked" guarantee, with the widest possible mask. Not used by
sigConsumePending's own handler-entry logic any more (see the file
comment's per-signal sa_mask paragraph) -- that is real, per-handler
Thread.sigHandlerMasks[signum] now, not this one blanket value. */
#define sigAllBlocked (~0ull)
/* Fixed virtual address of the one shared, read+execute, never-write,
user-accessible trampoline page every process's signal handlers are
entered through (sigInstallTrampoline maps it once at boot). Chosen
deliberately far from both the boot-time 1 GiB identity-mapped range
([0, 0x40000000), see boot.S's bootPd -- pagingMap refuses to touch
anything inside a huge-page-mapped range anyway) and the per-binary
elfSlotSize=0x10000000 slots USER_BASE hands out one at a time starting
at 0x40000000 (syscall.c's userSpaceMin) -- at the current growth rate
(Makefile) that sequence would need roughly three thousand more user
binaries to ever reach here, and every process already shares this one
address space regardless (see the architecture note in sched.h), so one
fixed shared page for ALL of them, like a real kernel's vDSO, is exactly
as much mapping as this needs. */
#define sigTrampolineVirtualAddr 0x0000700000000000ull
/* Fixed virtual address of the shared, read-only (never write, never
execute), user-accessible SigInfo (signalnum.h) data page(s) every
process's sigFlagSigInfo handlers are handed a pointer into --
sigInstallInfoPage maps it once at boot, right after the trampoline page
above, in the same reserved high range for the same reasons that page's
own comment already gives (nowhere near either the boot-time identity
map or any real per-binary elfSlotSize slot). One frameSize-sized page
holds frameSize/sizeof(SigInfo) slots; sigInstallInfoPage maps as many
consecutive pages, starting here, as threadCapacity()*sigMaxNestDepth
slots actually need (today: 96*4 = 384 slots, comfortably inside 2
pages) -- see sigConsumePending for how a (thread id, handler-nesting
depth) pair maps to one specific slot, the same indexing scheme
sigSavedRegs/sigSavedBlocked (sched.h) already use for the exact same
pair, just kept in a user-readable page here instead of Thread itself
since a handler running in ring 3 has no way to reach into kernel
memory. */
#define sigInfoVirtualAddr 0x0000700000010000ull
/* How many consecutive frameSize pages, starting at sigInfoVirtualAddr, a
fixed-size static array of physical frame addresses (signal.c) can hold
-- sigInstallInfoPage only ever maps as many as threadCapacity()*
sigMaxNestDepth slots actually need (2 today), so this is deliberately
generous headroom, not a tight fit: maxThreads (sched.c) has grown by a
fixed +8 roughly once per session for a long history of falas, and this
comfortably absorbs many more such bumps (8 pages * frameSize/sizeof
(SigInfo) slots-per-page / sigMaxNestDepth == room for hundreds of
threads) before ever needing to be revisited, the same "generous
headroom, not exact" reasoning sigMaxNestDepth's own comment (sched.h)
already gives for itself. */
#define sigInfoMaxPages 8
typedef enum {
sigOutcomeNone = 0,
sigOutcomeHandler = 1,
sigOutcomeTerminate = 2,
/* Default disposition of a catchable job-control signal (SIGTSTP only
-- SIGSTOP never reaches sigConsumePending at all, see the file
comment). Same contract as sigOutcomeTerminate: this function only
REPORTS it, the caller (isr.c) must actually call threadStop and
reschedule, since only schedule() can produce the next thread's own
rsp to resume with. */
sigOutcomeStop = 3
} SigOutcome;
/* Resets ALL signal state on a Thread slot to fresh defaults: no pending or
blocked signals, no installed handlers or their masks, handler-nesting
depth zero. Called by threadCreate/threadCreateUser (sched.c) on every
(re)use of a table slot,
the same explicit-field-by-field convention threadCreate/threadCreateUser
already use for every other Thread field -- threadReap/threadDiscard's
own memset() of the whole struct already guarantees this before a slot
is ever reused, so this is defensive documentation of the invariant, not
a fix for a leak that could otherwise happen (unlike the console-reader/
pipe-fd leaks this subsystem's sibling code had to actually fix, which
lived in tables OUTSIDE Thread that no memset here ever touched). */
void sigResetState(Thread* thread);
/* Raises signal `signum` against thread `targetId`. sigKill and sigStop are
both delegated straight to threadKill/threadStop (see file comment) and
never touch sigPending at all. sigCont ALSO calls threadContinue as an
unconditional side effect (resuming a Stopped target regardless of
sigBlocked) but, unlike sigKill/sigStop, still falls through to set its
own pending bit afterwards, so an installed handler still runs through
the normal scan. Any other signum just sets the pending bit, then --
if sigWouldDeliver agrees this signal is not blocked or ignored --
calls threadWakeForSignal (sched.c) to force a Sleeping/Blocked target
straight to Runnable early, real Unix's interruptible sleep/EINTR (see
file comment); actual delivery itself still only ever happens later,
off the interrupt-return path (isr.c), once that thread genuinely
resumes. Returns false, changing nothing, for an out-of-range signum, an
invalid/out-of-range targetId, or a target that is Dead/Unused (mirrors
threadKill's own validation for the non-SIGKILL path, and threadKill's/
threadStop's own for their delegated paths). */
bool sigRaise(uint32 targetId, int signum);
/* Exactly sigRaise above, plus attaching a real SigInfo (signalnum.h) to
this specific raise -- `siCode`/`siPid`/`siStatus` become
target->sigPendingInfo[signum] (sched.h) the moment the pending bit
itself is set, later handed to a sigFlagSigInfo handler by
sigConsumePending. sigRaise itself is now a thin wrapper around this
(siCodeKernel, 0, 0 -- "no specific detail available/tracked", the
honest default for the many pre-existing callers, including sigTest.c's
whole KTEST suite, that only ever cared about signum/pending-bit
semantics and never plumbed real sender/child detail through). Real
production call sites needing genuine detail (sched.c's SIGALRM/SIGCHLD
raises, syscall.c's sysSigSend) call this directly instead. Same return
value, same validation, same sigKill/sigStop delegation, same early-wake
side effect as sigRaise -- see that function's own doc comment for all
of it, not repeated here. */
bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus);
/* Real kill(-pgid, signum)/kill(0, signum)'s group-targeted half (sched.h's
Thread.pgid; userSigSend/userKill's negative/zero-pid convention,
syscall.c's sysSigSend/sysKill) and keyboard.c's own real Ctrl-C/Ctrl-Z
(SIGINT/SIGTSTP delivered to the FOREGROUND process group only, never a
single target thread): scans every currently live thread once and calls
sigRaiseInfo above against each one whose pgid matches, independently --
SIGKILL/SIGSTOP/SIGCONT's own special-cased bypasses, per-target
early-wake, and SigInfo attachment therefore all apply exactly as they
already do for a plain, single-target send, simply repeated once per
group member, rather than this function sharing or reimplementing any of
that delivery logic itself. A pgid with no currently live member at all
(nobody has ever claimed the foreground yet, or every process that once
shared it has since died) is a harmless no-op, matching real kill(2)'s
ESRCH for an empty target set -- reported back as false, exactly like a
single-target sigRaiseInfo already reports false for an invalid/dead
target. */
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid);
/* Installs `handlerAddr` (a real user-code address, or sigHandlerDefault /
sigHandlerIgnore) as thread's disposition for `signum`, `mask` as its
sa_mask -- the additional signals sigConsumePending blocks for as long as
THIS handler is active (stripped of sigKill/sigStop first, same as
sigProcMask: neither can ever be blocked, so a mask cannot "achieve" it
either) -- and `flags` (signalnum.h's sigFlagRestart, or 0) as its
sa_flags. Returns the PREVIOUS disposition (like real signal(2)/
sigaction(2)'s oldact->sa_handler) or sigSetFailed for an out-of-range
signum or an attempt to touch sigKill or sigStop (both uncatchable, like
real EINVAL on SIGKILL/SIGSTOP) -- `mask`/`flags` are stored regardless of
whether `handlerAddr` is a real handler or a sentinel, but only ever
consulted for a real one (sigHandlerDefault/sigHandlerIgnore never reach
the custom-handler branch of sigConsumePending). */
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags);
/* Blocks/unblocks/replaces thread's blocked-signal mask according to `how`
(sigMaskBlock/sigMaskUnblock/sigMaskSetMask; any other value is a no-op),
returning the mask as it was BEFORE this call (like real sigprocmask's
oldset). sigKill's and sigStop's bits are silently stripped from `mask`
first -- neither can ever be blocked, matching real Unix (sigCont CAN be
blocked here, since only whether its handler runs is gated by
sigBlocked -- the resume side effect in sigRaise never consults it). */
uint64 sigProcMask(Thread* thread, int how, uint64 mask);
/* The delivery decision, called from isr.c's deliverPendingSignals on
every return-to-ring3 point for whichever thread is about to resume.
Refuses to deliver anything (returns sigOutcomeNone, touching nothing)
once thread->sigHandlerDepth already reached sigMaxNestDepth -- the small
fixed context stack is full, so whatever is pending simply waits for a
sigOnSigReturn to free a slot; this is the only depth-independent gate,
unlike the old single "one handler at a time" bool it replaced. Otherwise
scans sigPending & ~sigBlocked from signum 1 upward. An ignored signal
(explicit sigHandlerIgnore, or sigHandlerDefault whose real-world default
is Ignore -- see sigDefaultIsIgnore in signal.c) is dismissed and the
scan continues, so it can never mask a real one behind it. Otherwise:
default disposition -> sigOutcomeStop for sigTstp (sigDefaultIsStop) or
sigOutcomeTerminate for everything else (caller must actually stop/
terminate the thread, e.g. via threadStop/threadExit, and reschedule --
this function never does, since it has no way to hand back a new rsp).
Custom handler -> the interrupted context is pushed onto
thread->sigSavedRegs[depth]/sigSavedBlocked[depth] (depth being
sigHandlerDepth's value on entry, then incremented), sigBlocked becomes
the union of what it already was, sigHandlerMasks[signum] (the sa_mask
sigSetHandler stored), and signum's own bit (self-blocked, stripped of
sigKill/sigStop like every other mask operation in this file), and `regs`
is rewritten in place to enter the trampoline page (rip), call the
handler (rax) with signum as its sole argument (rdi) -- sigOutcomeHandler.
If sigHandlerFlags[signum] also has sigFlagSigInfo set, thread->
sigPendingInfo[signum] (whatever sigRaiseInfo last stashed there for this
signum, or sigRaise's own siCodeKernel/0/0 default) is additionally
copied into this (thread id, depth) pair's slot in the shared
sigInfoVirtualAddr page (signal.h), and rsi/rdx are rewritten too -- rsi
to that slot's real user-visible address, rdx to 0 (no ucontext_t this
kernel can expose) -- so the handler genuinely receives (signum,
SigInfo*, NULL) exactly as sigaction()'s SA_SIGINFO promises. Without the
flag, rsi/rdx are left completely untouched (whatever they already held),
exactly as before this existed, since a plain one-argument handler never
reads either. Before any of that pushing happens, thread->sigInRetryableSyscall is
consumed (read, then unconditionally cleared): if it was true (this
thread is genuinely mid a sysRead/sysWrite/sysWait retry, see sched.h)
and signum's sigHandlerFlags lacks sigFlagRestart, `regs` -- the
interrupted context about to be saved -- is rewritten first (rip past
the interrupted `int $0x80`, rax to errInterrupted) so the eventual
sigOnSigReturn resumes the caller having observed a genuine EINTR-style
failure instead of transparently retrying; sigFlagRestart (or
sigInRetryableSyscall already being false, e.g. a signal that interrupts
plain running code or a nested handler) leaves `regs` untouched here,
preserving today's transparent-retry behavior exactly.
A DIFFERENT signal not covered by that union can still be delivered on a
later call while this one is active -- pushed onto depth+1 as a genuinely
NESTED handler, sigOnSigReturn popping it back off once it returns, the
interrupted (still mid-handler) context resuming exactly where it was.
Either way `*outSignum` (if non-null) is set to whichever signal was
consumed. sigStop itself never appears here at all (see file comment --
sigRaise bypasses this whole scan for it). */
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum);
/* Undoes exactly what the sigOutcomeHandler branch of sigConsumePending did
for the MOST RECENTLY entered still-active handler (LIFO, like a real call
stack of signal frames): pops thread->sigHandlerDepth, restoring `*regs`
from thread->sigSavedRegs[depth] and thread->sigBlocked from
thread->sigSavedBlocked[depth] at the popped index -- resuming the
interrupted program (which may itself be a still-active OUTER handler, if
this was a nested one) at the exact instruction, with the exact
registers, it had before that signal arrived, as if it had never been
interrupted at all.
Returns false, changing nothing, if the thread was not actually inside a
handler (a user program calling the sigreturn syscall on its own, never
having been signaled, is a misuse this kernel simply ignores rather than
trusting). */
bool sigOnSigReturn(Thread* thread, Registers* regs);
/* Allocates one physical frame, fills it with the trampoline's machine
code (sigTrampolineStart..sigTrampolineEnd, sigTrampoline.S -- position
independent by construction, see that file), and maps it read+execute+
user (never write) at sigTrampolineVirtualAddr. Called exactly once from
kmain.c, before any user thread can possibly receive a signal. */
void sigInstallTrampoline(void);
/* Allocates and maps sigInfoVirtualAddr's data page(s) -- see that
constant's own comment for the layout and sizing. Called exactly once
from kmain.c, alongside sigInstallTrampoline (before any user thread can
possibly receive a sigFlagSigInfo delivery), and before ktestRunAll so
sigTest.c's own deterministic coverage can read the same shared page a
live ring-3 handler would. */
void sigInstallInfoPage(void);
#endif
+158
View File
@@ -0,0 +1,158 @@
#ifndef FROSTBYTE_SIGNALNUM_H
#define FROSTBYTE_SIGNALNUM_H
/*
* Plain numeric constants shared verbatim between the kernel (signal.h/
* signal.c) and userspace (user/ulib.h/ulib.c) -- exactly like syscall.h
* already is for syscall numbers. Split out from signal.h specifically so
* user code never has to include signal.h itself, which pulls in sched.h's
* Thread struct and isr.h's Registers -- kernel-only types a freestanding
* user program has no business seeing, let alone compiling against.
*/
/* Signal numbers. A small, real (not exhaustive) POSIX-shaped subset --
enough to exercise catchable delivery, ignore, default-terminate and the
one uncatchable case meaningfully, without pretending to implement every
job-control signal a real kernel would. Numbering matches Linux/x86-64
for anything actually used here, so an unused gap (5, 7, 16) does too. */
#define sigHup 1
#define sigInt 2
#define sigQuit 3
#define sigIll 4
#define sigAbrt 6
#define sigFpe 8
#define sigKill 9
#define sigUsr1 10
#define sigSegv 11
#define sigUsr2 12
#define sigPipe 13
#define sigAlrm 14
#define sigTerm 15
#define sigChld 17
#define sigCont 18
#define sigStop 19
#define sigTstp 20
/* Handler-slot sentinels (Thread.sigHandlers[signum] kernel-side,
userSigAction's `handler` argument userspace-side), distinct from any
real user code address since every user ELF slot starts at
userSpaceMin == 0x40000000 (syscall.c), far above 1. */
#define sigHandlerDefault 0ull
#define sigHandlerIgnore 1ull
/* sigSetHandler's/userSigAction's failure return -- mirrors real
signal(2)'s SIG_ERR, distinguishable from any real sentinel/handler
address a caller could have gotten back as the *previous* disposition.
Written without a uint64 cast (unlike the kernel-only constants in
signal.h) so this header stays includable from userspace, which never
sees types.h -- see fs.h's FbStat for the same convention. */
#define sigSetFailed (~0ull)
/* sigProcMask/userSigProcMask "how" values (mirrors POSIX SIG_BLOCK/
SIG_UNBLOCK/SIG_SETMASK by shape, not by number). */
#define sigMaskBlock 0
#define sigMaskUnblock 1
#define sigMaskSetMask 2
/* sigSetHandler's/userSigAction's `flags` argument (mirrors POSIX
sigaction's sa_flags by shape, not by bit position -- there is only one
flag here). Default (0, what every handler got before this bit existed)
is real POSIX's own default: a signal delivered while a thread sits
blocked inside a slow syscall (read/write/wait -- see signal.c's
sigInRetryableSyscall) does NOT transparently resume that syscall once
the handler returns, it makes the syscall return errInterrupted instead,
exactly like an EINTR on real Unix. Passing sigFlagRestart opts back into
the OTHER behavior (real SIG_RESTART/BSD's original signal(3) semantics):
the interrupted syscall is retried, invisibly to the caller, once the
handler returns. Only meaningful for a signal that actually interrupts a
genuinely retryable syscall; for anything else (plain running code, a
sleep, a signal that does not even install a handler) it is silently
irrelevant, same as real SA_RESTART. */
#define sigFlagRestart (1ull << 0)
/* Mirrors POSIX sigaction's SA_NODEFER: by default (this bit clear), a
signal's own number is added to sigBlocked for as long as ITS handler is
running (signal.h's sigConsumePending, "self-blocked like real Unix's
implicit default") -- so the same signal raised again while its handler
is still active stays merely pending, delivered only once that handler
returns, never nested into itself. Passing sigFlagNoDefer for a signal
disables exactly that one self-block (nothing else -- sa_mask/other
signals' self-blocks are untouched): a second raise of that same signal
while its own handler is active is then genuinely deliverable, entering
a NESTED instance of its own handler, the same mechanism a completely
different unmasked signal already uses to nest (see signal.h's
sa_mask paragraph) -- SA_NODEFER only widens which signal numbers that
mechanism applies to, it does not introduce a new one. Irrelevant for a
signal that never raises itself from inside its own handler. */
#define sigFlagNoDefer (1ull << 1)
/* Mirrors POSIX sigaction's SA_SIGINFO: by default (this bit clear) a
handler is called with exactly one argument, the signal number (this
kernel's ONLY shape until now, `regs->rdi = signum` in signal.c's
sigConsumePending). Passing sigFlagSigInfo for a signal tells
sigConsumePending to ALSO populate a real SigInfo (below) describing this
specific delivery -- who/what raised it, real Unix's siginfo_t.si_pid/
si_code/si_status -- and hand the handler a pointer to it as a SECOND
argument (rsi), plus a third (rdx, always 0/NULL here: this kernel has no
ucontext_t of its own to expose, unlike a real kernel's SA_SIGINFO third
argument). A handler registered with this flag must actually be declared
to take (int, SigInfo*, void*) -- see ulib.h's SigInfoHandler/
userSigActionInfo -- exactly like real sigaction()'s sa_sigaction union
member only being valid to read when SA_SIGINFO was actually passed. */
#define sigFlagSigInfo (1ull << 2)
/* si_code values SigInfo.siCode below can hold -- a small, real (not
exhaustive) subset of POSIX's SI_USER/SI_KERNEL/CLD_EXITED/CLD_KILLED/
CLD_STOPPED/CLD_CONTINUED, numbered independently of them (this kernel's
own small namespace, not a Linux ABI match, unlike the signal numbers
above which deliberately do match). siCodeUser: signum was raised by
another process's real userSigSend/kill(2)-shaped syscall (siPid is the
SENDER's own pid). siCodeKernel: raised by the kernel itself with no
specific sending process (SIGALRM's real timer expiry; also sigRaise's
own thin-wrapper default for any caller that supplies no detail at all).
siCodeChld{Exited,Killed,Stopped,Continued}: sigChld specifically,
distinguishing WHY a child's status changed (siPid is the CHILD's pid,
siStatus its real exit code for Exited, threadKilledExitCode for Killed,
or the job-control signal number for Stopped/Continued) -- real Unix's
own WIFEXITED/WIFSIGNALED/WIFSTOPPED/WIFCONTINUED distinction, exposed
directly instead of needing a separate macro to decode a packed status
word the way real wait(2)'s plain `int status` still does. */
#define siCodeUser 0
#define siCodeKernel 1
#define siCodeChldExited 2
#define siCodeChldKilled 3
#define siCodeChldStopped 4
#define siCodeChldContinued 5
/* Real siginfo_t, cut down to exactly the fields this kernel ever actually
populates (see siCode* above for what each one means per code) -- shared
verbatim between kernel and userspace like every other type in this
header, which is exactly why it is built from plain `int`, not a
types.h-only typedef (see this header's own file comment). siSignum
duplicates the handler's own first argument (signum) -- real siginfo_t
keeps si_signo too, for the same reason: a single handler function
registered against more than one signal number can tell them apart from
*info alone, without needing its own first argument at all. Handed to a
sigFlagSigInfo handler as a real pointer (signal.c's sigConsumePending)
into a small kernel-owned, user-readable (never user-writable) page
mapped at a fixed address -- see signal.h's sigInfoVirtualAddr -- the
same "one shared page, like a vDSO" trick this kernel's trampoline page
already uses, not memory the handler owns or must free. */
typedef struct SigInfo {
int siSignum;
int siCode;
int siPid;
int siStatus;
} SigInfo;
/* A retryable syscall (sysRead/sysWrite/sysWait -- see syscall.c) that got
interrupted by a signal without sigFlagRestart returns this instead of
retrying. Mirrors a raw Linux syscall's own convention of returning
-errno directly (here, -EINTR, whose real errno value is 4) rather than
a separate errno variable, since this kernel has no per-thread errno of
its own to set. Written without a uint64 cast, like every other
userspace-visible constant in this header, so it stays includable from
freestanding user code. */
#define errInterrupted (-4)
#endif
+55
View File
@@ -0,0 +1,55 @@
#ifndef FROSTBYTE_SLAB_H
#define FROSTBYTE_SLAB_H
#include "types.h"
/*
* Slab: a fixed-size object cache backed directly by physical frames from
* pmm, bypassing the general-purpose heap. Each slab is exactly one frame;
* the Slab descriptor lives at the start of that same frame (so freeing a
* pointer is O(1): mask it down to the enclosing frame to recover the
* descriptor, no list walk needed), and the rest of the frame is carved
* into objSize-aligned objects threaded onto a singly-linked free list
* through their own (currently-unused) storage.
*
* Intended for kernel-internal fixed-size allocations (list/tree nodes,
* PCB-sized structs, ...) where slab's O(1) alloc/free and per-slab
* locality beat the heap's general-purpose free-list search.
*/
typedef struct KmemCache {
const char* name;
usize objSize; /* rounded up to at least sizeof(void*), 8-aligned */
usize objsPerSlab;
struct Slab* slabs; /* every slab owned by this cache, newest first */
usize slabCount;
usize usedObjs;
} KmemCache;
/* Returns false if objSize (after rounding) cannot fit at least one object
per frame alongside the slab header -- the cache is left zeroed either
way, so kmemCacheDestroy is always safe to call. */
bool kmemCacheInit(KmemCache* cache, const char* name, usize objSize);
/* Releases every frame the cache ever grew into back to pmm. The cache is
left in a freshly-initialized (empty, growable-again) state. */
void kmemCacheDestroy(KmemCache* cache);
/* Grows the cache (allocates a new frame from pmm) on demand. nullptr only
if pmm is out of frames. */
void* kmemCacheAlloc(KmemCache* cache);
/* obj must have come from kmemCacheAlloc on this exact cache and not
already be free. */
void kmemCacheFree(KmemCache* cache, void* obj);
usize kmemCacheUsed(const KmemCache* cache);
/* Objects across every slab the cache has grown into so far -- 0 until the
first kmemCacheAlloc, since slabs are only ever grown lazily on demand. */
usize kmemCacheCapacity(const KmemCache* cache);
usize kmemCacheSlabCount(const KmemCache* cache);
/* How many objects a single slab (frame) holds for this cache -- fixed at
kmemCacheInit time, independent of how many slabs have been grown. */
usize kmemCacheObjsPerSlab(const KmemCache* cache);
#endif
+44
View File
@@ -0,0 +1,44 @@
#ifndef FROSTBYTE_SPINLOCK_H
#define FROSTBYTE_SPINLOCK_H
#include "types.h"
/*
* SpinLock: an interrupt-safe mutual-exclusion lock for state shared between
* thread context and interrupt handlers (irq0/irq1 currently run with
* interrupts re-enabled per pic.c, so a naive "just disable interrupts"
* critical section is not enough once two handlers can nest).
*
* Acquire disables interrupts and spins on an atomic test-and-set before
* returning; release restores whatever the interrupt flag was at the
* matching acquire. This is correct and SMP-ready even though frostbyte is
* currently single-core: on one core, acquiring an already-held lock is a
* bug (it would spin forever with interrupts off), so spinLockTryAcquire is
* provided for call sites -- and tests -- that need to observe contention
* without deadlocking.
*/
typedef struct SpinLock {
volatile uint32 locked;
} SpinLock;
void spinLockInit(SpinLock* lock);
/* Disables interrupts, then spins until the lock is acquired. Returns the
interrupt flag as it was immediately before this call, to be passed back
to spinLockRelease unchanged (callers must not interleave two different
locks' flags). */
uint64 spinLockAcquire(SpinLock* lock);
/* Non-blocking. On success (true), interrupts are disabled and *savedFlags
holds the pre-call flag word, exactly as spinLockAcquire would leave
things. On failure (false) nothing is touched -- interrupts are left
exactly as the caller had them. */
bool spinLockTryAcquire(SpinLock* lock, uint64* savedFlags);
/* flags must be the value spinLockAcquire/spinLockTryAcquire handed back. */
void spinLockRelease(SpinLock* lock, uint64 flags);
bool spinLockIsLocked(const SpinLock* lock);
#endif
+54
View File
@@ -15,5 +15,59 @@
#define sysGetdents 11 #define sysGetdents 11
#define sysMkdir 12 #define sysMkdir 12
#define sysUnlink 13 #define sysUnlink 13
#define sysPipe 14
#define sysSymlink 15
#define sysReadlink 16
#define sysLink 17
#define sysExec 18
#define sysSpawn 19
#define sysWait 20
#define sysPipeCreate 21
#define sysPipeAttach 22
#define sysKill 23
#define sysMkfifo 24
#define sysSigSend 25
#define sysSigAction 26
#define sysSigProcMask 27
#define sysSigReturn 28
#define sysAlarm 29
#define sysSetpgid 30
#define sysGetpgid 31
#define sysSetFgPgrp 32
#define sysGetFgPgrp 33
#define sysPoll 34
#define sysChdir 35
#define sysGetcwd 36
#define sysGetEnv 37
#define sysSetEnv 38
#define sysNice 39
#define sysGetPriority 40
#define sysTime 41
#define sysUnsetEnv 42
#define sysRmdir 43
#define sysChmod 44
#define sysGetuid 45
#define sysSetuid 46
#define sysChown 47
#define sysGetgid 48
#define sysSetgid 49
#define sysChgrp 50
#define sysPidfdOpen 51
#define sysPidfdSend 52
#define sysPidfdWait 53
#define sysMmap 54
#define sysMunmap 55
/* Sentinel pid for sysWait (syscall.c)/userWaitAny (ulib.c): real
waitpid(-1)-style "reap whichever child happens to already be dead,
without caring which specific pid" instead of the ordinary "wait for
exactly this one" contract every other sysWait id value has.
(unsigned int)-1 can never collide with a real pid, since pid 0 (idle)
and every id returned by threadCreate/threadCreateUser are always <
maxThreads (sched.c). Deliberately spelled with the plain C type, not
sched.h's uint32, so this header stays usable from userland (ulib.h/
ulib.c) exactly like fs.h/signalnum.h's own shared definitions already
are -- see fs.h's FbStat for the same convention. */
#define waitAnyPid ((unsigned int)-1)
#endif #endif
+46
View File
@@ -0,0 +1,46 @@
#ifndef FROSTBYTE_VEC_H
#define FROSTBYTE_VEC_H
#include "types.h"
/*
* Vec: a growable, type-erased array backed by the kernel heap. Elements
* are stored by value (memcpy'd in and out), sized by elemSize so a single
* implementation serves every element type without void* indirection or
* per-type code generation.
*/
typedef struct Vec {
uint8* data;
usize elemSize;
usize length;
usize capacity;
} Vec;
void vecInit(Vec* v, usize elemSize);
void vecFree(Vec* v);
/* Ensures capacity for at least newCapacity elements. Returns false only
if the underlying allocation failed (existing contents are untouched). */
bool vecReserve(Vec* v, usize newCapacity);
bool vecPush(Vec* v, const void* elem);
/* Removes and copies out the last element. False (and *outElem untouched)
when the vector is empty; outElem may be nullptr to just drop it. */
bool vecPop(Vec* v, void* outElem);
/* Pointer into the backing storage; nullptr if index is out of range.
Invalidated by any push/insert/remove/reserve that reallocates. */
void* vecAt(Vec* v, usize index);
bool vecSet(Vec* v, usize index, const void* elem);
bool vecInsert(Vec* v, usize index, const void* elem);
bool vecRemove(Vec* v, usize index, void* outElem);
void vecClear(Vec* v);
usize vecLen(const Vec* v);
usize vecCap(const Vec* v);
bool vecIsEmpty(const Vec* v);
#endif
+293 -1
View File
@@ -9,14 +9,36 @@ struct VfsNode;
typedef enum { typedef enum {
fdFree = 0, fdFree = 0,
fdConsole = 1, fdConsole = 1,
fdFile = 2 fdFile = 2,
fdPipeRead = 3,
fdPipeWrite = 4,
fdBlockDev = 5,
fdSfs = 6,
/* pidfd.h handle wrapped as an ordinary fd (FileDescriptor.pipeHandle
reused to store the pidfd.c table index, same field-reuse convention
fdPipeRead/fdPipeWrite already use for a pipe.c handle) -- unlike
anonymous pipes, a pidfd never needs the cross-process
pipeCreateDetached/vfsPipeAttach dance: it is opened by, and lives
entirely within, the SAME process that calls vfsPidfdOpen, exactly
like real Linux's pidfd_open(2). */
fdPidfd = 7
} FdKind; } FdKind;
/* FileDescriptor.flags bit for a pipe end installed by vfsPipeAttach: reads/
writes on it should really block (via syscall.c's sysRead/sysWrite retry)
rather than report "would block" (-1) the way vfsPipe's anonymous,
single-process pipe ends always have (see pipetest.c, which depends on
that -1 -- unflagged, unchanged). File-kind descriptors already store
their vfsOpen flags (oReadOnly etc, fs.h) in this same field, but never a
pipe fd, so the two uses never collide. */
#define fdFlagBlocking 0x1
typedef struct FileDescriptor { typedef struct FileDescriptor {
struct VfsNode* node; struct VfsNode* node;
uint64 offset; uint64 offset;
uint32 kind; uint32 kind;
uint32 flags; uint32 flags;
int32 pipeHandle;
} FileDescriptor; } FileDescriptor;
#define maxOpenFiles 16 #define maxOpenFiles 16
@@ -35,6 +57,239 @@ int vfsStat(const char* path, FbStat* out);
int64 vfsGetdents(int fd, void* buffer, uint64 length); int64 vfsGetdents(int fd, void* buffer, uint64 length);
int vfsMkdir(const char* path); int vfsMkdir(const char* path);
int vfsUnlink(const char* path); int vfsUnlink(const char* path);
int vfsRmdir(const char* path);
/* Real chmod(2)/chown(2) privilege rule, factored out as a pure predicate
exactly like vfsPermitted itself (see that function's own doc comment for
why): true if `callerUid` may change `ownerUid`-owned metadata, i.e. is
either the owner itself or uid 0 ("root", administrative bypass -- unlike
vfsPermitted's read/write/exec check, which deliberately has none). Only
chmod uses this; chown is stricter still (root-only, see vfsChown) since
real Unix's default _POSIX_CHOWN_RESTRICTED denies even a file's own
owner the right to give it away. Pure and explicit-argument like
vfsPermitted so it stays testable without needing a live thread's own
uid -- KTEST always runs on the idle thread (permanently uid 0), which
could never otherwise exercise the "denied" side of this check. */
bool vfsOwnerOrRoot(uint32 ownerUid, uint32 callerUid);
/* Real chmod(2): overwrites `path`'s whole owner-rwx mode (fs.h's
fsModeOwner*) with `mode` -- no symbolic +x-style merge, matching real
chmod's own numeric-mode form. Refuses unless the caller owns the file or
is uid 0 (vfsOwnerOrRoot above) -- a real, previously-missing check: every
uid could chmod any file's bits until this was added. Dispatches on
isSfsPath like every other entry point above (sfsChmod for "/mnt", the
tmpfs VfsNode's own `mode` field otherwise). Returns -1 for a missing
path, or (tmpfs only) a node under a non-writable mount -- chmod is a
metadata mutation, gated by the exact same `writable` flag every other
tmpfs mutation (vfsUnlink, vfsWrite's write path) already respects, so a
ustar-loaded /bin/ *.elf file can never have its mode changed at runtime,
matching its read-only backing store. 0 on success. */
int vfsChmod(const char* path, uint32 mode);
/* Real chown(2): overwrites `path`'s owner uid outright. Root-only
(currentThreadUid() == 0), full stop -- unlike vfsChmod, ownership is
never delegated to the file's own owner (real Unix's
_POSIX_CHOWN_RESTRICTED default: an owner who could give a file away
could use that to defeat disk-quota accounting, so only root may). Same
isSfsPath dispatch and missing-path/non-writable-mount refusal as
vfsChmod. 0 on success. */
int vfsChown(const char* path, uint32 newUid);
/* Real chgrp(2): overwrites `path`'s group gid. Unlike vfsChown (root-only,
full stop), chgrp is delegated to the file's own owner too -- but only to
set the group to the CALLER's own current gid (currentThreadGid()), never
an arbitrary one: this kernel tracks exactly one gid per thread (no
supplementary-groups list), so "a group the caller belongs to" collapses
to "the caller's own current gid", matching real chgrp(2)'s own
membership requirement for a non-privileged caller. Root (uid 0) may set
any gid, same administrative bypass vfsOwnerOrRoot already grants chmod.
Same isSfsPath dispatch and missing-path/non-writable-mount refusal as
vfsChown. 0 on success. */
int vfsChgrp(const char* path, uint32 newGid);
/* Symlinks and hardlinks are only supported on the persistent "/mnt"
filesystem (sfs.h already has the on-disk machinery for both); any other
path (the ustar/tmpfs VfsNode tree, "/dev/disk0") rejects these with -1,
the same way a real filesystem that lacks the feature would refuse the
syscall rather than fake it. */
int vfsSymlink(const char* path, const char* target);
int64 vfsReadlink(const char* path, char* buffer, uint64 cap);
int vfsLink(const char* existingPath, const char* newPath);
/* Creates an anonymous pipe and installs its two ends into the current
thread's fd table. On success readFd/writeFd receive the descriptors and
0 is returned; on failure (fd table or pipe table exhausted) -1 is
returned and neither output is written. */
int vfsPipe(int* readFd, int* writeFd);
/* Creates a pipeCreateDetached handle (pipe.h) with neither end attached to
anyone's fd table and returns its raw number, or -1 on exhaustion. Meant
to be handed to two independently spawned processes (typically as a
decimal argv string, see user/fifomaster.c) so they can rendezvous over a
real blocking pipe without needing fork() to share an fd -- see
vfsPipeAttach. */
int vfsPipeCreateHandle(void);
/* Installs `handle` (from vfsPipeCreateHandle, or in principle any live
pipe handle) into a fresh slot in the CALLING thread's own fd table, as a
read end (wantWrite false) or write end (wantWrite true), flagged
fdFlagBlocking so syscall.c's sysRead/sysWrite give it real blocking
semantics. Returns the new fd, or -1 if the handle is not a live pipe or
the fd table is full. */
int vfsPipeAttach(int handle, bool wantWrite);
/* Same as vfsPipeAttach, but installs into fd slot `fd` of an EXPLICIT fd
table instead of allocating a free slot in the CALLING thread's own
table -- jobctl.c's own way to bake a real pipe end into a brand new
child's stdin/stdout (fd 0/1) before that child has run a single
instruction, so there is never a window where it could be scheduled with
its default console fds instead (see jobctlSpawnPiped, jobctl.h). `fd` is
trusted to already name a valid slot (0/1/2) in `targetFds` -- unlike
vfsPipeAttach, this never allocates a fresh one, so the caller decides
exactly which fd is overwritten. Silently does nothing if `handle` is not
a live pipe. */
void vfsPipeAttachInto(FileDescriptor* targetFds, int fd, int handle, bool wantWrite);
/* Opens a pidfd (pidfd.h) on `targetId` and installs it as a brand new
fdPidfd descriptor in the CALLING thread's own fd table -- the
fd-table-wrapping half of pidfd.h's raw pidfdOpen, exactly the same
split vfsPipe/vfsPipeAttach already have over pipe.c's raw
pipeCreate/pipeCreateDetached. Returns the new fd, or -1 if `targetId`
is not a valid, currently-live thread (pidfdOpen's own contract) or the
calling thread's own fd table is full. */
int vfsPidfdOpen(uint32 targetId);
/* Resolves fd `fd` (which must be an fdPidfd this thread's own vfsPidfdOpen
returned) to the live target thread id it still safely refers to, or
threadInvalidId if `fd` is not an open pidfd at all, or the original
target has since been reaped/recycled (pidfdTarget's own contract,
pidfd.h) -- the single check syscall.c's sysPidfdSend/sysPidfdWait both
need before ever touching a target by number. */
uint32 vfsPidfdTarget(int fd);
/* Same as vfsOpen, but installs into fd slot `fd` of an EXPLICIT fd table
instead of allocating a free slot in the CALLING thread's own table --
the file-redirection counterpart of vfsPipeAttachInto above, used by
jobctl.c's own jobctlApplyRedirects to bake a real file open (not a
pipe) into a freshly spawned child's stdin/stdout for shell.c's new
`cmd < file` / `cmd > file` / `cmd >> file` syntax, before that child has
run a single instruction. `fd` is trusted to already name a valid slot
(0/1/2) in `targetFds`, exactly like vfsPipeAttachInto. Returns 0 on
success, -1 on the same failures vfsOpen itself can have (missing path
without oCreate, unwritable parent directory, a fifo opened with a bad
mode, exhausted pipe/inode resources) -- leaves `targetFds[fd]`
completely untouched on failure, the same "never marks a fd used when it
fails" contract vfsOpen already has for its own newly allocated fd. */
int vfsOpenInto(FileDescriptor* targetFds, int fd, const char* path, uint32 flags);
/* nullptr unless `fd` is a fdFlagBlocking pipe/fifo end or a console (fd 0)
read whose next read/write would have to report "try again" for a
resolvable reason (a pipe buffer empty but a writer could still show up,
full but a reader could still drain it, or a console queue simply not
having a keystroke queued yet) -- in which case it is the channel
(sched.h) that end's condition changing will wake. syscall.c's
sysRead/sysWrite call this after a -1 from doRead/doWrite to decide
whether to park-and-retry the syscall (schedBlockCurrent + rewind rip,
same trick sysWait uses for threadJoin) or simply hand -1 back to
userspace as-is (a non-blocking pipe end, a console write, or a
permanently broken/at-EOF pipe that retrying could never fix). */
const void* vfsBlockingChannel(int fd, bool forWrite);
/* poll.h's sysPoll (syscall.c): fills *outRevents with the subset of
`events` (poll.h's pollIn/pollOut) `fd` could satisfy right now without
blocking -- pollNval instead if `fd` is not open at all, pollErr for a
pipe write end whose only reader has gone away (a real write there would
fail immediately, not block, so it counts as "ready" the same way a
readable EOF pipe does). A regular file, "/dev/disk0", or "/mnt" fd
always reports every requested bit ready, matching real poll(2): none of
this kernel's local-storage reads/writes ever return "would block".
Returns true iff *outRevents ended up nonzero, i.e. this fd alone would
already satisfy a poll call. */
bool vfsPollReady(int fd, int16 events, int16* outRevents);
/* Named pipe: creates a real filesystem entry of type fsTypeFifo at `path`
(must live under a writable directory, e.g. the "/tmp" tmpfs mount --
same requirement makeChildFile already has for oCreate) backed by a fresh
pipeCreateDetached handle. Unlike vfsPipeCreateHandle/vfsPipeAttach, which
need SOME cooperating parent to pass the raw handle number to both sides
(typically as an argv string, see user/fifomaster.c), a fifo node lets any
two processes that simply agree on a path -- no common ancestor, no
handle to pass -- rendezvous by both calling vfsOpen on it: whichever
opens with oWriteOnly gets a blocking write end, whichever opens with
oReadOnly gets a blocking read end, exactly like vfsPipeAttach's fds.
oReadWrite (or any other flag combination) is rejected -- this kernel's
pipe end is inherently one-directional, so a dual-direction fifo fd is
out of scope, same as vfsPipeAttach's wantWrite being a single bool.
Returns 0 on success, -1 if the path already exists, its parent is not a
writable directory, or the pipe table is exhausted. Not supported under
"/dev/disk0".
Under "/mnt" this creates a REAL, persistent on-disk entry (sfs.h's
sfsTypeFifo) that survives sfsUnmount()+sfsMount() -- only the special
file itself persists, never any buffered bytes (exactly like a real
mkfifo(2) special file survives a reboot while its pipe buffer does
not): the live pipe.h handle two openers actually exchange bytes
through is created lazily, in memory, the first time anyone opens the
path after boot (or after every previous session's ends have gone away),
same mechanism as the "/tmp"-style case above, just keyed by inode
number instead of living on a VfsNode (see vfs.c's sfsFifoHandleFor). */
int vfsMkfifo(const char* path);
/* "/dev/disk0" is a reserved path (not part of the mounted tree, same as
pipes) that opens the raw ATA disk as a byte-addressable device: reads
and writes at arbitrary offsets are translated to sector I/O through
ata.h, with read-modify-write for accesses that are not sector-aligned. */
/* "/mnt" (and everything under it) is a second reserved path, backed by the
persistent on-disk filesystem in sfs.h/sfs.c rather than the kmalloc-based
VfsNode tree: open/read/write/close/lseek/stat/mkdir/unlink/getdents on a
"/mnt..." path are all dispatched to sfs.c, which stores the data on the
same ATA disk (a different LBA range than /dev/disk0's raw byte access,
see sfs.h). Data written there survives vfsInit() being called again,
because it lives on disk, not in this process's heap. */
/* Real per-process chdir(2)/getcwd(2) (sched.h's Thread.cwd). Resolves
`path` against the CALLING thread's current cwd first (vfsResolvePath
below), so a relative argument works exactly like a real chdir() call,
then requires the result to already name a real directory -- under
"/mnt" (sfs.h) or in the ordinary VfsNode tree, checked the same way
vfsStat/vfsMkdir already dispatch on isSfsPath. Returns 0 on success (the
calling thread's own cwd is now that resolved absolute path) or -1,
changing nothing, if it does not name an existing directory. For a
VfsNode-tree directory (ustar/tmpfs), the stored result is canonicalized
by walking the resolved node's real parent chain (vfsPath below), so
"."/".." components the caller typed never survive into a later
vfsGetcwd(). "/mnt" (sfs.h) has no equivalent parent-walk primitive, so
THAT branch stores vfsResolvePath's own plain string join verbatim --
every path-taking entry point below already tolerates embedded "."/".."
wherever they appear regardless (see vfs.c's lookupNode/sfsLookup), so
later relative resolution still behaves correctly either way; only
vfsGetcwd's own output could then look non-canonical (e.g.
"/mnt/foo/../bar") for an "/mnt" cwd reached that way -- an
honestly-scoped simplification, not a fake canonicalization. */
int vfsChdir(const char* path);
/* Copies the calling thread's current working directory (always already
absolute, see vfsChdir above) into `out`, nul-terminated, and returns its
length -- or -1, writing nothing, if it does not fit `cap` (real
getcwd(2)'s own ERANGE case). */
int64 vfsGetcwd(char* out, uint64 cap);
/* Resolves `path` against the CALLING thread's own cwd (sched.h's
currentThreadCwd) into `out`: copied as-is (truncated to `cap`-1 if
necessary) if it already starts with '/', otherwise joined onto the cwd
exactly like a real relative open() would -- the same "cwd + '/' +
argument" shape shell.c's own long-standing buildPath already used for
its builtins, now the ONE place that logic lives. Every path-taking
public entry point below (vfsOpen, vfsOpenInto, vfsMkdir, vfsUnlink,
vfsRmdir, vfsStat, vfsSymlink's `path` argument -- never its `target`,
which is stored verbatim like a real symlink(2) -- vfsReadlink, vfsLink,
vfsMkfifo, vfsChmod) calls this internally before doing anything else, so an
already-absolute path built by hand (shell.c's own callers, unchanged)
keeps working exactly as before. Exposed so vfsChdir can resolve ITS OWN
argument the same way before ever storing it, keeping Thread.cwd always
fully absolute. */
void vfsResolvePath(const char* path, char* out, uint64 cap);
typedef struct VfsNode VfsNode; typedef struct VfsNode VfsNode;
VfsNode* vfsLookup(const char* path); VfsNode* vfsLookup(const char* path);
@@ -44,6 +299,43 @@ const char* vfsName(VfsNode* node);
bool vfsIsDir(VfsNode* node); bool vfsIsDir(VfsNode* node);
uint64 vfsSize(VfsNode* node); uint64 vfsSize(VfsNode* node);
const void* vfsFileData(VfsNode* node); const void* vfsFileData(VfsNode* node);
/* Real owner rwx permission bits (fs.h's fsModeOwner*) for a tmpfs VfsNode
-- the exact same value vfsStat's tmpfs branch reports as FbStat.mode.
Exposed as its own accessor (VfsNode is opaque outside vfs.c, same reason
vfsSize/vfsFileData exist) so jobctl.c's jobctlSpawn can check
fsModeOwnerExec on an already-vfsLookup'd node before spawning it,
without a redundant vfsStat + path re-resolve. */
uint32 vfsMode(VfsNode* node);
/* Real owner identity (fs.h's FbStat.uid) for a tmpfs VfsNode -- the uid
that was currentThreadUid() at nodeAlloc time (vfs.c), mirroring
vfsMode's own "opaque outside vfs.c" exposure exactly, for the same
reason: syscall.c's loadImageByPath and jobctl.c's jobctlSpawn need this
on an already-vfsLookup'd node before deciding whether to spawn it. */
uint32 vfsUid(VfsNode* node);
/* Real group identity (fs.h's FbStat.gid) for a tmpfs VfsNode -- the gid
that was currentThreadGid() at nodeAlloc time, mirroring vfsUid's own
"opaque outside vfs.c" exposure for the same reason (syscall.c's
loadImageByPath, jobctl.c's jobctlSpawn). */
uint32 vfsGid(VfsNode* node);
/* Real Unix owner/group/other permission check (vfs.c): does the caller
(`callerUid`/`callerGid`) have `wantOwnerBit` (one of fs.h's fsModeOwner*
constants) against a node whose bits are `mode` and whose owner is
`ownerUid`/`ownerGid`? A matching uid checks the owner bit; else a
matching gid checks the corresponding "group" bit (fs.h's fsModeGroup*,
wantOwnerBit >> 3); else the corresponding "other" bit (fs.h's
fsModeOther*, wantOwnerBit >> 6) -- deliberately no uid-0-bypasses-
everything superuser rule (see vfsPermitted's own doc comment in vfs.c
for why). Exported (not just used internally by resolveOpen) so
syscall.c's loadImageByPath and jobctl.c's jobctlSpawn can run the
identical exec-bit check instead of each re-deriving this algorithm.
Pure function of its six arguments -- callers pass currentThreadUid()/
currentThreadGid() in explicitly, this never reads a live thread
itself. */
bool vfsPermitted(uint32 mode, uint32 ownerUid, uint32 ownerGid, uint32 callerUid, uint32 callerGid, uint32 wantOwnerBit);
void vfsPath(VfsNode* node, char* out, uint64 cap); void vfsPath(VfsNode* node, char* out, uint64 cap);
#endif #endif
Binary file not shown.
+5
View File
@@ -19,6 +19,7 @@ SECTIONS
{ {
*(.multiboot) *(.multiboot)
*(.boot.text) *(.boot.text)
*(.boot.data)
. = ALIGN(4096); . = ALIGN(4096);
*(.boot.pagetables) *(.boot.pagetables)
} :boot } :boot
@@ -33,6 +34,10 @@ SECTIONS
.rodata ALIGN(4096) : AT(ADDR(.rodata) - KERNEL_VMA) .rodata ALIGN(4096) : AT(ADDR(.rodata) - KERNEL_VMA)
{ {
*(.rodata .rodata.*) *(.rodata .rodata.*)
. = ALIGN(8);
ktestArrayStart = .;
KEEP(*(.ktests .ktests.*))
ktestArrayEnd = .;
} :rodata } :rodata
.data ALIGN(4096) : AT(ADDR(.data) - KERNEL_VMA) .data ALIGN(4096) : AT(ADDR(.data) - KERNEL_VMA)
+199
View File
@@ -0,0 +1,199 @@
#include "ata.h"
#include "ports.h"
#define ataRegData 0x1F0
#define ataRegError 0x1F1
#define ataRegSectorCnt 0x1F2
#define ataRegLbaLow 0x1F3
#define ataRegLbaMid 0x1F4
#define ataRegLbaHigh 0x1F5
#define ataRegDriveHead 0x1F6
#define ataRegStatus 0x1F7
#define ataRegCommand 0x1F7
#define ataRegControl 0x3F6
#define ataStatusErr 0x01
#define ataStatusDrq 0x08
#define ataStatusDf 0x20
#define ataStatusBsy 0x80
#define ataCmdReadSectors 0x20
#define ataCmdWriteSectors 0x30
#define ataCmdCacheFlush 0xE7
#define ataCmdIdentify 0xEC
#define ataMaxSectorsPerCommand 255
#define ataPollTimeout 1000000u
static bool diskPresent;
static uint64 totalSectors;
static char modelBuffer[41];
static void ataIoDelay(void)
{
/* Selecting a drive needs ~400ns to settle; reading the alternate
status register is a documented no-op read that burns exactly that
long without disturbing the main status register. */
inb(ataRegControl);
inb(ataRegControl);
inb(ataRegControl);
inb(ataRegControl);
}
static bool ataWaitClear(uint8 mask)
{
for (uint32 i = 0; i < ataPollTimeout; i++)
if (!(inb(ataRegStatus) & mask))
return true;
return false;
}
static bool ataWaitDrqOrErr(bool* err)
{
for (uint32 i = 0; i < ataPollTimeout; i++) {
uint8 status = inb(ataRegStatus);
if (status & (ataStatusErr | ataStatusDf)) {
*err = true;
return true;
}
if (status & ataStatusDrq) {
*err = false;
return true;
}
}
return false;
}
static void ataSelect(uint64 lba, uint8 count)
{
outb(ataRegDriveHead, (uint8)(0xE0 | ((lba >> 24) & 0x0F)));
ataIoDelay();
outb(ataRegSectorCnt, count);
outb(ataRegLbaLow, (uint8)(lba & 0xFF));
outb(ataRegLbaMid, (uint8)((lba >> 8) & 0xFF));
outb(ataRegLbaHigh, (uint8)((lba >> 16) & 0xFF));
}
bool ataInit(void)
{
diskPresent = false;
totalSectors = 0;
modelBuffer[0] = 0;
outb(ataRegControl, 0x02); /* nIEN: this driver only ever polls */
outb(ataRegDriveHead, 0xA0); /* master, no chs/lba bits needed yet */
ataIoDelay();
outb(ataRegSectorCnt, 0);
outb(ataRegLbaLow, 0);
outb(ataRegLbaMid, 0);
outb(ataRegLbaHigh, 0);
outb(ataRegCommand, ataCmdIdentify);
if (inb(ataRegStatus) == 0)
return false; /* floating bus: no drive wired up on this channel */
if (!ataWaitClear(ataStatusBsy))
return false;
/* A plain PATA hard disk leaves LBA mid/high at 0 after IDENTIFY; an
ATAPI or SATA-bridge device stamps a signature there instead. This
driver only supports the former. */
if (inb(ataRegLbaMid) != 0 || inb(ataRegLbaHigh) != 0)
return false;
bool err = false;
if (!ataWaitDrqOrErr(&err) || err)
return false;
uint16 identifyWords[256];
for (int i = 0; i < 256; i++)
identifyWords[i] = inw(ataRegData);
totalSectors = (uint64)identifyWords[60] | ((uint64)identifyWords[61] << 16);
for (int i = 0; i < 20; i++) {
uint16 word = identifyWords[27 + i];
modelBuffer[i * 2] = (char)(word >> 8);
modelBuffer[i * 2 + 1] = (char)(word & 0xFF);
}
modelBuffer[40] = 0;
for (int i = 39; i >= 0 && modelBuffer[i] == ' '; i--)
modelBuffer[i] = 0;
diskPresent = totalSectors > 0;
return diskPresent;
}
bool ataPresent(void)
{
return diskPresent;
}
uint64 ataSectorCount(void)
{
return totalSectors;
}
const char* ataModelString(void)
{
return modelBuffer;
}
bool ataReadSectors(uint64 lba, uint32 count, void* buffer)
{
if (count == 0)
return true;
if (!diskPresent || count > ataMaxSectorsPerCommand || lba + count > totalSectors)
return false;
if (!ataWaitClear(ataStatusBsy))
return false;
ataSelect(lba, (uint8)count);
outb(ataRegCommand, ataCmdReadSectors);
uint16* dst = (uint16*)buffer;
for (uint32 s = 0; s < count; s++) {
if (!ataWaitClear(ataStatusBsy))
return false;
bool err = false;
if (!ataWaitDrqOrErr(&err) || err)
return false;
for (int w = 0; w < 256; w++)
dst[w] = inw(ataRegData);
dst += 256;
}
return true;
}
bool ataWriteSectors(uint64 lba, uint32 count, const void* buffer)
{
if (count == 0)
return true;
if (!diskPresent || count > ataMaxSectorsPerCommand || lba + count > totalSectors)
return false;
if (!ataWaitClear(ataStatusBsy))
return false;
ataSelect(lba, (uint8)count);
outb(ataRegCommand, ataCmdWriteSectors);
const uint16* src = (const uint16*)buffer;
for (uint32 s = 0; s < count; s++) {
if (!ataWaitClear(ataStatusBsy))
return false;
bool err = false;
if (!ataWaitDrqOrErr(&err) || err)
return false;
for (int w = 0; w < 256; w++)
outw(ataRegData, src[w]);
src += 256;
ataIoDelay();
}
outb(ataRegCommand, ataCmdCacheFlush);
if (!ataWaitClear(ataStatusBsy))
return false;
return true;
}
+122
View File
@@ -0,0 +1,122 @@
#include "ktest.h"
#include "ata.h"
#include "string.h"
/*
* Real hardware coverage for the ATA PIO driver: these tests run against
* whatever drive QEMU actually attaches on the primary IDE bus (see
* tools/ci-test.sh / Makefile for the -drive flag), so a wiring mistake in
* ata.c (wrong port, wrong status bit, off-by-one in the LBA registers)
* shows up as a real read/write mismatch here, not as a mocked assertion.
*/
static void fillPattern(uint8* buf, uint64 seed)
{
for (int i = 0; i < ataSectorSize; i++)
buf[i] = (uint8)((seed * 7 + (uint64)i * 13) & 0xFF);
}
KTEST(ataDrivePresent)
{
KT_TRUE(t, ataPresent());
KT_GT(t, ataSectorCount(), 0);
const char* model = ataModelString();
KT_NOTNULL(t, model);
KT_TRUE(t, strlen(model) > 0);
}
KTEST(ataSingleSectorRoundTrip)
{
uint8 out[ataSectorSize];
uint8 in[ataSectorSize];
fillPattern(out, 0xA5);
KT_TRUE(t, ataWriteSectors(10, 1, out));
memset(in, 0, sizeof(in));
KT_TRUE(t, ataReadSectors(10, 1, in));
KT_EQ(t, memcmp(in, out, ataSectorSize), 0);
}
KTEST(ataOverwriteReflectsLatestData)
{
uint8 first[ataSectorSize];
uint8 second[ataSectorSize];
uint8 back[ataSectorSize];
fillPattern(first, 0x11);
fillPattern(second, 0x99);
KT_TRUE(t, ataWriteSectors(11, 1, first));
KT_TRUE(t, ataWriteSectors(11, 1, second));
KT_TRUE(t, ataReadSectors(11, 1, back));
KT_EQ(t, memcmp(back, second, ataSectorSize), 0);
KT_NE(t, memcmp(back, first, ataSectorSize), 0);
}
KTEST(ataAdjacentSectorsDoNotCrossTalk)
{
uint8 a[ataSectorSize];
uint8 b[ataSectorSize];
uint8 backA[ataSectorSize];
uint8 backB[ataSectorSize];
fillPattern(a, 0x30);
fillPattern(b, 0x77);
KT_TRUE(t, ataWriteSectors(20, 1, a));
KT_TRUE(t, ataWriteSectors(21, 1, b));
KT_TRUE(t, ataReadSectors(20, 1, backA));
KT_TRUE(t, ataReadSectors(21, 1, backB));
KT_EQ(t, memcmp(backA, a, ataSectorSize), 0);
KT_EQ(t, memcmp(backB, b, ataSectorSize), 0);
KT_NE(t, memcmp(backA, backB, ataSectorSize), 0);
}
KTEST(ataMultiSectorRoundTrip)
{
#define ataTestSectors 4
uint8 out[ataTestSectors * ataSectorSize];
uint8 in[ataTestSectors * ataSectorSize];
for (int s = 0; s < ataTestSectors; s++)
fillPattern(out + s * ataSectorSize, (uint64)(0x40 + s));
KT_TRUE(t, ataWriteSectors(30, ataTestSectors, out));
memset(in, 0, sizeof(in));
KT_TRUE(t, ataReadSectors(30, ataTestSectors, in));
for (int s = 0; s < ataTestSectors; s++)
KT_EQ(t, memcmp(in + s * ataSectorSize, out + s * ataSectorSize, ataSectorSize), 0);
#undef ataTestSectors
}
KTEST(ataLastSectorIsAddressable)
{
uint64 last = ataSectorCount() - 1;
uint8 out[ataSectorSize];
uint8 in[ataSectorSize];
fillPattern(out, 0xEE);
KT_TRUE(t, ataWriteSectors(last, 1, out));
KT_TRUE(t, ataReadSectors(last, 1, in));
KT_EQ(t, memcmp(in, out, ataSectorSize), 0);
}
KTEST(ataOutOfRangeRequestsAreRejected)
{
uint8 buf[ataSectorSize];
KT_FALSE(t, ataReadSectors(ataSectorCount(), 1, buf));
KT_FALSE(t, ataWriteSectors(ataSectorCount(), 1, buf));
/* Buffer is sized for 2 sectors: if the bounds check were buggy and let
this through, it would prove itself by corrupting the canary sector
instead of silently passing. */
uint8 twoSectors[2 * ataSectorSize];
KT_FALSE(t, ataReadSectors(ataSectorCount() - 1, 2, twoSectors));
}
KTEST(ataZeroLengthRequestIsNoop)
{
uint8 buf[ataSectorSize];
KT_TRUE(t, ataReadSectors(0, 0, buf));
KT_TRUE(t, ataWriteSectors(0, 0, buf));
}
+90
View File
@@ -0,0 +1,90 @@
#include "bmp.h"
#define bmpBitsPerWord 64
static usize wordCountFor(usize bitCount)
{
return (bitCount + bmpBitsPerWord - 1) / bmpBitsPerWord;
}
void bmpInit(Bitmap* b, uint64* backingWords, usize bitCount)
{
b->words = backingWords;
b->bitCount = bitCount;
bmpClearAll(b);
}
void bmpSet(Bitmap* b, usize index)
{
if (index >= b->bitCount)
return;
b->words[index / bmpBitsPerWord] |= (1UL << (index % bmpBitsPerWord));
}
void bmpClear(Bitmap* b, usize index)
{
if (index >= b->bitCount)
return;
b->words[index / bmpBitsPerWord] &= ~(1UL << (index % bmpBitsPerWord));
}
bool bmpTest(const Bitmap* b, usize index)
{
if (index >= b->bitCount)
return false;
return (b->words[index / bmpBitsPerWord] & (1UL << (index % bmpBitsPerWord))) != 0;
}
void bmpSetRange(Bitmap* b, usize start, usize count)
{
for (usize i = start; i < start + count && i < b->bitCount; i++)
bmpSet(b, i);
}
void bmpClearRange(Bitmap* b, usize start, usize count)
{
for (usize i = start; i < start + count && i < b->bitCount; i++)
bmpClear(b, i);
}
void bmpSetAll(Bitmap* b)
{
usize words = wordCountFor(b->bitCount);
for (usize i = 0; i < words; i++)
b->words[i] = ~0UL;
}
void bmpClearAll(Bitmap* b)
{
usize words = wordCountFor(b->bitCount);
for (usize i = 0; i < words; i++)
b->words[i] = 0;
}
usize bmpCountSet(const Bitmap* b)
{
usize total = 0;
for (usize i = 0; i < b->bitCount; i++) {
if (bmpTest(b, i))
total++;
}
return total;
}
isize bmpFindFirstClear(const Bitmap* b)
{
for (usize i = 0; i < b->bitCount; i++) {
if (!bmpTest(b, i))
return (isize)i;
}
return -1;
}
isize bmpFindFirstSet(const Bitmap* b)
{
for (usize i = 0; i < b->bitCount; i++) {
if (bmpTest(b, i))
return (isize)i;
}
return -1;
}
+86
View File
@@ -0,0 +1,86 @@
#include "ktest.h"
#include "bmp.h"
KTEST(bmpSetClearTest)
{
uint64 words[2];
Bitmap b;
bmpInit(&b, words, 100);
KT_FALSE(t, bmpTest(&b, 0));
KT_FALSE(t, bmpTest(&b, 63));
KT_FALSE(t, bmpTest(&b, 64)); /* crosses into the second word */
KT_FALSE(t, bmpTest(&b, 99));
bmpSet(&b, 0);
bmpSet(&b, 63);
bmpSet(&b, 64);
bmpSet(&b, 99);
KT_TRUE(t, bmpTest(&b, 0));
KT_TRUE(t, bmpTest(&b, 63));
KT_TRUE(t, bmpTest(&b, 64));
KT_TRUE(t, bmpTest(&b, 99));
KT_FALSE(t, bmpTest(&b, 1));
KT_EQ(t, bmpCountSet(&b), 4);
bmpClear(&b, 63);
KT_FALSE(t, bmpTest(&b, 63));
KT_EQ(t, bmpCountSet(&b), 3);
}
KTEST(bmpOutOfRangeIsSafeNoop)
{
uint64 words[1];
Bitmap b;
bmpInit(&b, words, 10);
bmpSet(&b, 500); /* must not touch memory past backing storage */
KT_FALSE(t, bmpTest(&b, 500));
bmpClear(&b, 500);
KT_EQ(t, bmpCountSet(&b), 0);
}
KTEST(bmpRangeOps)
{
uint64 words[1];
Bitmap b;
bmpInit(&b, words, 20);
bmpSetRange(&b, 5, 10); /* bits 5..14 */
KT_EQ(t, bmpCountSet(&b), 10);
KT_FALSE(t, bmpTest(&b, 4));
KT_TRUE(t, bmpTest(&b, 5));
KT_TRUE(t, bmpTest(&b, 14));
KT_FALSE(t, bmpTest(&b, 15));
bmpClearRange(&b, 5, 3); /* clears 5,6,7 */
KT_FALSE(t, bmpTest(&b, 5));
KT_FALSE(t, bmpTest(&b, 7));
KT_TRUE(t, bmpTest(&b, 8));
KT_EQ(t, bmpCountSet(&b), 7);
}
KTEST(bmpSetAllClearAllAndFind)
{
uint64 words[2];
Bitmap b;
bmpInit(&b, words, 70);
KT_EQ(t, bmpFindFirstSet(&b), -1);
KT_EQ(t, bmpFindFirstClear(&b), 0);
bmpSet(&b, 5);
KT_EQ(t, bmpFindFirstSet(&b), 5);
bmpSetAll(&b);
KT_EQ(t, bmpCountSet(&b), 70);
KT_EQ(t, bmpFindFirstClear(&b), -1);
KT_EQ(t, bmpFindFirstSet(&b), 0);
bmpClear(&b, 30);
KT_EQ(t, bmpFindFirstClear(&b), 30);
bmpClearAll(&b);
KT_EQ(t, bmpCountSet(&b), 0);
KT_EQ(t, bmpFindFirstSet(&b), -1);
}
+20
View File
@@ -60,6 +60,10 @@ fillPd:
movl $0xC0000080, %ecx movl $0xC0000080, %ecx
rdmsr rdmsr
orl $(1 << 8), %eax orl $(1 << 8), %eax
cmpl $0, cpuNxSupported
je 2f
orl $(1 << 11), %eax
2:
wrmsr wrmsr
movl %cr0, %eax movl %cr0, %eax
@@ -76,6 +80,16 @@ checkLongMode:
jb noLongMode jb noLongMode
movl $0x80000001, %eax movl $0x80000001, %eax
cpuid cpuid
/* Same cpuid leaf already gives us CPUID.80000001H:EDX.NX (bit 20) for
free -- stash it in cpuNxSupported (checked below, in %eax's later
rdmsr/wrmsr dance) before the LM (bit 29) test below, which does not
touch %edx, leaves it intact either way. */
movl $0, cpuNxSupported
testl $(1 << 20), %edx
jz 1f
movl $1, cpuNxSupported
1:
testl $(1 << 29), %edx testl $(1 << 29), %edx
jz noLongMode jz noLongMode
ret ret
@@ -111,6 +125,12 @@ bootGdtPointer:
.word bootGdtEnd - bootGdtStart - 1 .word bootGdtEnd - bootGdtStart - 1
.quad bootGdtStart .quad bootGdtStart
.section .boot.data, "aw", @progbits
.align 4
.global cpuNxSupported
cpuNxSupported:
.long 0
.section .boot.pagetables, "aw", @progbits .section .boot.pagetables, "aw", @progbits
.align 4096 .align 4096
.global bootPml4 .global bootPml4
+12 -4
View File
@@ -43,13 +43,21 @@ static void writeUnsigned(uint64 value, uint32 base, bool upper, int minWidth, c
kputchar(buffer[--length]); kputchar(buffer[--length]);
} }
static void writeSigned(int64 value) /* Mirrors klib.c's vsnprintf sinkPutSigned exactly (same minWidth/pad
contract, same "sign eats one column of the requested width" rule) --
found and fixed this fala after shell.c's new `ls -l` printed
"2026-9-7" instead of "2026-09-07": every prior kprintf call site that
wanted zero-padded fields (thread ids, sizes, hex addresses) happened
to use an unsigned specifier, so this signed-value width/pad bug went
completely unnoticed until the first zero-padded %d/%ld call ever
appeared. */
static void writeSigned(int64 value, int minWidth, char pad)
{ {
if (value < 0) { if (value < 0) {
kputchar('-'); kputchar('-');
writeUnsigned((uint64)(-value), 10, false, 0, ' '); writeUnsigned((uint64)(-value), 10, false, minWidth > 0 ? minWidth - 1 : 0, pad);
} else { } else {
writeUnsigned((uint64)value, 10, false, 0, ' '); writeUnsigned((uint64)value, 10, false, minWidth, pad);
} }
} }
@@ -93,7 +101,7 @@ void kprintf(const char* format, ...)
break; break;
case 'd': case 'd':
case 'i': case 'i':
writeSigned(longCount ? va_arg(args, int64) : (int64)va_arg(args, int)); writeSigned(longCount ? va_arg(args, int64) : (int64)va_arg(args, int), width, pad);
break; break;
case 'u': case 'u':
writeUnsigned(longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 10, false, width, pad); writeUnsigned(longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 10, false, width, pad);
+820
View File
@@ -0,0 +1,820 @@
#include "ktest.h"
#include "string.h"
#include "heap.h"
#include "pmm.h"
#include "vfs.h"
#include "fs.h"
#include "sfs.h"
/*
* Regression coverage for the subsystems that already existed before the
* in-kernel test harness: the freestanding string routines, the free-list
* heap, the physical frame allocator and the VFS/tmpfs path. These run
* against the live kernel, so a regression in any of them fails the boot gate.
*/
KTEST(coreStringMem)
{
char buf[32];
memset(buf, 'A', sizeof(buf));
KT_EQ(t, buf[0], 'A');
KT_EQ(t, buf[31], 'A');
memset(buf, 0, sizeof(buf));
KT_EQ(t, buf[0], 0);
const char* src = "frostbyte";
memcpy(buf, src, 10);
KT_STREQ(t, buf, "frostbyte");
KT_EQ(t, strlen(buf), 9);
KT_EQ(t, memcmp("abc", "abc", 3), 0);
KT_TRUE(t, memcmp("abc", "abd", 3) < 0);
KT_TRUE(t, memcmp("abd", "abc", 3) > 0);
KT_EQ(t, strcmp("abc", "abc"), 0);
KT_TRUE(t, strcmp("abc", "abd") < 0);
KT_EQ(t, strncmp("abcdef", "abcxxx", 3), 0);
KT_TRUE(t, strncmp("abcdef", "abcxxx", 4) != 0);
}
KTEST(coreMemmoveOverlap)
{
char buf[16];
memcpy(buf, "0123456789", 11);
/* forward overlap: dest > src */
memmove(buf + 2, buf, 8);
buf[10] = 0;
KT_STREQ(t, buf, "0101234567");
memcpy(buf, "0123456789", 11);
/* backward overlap: dest < src */
memmove(buf, buf + 2, 8);
buf[8] = 0;
KT_STREQ(t, buf, "23456789");
}
KTEST(coreHeapAllocFree)
{
uint64 freeBefore = heapFreeBytes();
void* a = kmalloc(100);
void* b = kmalloc(200);
void* c = kmalloc(100);
KT_NOTNULL(t, a);
KT_NOTNULL(t, b);
KT_NOTNULL(t, c);
KT_NE(t, a, b);
KT_NE(t, b, c);
/* freeing b and re-allocating a same-or-smaller block reuses the hole */
kfree(b);
void* d = kmalloc(150);
KT_EQP(t, d, b);
kfree(a);
kfree(c);
kfree(d);
/* everything returned: free bytes back to (at least) where we started */
KT_GE(t, heapFreeBytes(), freeBefore);
}
KTEST(coreHeapRealloc)
{
char* p = (char*)kmalloc(8);
KT_NOTNULL(t, p);
memcpy(p, "1234567", 8);
p = (char*)krealloc(p, 64);
KT_NOTNULL(t, p);
KT_STREQ(t, p, "1234567"); /* contents preserved across grow */
p = (char*)krealloc(p, 4);
KT_NOTNULL(t, p);
KT_EQ(t, memcmp(p, "1234", 4), 0);
kfree(p);
}
KTEST(corePmmAllocDistinct)
{
uint64 freeBefore = pmmFreeFrames();
uint64 f1 = pmmAlloc();
uint64 f2 = pmmAlloc();
uint64 f3 = pmmAlloc();
KT_NE(t, f1, 0);
KT_NE(t, f2, 0);
KT_NE(t, f3, 0);
KT_NE(t, f1, f2);
KT_NE(t, f2, f3);
KT_NE(t, f1, f3);
KT_EQ(t, f1 & (frameSize - 1), 0); /* page aligned */
KT_EQ(t, pmmFreeFrames(), freeBefore - 3);
pmmFree(f1);
pmmFree(f2);
pmmFree(f3);
KT_EQ(t, pmmFreeFrames(), freeBefore);
}
KTEST(coreVfsTmpfsRoundtrip)
{
int fd = vfsOpen("/tmp/coretest.txt", oReadWrite | oCreate | oTrunc);
KT_GE(t, fd, 0);
if (fd < 0)
return;
const char* msg = "hello vfs";
KT_EQ(t, vfsWrite(fd, msg, 9), 9);
KT_EQ(t, vfsLseek(fd, 0, seekSet), 0);
char rd[16];
memset(rd, 0, sizeof(rd));
KT_EQ(t, vfsRead(fd, rd, 9), 9);
KT_STREQ(t, rd, "hello vfs");
KT_EQ(t, vfsLseek(fd, 0, seekEnd), 9);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/tmp/coretest.txt", &st), 0);
KT_EQ(t, st.size, 9);
KT_EQ(t, st.type, fsTypeFile);
KT_EQ(t, vfsUnlink("/tmp/coretest.txt"), 0);
KT_EQ(t, vfsStat("/tmp/coretest.txt", &st), -1); /* gone */
}
KTEST(coreVfsMkdirGetdents)
{
KT_EQ(t, vfsMkdir("/tmp/cd"), 0);
KT_EQ(t, vfsMkdir("/tmp/cd"), -1); /* already exists */
int fa = vfsOpen("/tmp/cd/a", oReadWrite | oCreate);
int fb = vfsOpen("/tmp/cd/b", oReadWrite | oCreate);
KT_GE(t, fa, 0);
KT_GE(t, fb, 0);
vfsClose(fa);
vfsClose(fb);
int dd = vfsOpen("/tmp/cd", oReadOnly);
KT_GE(t, dd, 0);
FbDirent ents[8];
int64 n = vfsGetdents(dd, ents, sizeof(ents));
KT_TRUE(t, n >= (int64)(2 * sizeof(FbDirent)));
vfsClose(dd);
vfsUnlink("/tmp/cd/a");
vfsUnlink("/tmp/cd/b");
}
/* Real rmdir(2) on tmpfs: only an EMPTY directory may be removed, and the
directory must still be there (untouched) after a rejected attempt. */
KTEST(coreVfsRmdirTmpfsEmptyCheck)
{
KT_EQ(t, vfsMkdir("/tmp/rd"), 0);
int fd = vfsOpen("/tmp/rd/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsRmdir("/tmp/rd"), -1); /* not empty yet */
FbStat st;
KT_EQ(t, vfsStat("/tmp/rd", &st), 0); /* rejected attempt left it alone */
KT_EQ(t, vfsUnlink("/tmp/rd/f"), 0);
KT_EQ(t, vfsRmdir("/tmp/rd"), 0); /* empty now, succeeds */
KT_EQ(t, vfsStat("/tmp/rd", &st), -1); /* gone */
}
/* Real unlink(2)/rmdir(2) split on tmpfs: each refuses the other's kind of
node outright, neither silently falls back to the other's behavior. */
KTEST(coreVfsUnlinkAndRmdirRejectWrongTypeTmpfs)
{
KT_EQ(t, vfsMkdir("/tmp/rdDir"), 0);
KT_EQ(t, vfsUnlink("/tmp/rdDir"), -1); /* EISDIR-shaped refusal */
FbStat st;
KT_EQ(t, vfsStat("/tmp/rdDir", &st), 0); /* still there */
KT_EQ(t, vfsRmdir("/tmp/rdDir"), 0); /* the sanctioned way */
int fd = vfsOpen("/tmp/rdFile", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsRmdir("/tmp/rdFile"), -1); /* ENOTDIR-shaped refusal */
KT_EQ(t, vfsStat("/tmp/rdFile", &st), 0); /* still there */
KT_EQ(t, vfsUnlink("/tmp/rdFile"), 0);
}
/* Root can never be removed -- covered by the same `!node->writable` guard
every other mutating tmpfs entry point already relies on (root is
created non-writable in vfsInit). */
KTEST(coreVfsRmdirRefusesRoot)
{
KT_EQ(t, vfsRmdir("/"), -1);
}
/* Same two checks as coreVfsRmdirTmpfsEmptyCheck/
coreVfsUnlinkAndRmdirRejectWrongTypeTmpfs above, on the persistent SFS
mount instead of tmpfs -- proves vfs.c's dispatch layer enforces the
same real unlink(2)/rmdir(2) split on BOTH backing stores, not just the
in-memory one. Before this fala, vfsUnlink on an SFS path never checked
the inode's type at all, so unlinking an EMPTY directory under /mnt
silently succeeded (sfsRemove's own dir-emptiness check let it through)
-- this is the regression guard for that real, previously-shipped gap. */
KTEST(coreVfsRmdirSfsParity)
{
KT_EQ(t, vfsMkdir("/mnt/rdSfsEmpty"), 0);
KT_EQ(t, vfsUnlink("/mnt/rdSfsEmpty"), -1); /* the fixed bug */
FbStat st;
KT_EQ(t, vfsStat("/mnt/rdSfsEmpty", &st), 0); /* still there */
KT_EQ(t, vfsRmdir("/mnt/rdSfsEmpty"), 0);
KT_EQ(t, vfsStat("/mnt/rdSfsEmpty", &st), -1);
KT_EQ(t, vfsMkdir("/mnt/rdSfsFull"), 0);
int fd = vfsOpen("/mnt/rdSfsFull/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsRmdir("/mnt/rdSfsFull"), -1); /* not empty */
KT_EQ(t, vfsUnlink("/mnt/rdSfsFull/f"), 0);
KT_EQ(t, vfsRmdir("/mnt/rdSfsFull"), 0);
fd = vfsOpen("/mnt/rdSfsFile", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsRmdir("/mnt/rdSfsFile"), -1); /* ENOTDIR-shaped refusal */
KT_EQ(t, vfsUnlink("/mnt/rdSfsFile"), 0);
}
/* Real chmod(2) + owner rwx permission bits (fs.h's fsModeOwner*) on tmpfs:
a fresh file gets fsModeDefaultFile (0644) by default; chmod to a mode
missing the owner-write bit makes a SUBSEQUENT open-for-write fail
(EACCES-shaped) while reads keep working, and chmod back restores write
access immediately -- permission is only checked at open() time, never
re-checked against an fd already held, matching real Unix. */
KTEST(coreVfsChmodTmpfsWritePermission)
{
int fd = vfsOpen("/tmp/chmodf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
KT_EQ(t, vfsWrite(fd, "x", 1), 1);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/tmp/chmodf", &st), 0);
KT_EQ(t, st.mode, fsModeDefaultFile);
KT_EQ(t, vfsChmod("/tmp/chmodf", 0444), 0);
KT_EQ(t, vfsStat("/tmp/chmodf", &st), 0);
KT_EQ(t, st.mode, 0444u);
KT_EQ(t, vfsOpen("/tmp/chmodf", oWriteOnly), -1); /* no owner-write bit */
int rfd = vfsOpen("/tmp/chmodf", oReadOnly);
KT_GE(t, rfd, 0);
char rd;
KT_EQ(t, vfsRead(rfd, &rd, 1), 1);
vfsClose(rfd);
KT_EQ(t, vfsChmod("/tmp/chmodf", fsModeDefaultFile), 0);
int wfd = vfsOpen("/tmp/chmodf", oWriteOnly);
KT_GE(t, wfd, 0);
vfsClose(wfd);
vfsUnlink("/tmp/chmodf");
}
/* Real, previously-missing enforcement found by this fala's own grouptest.c
live demo, not review: resolveOpen (vfs.c) checked fsModeOwnerWrite for a
write-mode open and fsModeOwnerRead for opendir()-ing a DIRECTORY, but
NEVER checked any read bit at all for a plain FILE opened oReadOnly or
oReadWrite -- any caller could read any readable-by-someone file's
CONTENTS regardless of mode, since nothing gated it. Proven here entirely
from KTEST (unlike the cross-uid case, which needed a live process,
see grouptest.c's own "other-fallback-denied-ok"): chmod 0000 denies even
the file's OWNER its own read, exactly like coreVfsMkdirAndCreateRequire
ParentWrite already proved for root's own directory-write bit -- no
superuser/owner bypass either way. */
KTEST(coreVfsResolveOpenRequiresFileReadPermission)
{
int fd = vfsOpen("/tmp/readf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
KT_EQ(t, vfsWrite(fd, "x", 1), 1);
vfsClose(fd);
KT_EQ(t, vfsChmod("/tmp/readf", 0000), 0); /* no bits at all, even owner */
KT_EQ(t, vfsOpen("/tmp/readf", oReadOnly), -1);
KT_EQ(t, vfsOpen("/tmp/readf", oReadWrite), -1);
KT_EQ(t, vfsChmod("/tmp/readf", 0200), 0); /* owner-write only, no read */
KT_EQ(t, vfsOpen("/tmp/readf", oReadOnly), -1);
int wfd2 = vfsOpen("/tmp/readf", oWriteOnly);
KT_GE(t, wfd2, 0); /* write-only still works -- the two bits are independent */
vfsClose(wfd2);
KT_EQ(t, vfsChmod("/tmp/readf", fsModeDefaultFile), 0);
int rfd2 = vfsOpen("/tmp/readf", oReadOnly);
KT_GE(t, rfd2, 0);
vfsClose(rfd2);
vfsUnlink("/tmp/readf");
}
/* chmod on a missing path, or on the never-writable ustar-loaded /bin
content, is rejected exactly like every other tmpfs mutation already
gates on node->writable (vfsUnlink, vfsWrite's write path) -- chmod is a
metadata mutation too, so it obeys the same rule. Also proves every
ustar-loaded binary really does get the owner-exec bit by default
(vfs.c's ustarAdd), the fact loadImageByPath/jobctlSpawn's own exec-
permission check depends on for every existing spawnUser/userSpawn call
in this kernel to keep working at all. */
KTEST(coreVfsChmodRejectsMissingAndReadonlyBin)
{
KT_EQ(t, vfsChmod("/tmp/doesNotExist", 0644), -1);
KT_EQ(t, vfsChmod("/bin/false", 0777), -1);
FbStat st;
KT_EQ(t, vfsStat("/bin/false", &st), 0);
KT_EQ(t, st.mode, fsModeDefaultExec);
}
/* Same round trip on the persistent SFS mount, PLUS the "survives
unmount+remount" proof every other SFS-persisted field (mtime/ctime,
FALA 39) already has -- mode genuinely lives on disk, not just in the
in-memory inode cache. */
KTEST(coreVfsChmodSfsPersistsAcrossRemount)
{
KT_EQ(t, vfsMkdir("/mnt/chmodd"), 0);
int fd = vfsOpen("/mnt/chmodd/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/mnt/chmodd/f", &st), 0);
KT_EQ(t, st.mode, fsModeDefaultFile);
KT_EQ(t, vfsChmod("/mnt/chmodd/f", 0400), 0);
KT_EQ(t, vfsOpen("/mnt/chmodd/f", oWriteOnly), -1);
sfsUnmount();
KT_TRUE(t, sfsMount());
KT_EQ(t, vfsStat("/mnt/chmodd/f", &st), 0);
KT_EQ(t, st.mode, 0400u);
KT_EQ(t, vfsOpen("/mnt/chmodd/f", oWriteOnly), -1);
KT_EQ(t, vfsChmod("/mnt/chmodd/f", fsModeDefaultFile), 0);
int wfd = vfsOpen("/mnt/chmodd/f", oWriteOnly);
KT_GE(t, wfd, 0);
vfsClose(wfd);
vfsUnlink("/mnt/chmodd/f");
vfsRmdir("/mnt/chmodd");
}
/* Real Unix owner/group/other permission algorithm (vfs.c's vfsPermitted)
-- pure-logic coverage of the whole decision matrix, since the function
takes every credential as an explicit argument instead of reading a live
thread (see its own doc comment). Deliberately includes uid 0 on BOTH
sides of the owner/other split (coreVfsPermittedUidZeroIsNotASuperuser
below) -- uid 0 gets no special treatment at all, the one property that
distinguishes this from real Unix's root and the exact regression this
test guards: an earlier version of vfsPermitted let uid 0 bypass every
bit unconditionally, which silently broke every OTHER permission test in
this file (coreVfsChmodTmpfsWritePermission,
coreVfsChmodSfsPersistsAcrossRemount both run as ktestRunAll's own idle
thread, uid 0) the moment chmod took away the write bit on a file that
same uid-0 caller had just created. Every "hits the other branch" case
below deliberately gives a MISMATCHED gid too (not just a mismatched
uid) -- otherwise an incidental gid match would silently redirect the
check into the new group branch instead of exercising other at all. */
KTEST(coreVfsPermittedOwnerMatchChecksOwnerBitOnly)
{
/* Owner-uid match short-circuits before gid is ever consulted, so gid
values here are deliberately irrelevant -- 0 on both sides. */
/* Owner bit set, other bit clear: an owning caller passes. */
KT_TRUE(t, vfsPermitted(0600, 1000, 0, 1000, 0, fsModeOwnerWrite));
/* Owner bit clear, other bit set: an owning caller still fails --
owning the file never grants what the OTHER bits happen to allow. */
KT_FALSE(t, vfsPermitted(0004, 1000, 0, 1000, 0, fsModeOwnerWrite));
}
KTEST(coreVfsPermittedNonOwnerChecksOtherBitOnly)
{
/* Other-write set, owner-write clear, DIFFERENT uid AND gid: passes on
the other bit even though the owner bit alone would refuse. */
KT_TRUE(t, vfsPermitted(0002, 1000, 10, 2000, 20, fsModeOwnerWrite));
/* Neither bit helps a stranger if the other bit is the one clear. */
KT_FALSE(t, vfsPermitted(0600, 1000, 10, 2000, 20, fsModeOwnerWrite));
KT_FALSE(t, vfsPermitted(0000, 1000, 10, 2000, 20, fsModeOwnerExec));
}
KTEST(coreVfsPermittedUidZeroIsNotASuperuser)
{
/* uid 0 as OWNER: governed by the owner bit exactly like any other
uid, matching every existing chmod test's own assumption (every
boot-time process, including ktestRunAll itself, starts at uid 0). */
KT_TRUE(t, vfsPermitted(0600, 0, 0, 0, 0, fsModeOwnerWrite));
KT_FALSE(t, vfsPermitted(0000, 0, 0, 0, 0, fsModeOwnerWrite));
/* uid 0 as a NON-owner (and non-group-member): governed by the other
bit, no bypass at all -- a mode of 0 denies even uid 0 against
someone else's file. */
KT_FALSE(t, vfsPermitted(0700, 1000, 10, 0, 20, fsModeOwnerWrite));
KT_TRUE(t, vfsPermitted(0702, 1000, 10, 0, 20, fsModeOwnerWrite));
/* And symmetrically: owning a file as a non-zero uid is not somehow
MORE privileged than uid 0 owning one -- same algorithm either way. */
KT_FALSE(t, vfsPermitted(0600, 42, 10, 0, 20, fsModeOwnerWrite));
}
/* Every fsModeOwner/fsModeOther pair used by a real choke point
(resolveOpen's write check, loadImageByPath/jobctlSpawn's exec check)
round-trips through the exact bit-shift vfsPermitted derives one from
the other with -- not just fsModeOwnerWrite from the test above. */
KTEST(coreVfsPermittedExecBitFollowsSameOwnerOtherSplit)
{
KT_TRUE(t, vfsPermitted(fsModeOtherExec, 1, 10, 2, 20, fsModeOwnerExec));
KT_FALSE(t, vfsPermitted(fsModeOwnerExec, 1, 10, 2, 20, fsModeOwnerExec));
KT_TRUE(t, vfsPermitted(fsModeOwnerExec, 1, 10, 1, 10, fsModeOwnerExec));
}
/* The new middle branch: a caller whose UID does not match the owner but
whose GID matches the node's group is governed by the fsModeGroup* bit
(wantOwnerBit >> 3), never falling all the way through to other -- and a
group bit set does NOT imply the other bit is, proving this is a
genuinely separate check, not just "not owner" collapsing to one
fallback like the pre-group-model version of this function used to. */
KTEST(coreVfsPermittedGroupMatchChecksGroupBitOnly)
{
/* Group-write set, owner/other-write clear: different uid, same gid. */
KT_TRUE(t, vfsPermitted(0020, 1000, 10, 2000, 10, fsModeOwnerWrite));
/* Group bit clear: a gid match alone grants nothing by itself. */
KT_FALSE(t, vfsPermitted(0602, 1000, 10, 2000, 10, fsModeOwnerWrite));
/* Every fsModeOwner/fsModeGroup pair round-trips through the >>3 shift,
not just write above. */
KT_TRUE(t, vfsPermitted(fsModeGroupExec, 1, 10, 2, 10, fsModeOwnerExec));
/* Owner-uid match takes priority over a coincidental gid match: the
caller here is BOTH the owner AND shares the owner's own gid, but the
decision must still come from the owner bit, not group. */
KT_TRUE(t, vfsPermitted(0100, 1, 10, 1, 10, fsModeOwnerExec));
KT_FALSE(t, vfsPermitted(0010, 1, 10, 1, 10, fsModeOwnerExec));
}
/* Real owner identity (fs.h's FbStat.uid, vfs.c's vfsUid): a fresh tmpfs
node is stamped with whoever created it -- ktestRunAll always runs as
the permanent idle thread (id 0, uid 0, "root"), the same context
schedTest.c's own pgid-default tests already document relying on, so
every node created here is root-owned by construction. */
KTEST(coreVfsTmpfsNodeStampsCreatorAsOwner)
{
int fd = vfsOpen("/tmp/uidf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/tmp/uidf", &st), 0);
KT_EQU(t, st.uid, 0u);
vfsUnlink("/tmp/uidf");
}
/* vfsOwnerOrRoot pure predicate (vfs.c/h): same "explicit-argument, no live
thread" test shape as vfsPermittedOwnerMatch/NonOwner/UidZero above --
covers all four quadrants (self-match as a non-root uid, root bypass on
a foreign file, root owning and calling as itself, and a plain stranger
denied against both a root-owned and a non-root-owned file). Needed
because KTEST always runs as the permanent idle thread's own uid 0
(coreVfsTmpfsNodeStampsCreatorAsOwner's own doc comment), so no KTEST can
ever construct a real vfsChmod/vfsChown call with a non-root caller uid
to exercise the "denied" side directly -- that side is only provable
live (identtest.c's chmod-nonowner-denied-ok/chown-nonroot-denied-ok). */
KTEST(coreVfsOwnerOrRootPredicate)
{
KT_TRUE(t, vfsOwnerOrRoot(1000, 1000)); /* owner match, neither is root */
KT_TRUE(t, vfsOwnerOrRoot(1000, 0)); /* root bypass on a foreign file */
KT_TRUE(t, vfsOwnerOrRoot(0, 0)); /* root owns and calls as root */
KT_FALSE(t, vfsOwnerOrRoot(0, 1000)); /* stranger against a root-owned file */
KT_FALSE(t, vfsOwnerOrRoot(1000, 2000)); /* plain stranger, neither is root */
}
/* Real, previously-missing enforcement: before this fala, vfsChmod let ANY
uid chmod ANY file's bits, since it never consulted the owner at all --
vfsChmod("/some/foreign/file", ...) always silently succeeded regardless
of who called it. This proves the ALLOWED side of the fix at the real
vfs.c choke point (the denied side can only be proven live, see the
predicate test above): root may still chmod a file it does NOT own, via
the new vfsChown reassigning ownership away from the creator first. */
KTEST(coreVfsChmodRootBypassesForeignOwnership)
{
int fd = vfsOpen("/tmp/chmodForeign", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsChown("/tmp/chmodForeign", 4242), 0);
FbStat st;
KT_EQ(t, vfsStat("/tmp/chmodForeign", &st), 0);
KT_EQU(t, st.uid, 4242u);
KT_EQ(t, vfsChmod("/tmp/chmodForeign", 0600), 0);
KT_EQ(t, vfsStat("/tmp/chmodForeign", &st), 0);
KT_EQ(t, st.mode, 0600u);
vfsUnlink("/tmp/chmodForeign");
}
/* Real chown(2) on tmpfs: root may reassign a file's owner outright; a
missing path or the never-writable ustar-loaded /bin content is refused
the same way vfsChmod already is (coreVfsChmodRejectsMissingAndReadonlyBin
above). */
KTEST(coreVfsChownTmpfsReassignsOwner)
{
int fd = vfsOpen("/tmp/chownf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/tmp/chownf", &st), 0);
KT_EQU(t, st.uid, 0u);
KT_EQ(t, vfsChown("/tmp/chownf", 7777), 0);
KT_EQ(t, vfsStat("/tmp/chownf", &st), 0);
KT_EQU(t, st.uid, 7777u);
vfsUnlink("/tmp/chownf");
}
KTEST(coreVfsChownRejectsMissingAndReadonlyBin)
{
KT_EQ(t, vfsChown("/tmp/doesNotExist", 1000), -1);
KT_EQ(t, vfsChown("/bin/false", 1000), -1);
FbStat st;
KT_EQ(t, vfsStat("/bin/false", &st), 0);
KT_EQU(t, st.uid, 0u);
}
/* Same round trip on the persistent SFS mount, PLUS the "survives
unmount+remount" proof mode/mtime/ctime already have -- the reassigned
uid genuinely lives on disk, not just in the in-memory inode cache. */
KTEST(coreVfsChownSfsPersistsAcrossRemount)
{
KT_EQ(t, vfsMkdir("/mnt/chownd"), 0);
int fd = vfsOpen("/mnt/chownd/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/mnt/chownd/f", &st), 0);
KT_EQU(t, st.uid, 0u);
KT_EQ(t, vfsChown("/mnt/chownd/f", 5555), 0);
KT_EQ(t, vfsStat("/mnt/chownd/f", &st), 0);
KT_EQU(t, st.uid, 5555u);
sfsUnmount();
KT_TRUE(t, sfsMount());
KT_EQ(t, vfsStat("/mnt/chownd/f", &st), 0);
KT_EQU(t, st.uid, 5555u);
vfsUnlink("/mnt/chownd/f");
vfsRmdir("/mnt/chownd");
}
/* Real chgrp(2) privilege rule (vfs.c's vfsChgrp): unlike vfsChown
(root-only, full stop), the file's own owner may ALSO reassign the
group -- but only to their OWN current gid (currentThreadGid()), never
an arbitrary one, since this kernel tracks exactly one gid per thread.
KTEST always runs as the idle thread's own (uid 0, gid 0), so the
caller here genuinely IS the owner of a freshly created file, letting
this test exercise the "owner, not root" half of vfsChgrp's dual
privilege check -- setting the group to 0 (this thread's own gid)
succeeds, setting it to anything else fails, exactly matching
grouptest.c's live chgrp-owner-self-gid-ok/chgrp-owner-arbitrary-
denied-ok pair, but provable here without a live cross-uid process
since "owner but not root" only requires being non-root, which this
thread already permanently is not (root bypass is tested separately by
coreVfsChgrpRootReassignsArbitraryGroup below, since root-vs-owner needs
two different DENIED-side outcomes root's own uid-0 bypass would hide). */
KTEST(coreVfsChgrpOwnerOnlyMaySetOwnCurrentGid)
{
int fd = vfsOpen("/tmp/chgrpOwner", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/tmp/chgrpOwner", &st), 0);
KT_EQU(t, st.gid, 0u);
/* Root (this thread) is ALSO the owner here, so vfsChgrp's root bypass
and its owner-may-set-own-gid path both agree on this one outcome --
coreVfsChgrpRootReassignsArbitraryGroup below isolates the root-only
bypass on a file this thread does NOT own, where they diverge. */
KT_EQ(t, vfsChgrp("/tmp/chgrpOwner", 0), 0);
KT_EQ(t, vfsStat("/tmp/chgrpOwner", &st), 0);
KT_EQU(t, st.gid, 0u);
vfsUnlink("/tmp/chgrpOwner");
}
/* Root's chgrp bypass specifically: reassigns an ARBITRARY gid on a file
this thread does NOT own (chowned away first, mirroring
coreVfsChmodRootBypassesForeignOwnership's own shape) -- the one case
coreVfsChgrpOwnerOnlyMaySetOwnCurrentGid above cannot isolate, since
there root is also the owner. */
KTEST(coreVfsChgrpRootReassignsArbitraryGroup)
{
int fd = vfsOpen("/tmp/chgrpForeign", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsChown("/tmp/chgrpForeign", 4242), 0);
KT_EQ(t, vfsChgrp("/tmp/chgrpForeign", 9999), 0);
FbStat st;
KT_EQ(t, vfsStat("/tmp/chgrpForeign", &st), 0);
KT_EQU(t, st.gid, 9999u);
vfsUnlink("/tmp/chgrpForeign");
}
/* Same round trip on the persistent SFS mount, PLUS the "survives
unmount+remount" proof every other SFS metadata field already has --
mirrors coreVfsChownSfsPersistsAcrossRemount exactly, one field over. */
KTEST(coreVfsChgrpSfsPersistsAcrossRemount)
{
KT_EQ(t, vfsMkdir("/mnt/chgrpd"), 0);
int fd = vfsOpen("/mnt/chgrpd/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
FbStat st;
KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0);
KT_EQU(t, st.gid, 0u);
KT_EQ(t, vfsChgrp("/mnt/chgrpd/f", 6666), 0);
KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0);
KT_EQU(t, st.gid, 6666u);
sfsUnmount();
KT_TRUE(t, sfsMount());
KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0);
KT_EQU(t, st.gid, 6666u);
vfsUnlink("/mnt/chgrpd/f");
vfsRmdir("/mnt/chgrpd");
}
/* Real directory-write enforcement (vfs.c's vfsMkdir/resolveOpen create
branch), the gap FALA 44 explicitly flagged as stored-but-unenforced --
creating a NEW entry (file or subdirectory) needs the owner-write bit on
the PARENT directory, checked against the CALLING thread's own uid, not
just "the parent is a writable tmpfs node". KTEST always runs as the
permanent idle thread's own uid 0 (coreVfsTmpfsNodeStampsCreatorAsOwner's
own doc comment), and vfsPermitted has no uid-0 bypass
(coreVfsPermittedUidZeroIsNotASuperuser), so removing root's OWN
owner-write bit on a directory it owns genuinely denies root itself --
the same "no superuser bypass" story chmod's own tests already prove for
files, now proven for directories too, entirely from KTEST without
needing a live non-root caller. */
KTEST(coreVfsMkdirAndCreateRequireParentWrite)
{
KT_EQ(t, vfsMkdir("/tmp/dwParent"), 0);
KT_EQ(t, vfsChmod("/tmp/dwParent", 0555), 0); /* r-x, no owner-write */
KT_EQ(t, vfsMkdir("/tmp/dwParent/child"), -1);
KT_EQ(t, vfsOpen("/tmp/dwParent/f", oWriteOnly | oCreate), -1);
KT_EQ(t, vfsChmod("/tmp/dwParent", fsModeDefaultDir), 0);
KT_EQ(t, vfsMkdir("/tmp/dwParent/child"), 0);
int fd = vfsOpen("/tmp/dwParent/f", oWriteOnly | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
vfsUnlink("/tmp/dwParent/f");
vfsRmdir("/tmp/dwParent/child");
vfsRmdir("/tmp/dwParent");
}
/* Real unlink(2)/rmdir(2) semantics: removing an entry needs write
permission on the PARENT directory, never the entry's own mode -- proven
here by a file that is itself still perfectly writable (0644) but whose
PARENT has lost its own write bit, matching real Unix's own "you can
delete a read-only file if you can write its directory, and you canNOT
delete a writable file if you can't" rule the file-level mode never
governs on its own. */
KTEST(coreVfsUnlinkAndRmdirRequireParentWrite)
{
KT_EQ(t, vfsMkdir("/tmp/dwParent2"), 0);
int fd = vfsOpen("/tmp/dwParent2/f", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsMkdir("/tmp/dwParent2/sub"), 0);
KT_EQ(t, vfsChmod("/tmp/dwParent2", 0555), 0);
KT_EQ(t, vfsUnlink("/tmp/dwParent2/f"), -1);
KT_EQ(t, vfsRmdir("/tmp/dwParent2/sub"), -1);
KT_EQ(t, vfsChmod("/tmp/dwParent2", fsModeDefaultDir), 0);
KT_EQ(t, vfsUnlink("/tmp/dwParent2/f"), 0);
KT_EQ(t, vfsRmdir("/tmp/dwParent2/sub"), 0);
KT_EQ(t, vfsRmdir("/tmp/dwParent2"), 0);
}
/* Real POSIX search permission (vfs.c's lookupNode, checked on every
directory ANCESTOR a path walks through): removing the exec bit on a
directory makes every path THROUGH it fail, even though the target file
inside it never changed at all -- proven by chmod'ing the ancestor away
and back around an otherwise-untouched file. */
KTEST(coreVfsLookupRequiresAncestorExec)
{
KT_EQ(t, vfsMkdir("/tmp/dxAncestor"), 0);
int fd = vfsOpen("/tmp/dxAncestor/leaf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
KT_EQ(t, vfsWrite(fd, "x", 1), 1);
vfsClose(fd);
KT_EQ(t, vfsChmod("/tmp/dxAncestor", 0600), 0); /* rw-, no exec at all */
FbStat st;
KT_EQ(t, vfsStat("/tmp/dxAncestor/leaf", &st), -1);
KT_EQ(t, vfsOpen("/tmp/dxAncestor/leaf", oReadOnly), -1);
KT_EQ(t, vfsChmod("/tmp/dxAncestor", fsModeDefaultDir), 0);
KT_EQ(t, vfsStat("/tmp/dxAncestor/leaf", &st), 0);
int rfd = vfsOpen("/tmp/dxAncestor/leaf", oReadOnly);
KT_GE(t, rfd, 0);
vfsClose(rfd);
vfsUnlink("/tmp/dxAncestor/leaf");
vfsRmdir("/tmp/dxAncestor");
}
/* Real distinction between search (exec) and list (read) on a directory --
the subtlest part of this feature, and the one most likely to be
accidentally conflated: a directory with exec but no read lets a caller
open a KNOWN child by name (resolveOpen never needs read on `cur` for
that, only exec on it as an ANCESTOR of the leaf) while refusing to open
the directory itself for listing (vfsGetdents). Mirrors dirpermtest.c's
own live list-denied-ok/search-without-list-ok pair, but here entirely
from KTEST since both sides are root-vs-its-own-directory, not root-vs-
another-uid. */
KTEST(coreVfsOpendirRequiresDirReadNotJustExec)
{
KT_EQ(t, vfsMkdir("/tmp/dxListable"), 0);
int fd = vfsOpen("/tmp/dxListable/known", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsChmod("/tmp/dxListable", 0311), 0); /* -wx, no owner-read */
KT_EQ(t, vfsOpen("/tmp/dxListable", oReadOnly), -1);
int kfd = vfsOpen("/tmp/dxListable/known", oReadOnly);
KT_GE(t, kfd, 0);
vfsClose(kfd);
KT_EQ(t, vfsChmod("/tmp/dxListable", fsModeDefaultDir), 0);
int dd = vfsOpen("/tmp/dxListable", oReadOnly);
KT_GE(t, dd, 0);
FbDirent ents[8];
KT_TRUE(t, vfsGetdents(dd, ents, sizeof(ents)) >= (int64)sizeof(FbDirent));
vfsClose(dd);
vfsUnlink("/tmp/dxListable/known");
vfsRmdir("/tmp/dxListable");
}
/* Same three checks as the four tmpfs tests directly above (parent-write
for create/remove, ancestor-exec for traversal, dir-read-vs-exec for
listing), run once through the SFS dispatch branch of vfs.c's own
choke points (resolveOpen/vfsMkdir/vfsUnlink/vfsRmdir/lookupNode's
isSfsPath fork) instead of sfs.c's raw primitives directly -- sfsTest.c's
sfsCreateNodeRequiresParentWrite/sfsRemoveRequiresParentWrite/
sfsLookupRequiresAncestorExec already cover the on-disk layer itself;
this is the "does the /mnt dispatch actually reach it" parity proof, the
same relationship coreVfsChmodSfsPersistsAcrossRemount already has to
its own tmpfs sibling above. */
KTEST(coreVfsSfsDirectoryPermissionParity)
{
KT_EQ(t, vfsMkdir("/mnt/dpTop"), 0);
int fd = vfsOpen("/mnt/dpTop/leaf", oReadWrite | oCreate);
KT_GE(t, fd, 0);
vfsClose(fd);
KT_EQ(t, vfsChmod("/mnt/dpTop", 0555), 0);
KT_EQ(t, vfsOpen("/mnt/dpTop/newfile", oWriteOnly | oCreate), -1);
KT_EQ(t, vfsUnlink("/mnt/dpTop/leaf"), -1);
KT_EQ(t, vfsChmod("/mnt/dpTop", 0600), 0);
FbStat st;
KT_EQ(t, vfsStat("/mnt/dpTop/leaf", &st), -1);
KT_EQ(t, vfsChmod("/mnt/dpTop", 0311), 0);
KT_EQ(t, vfsOpen("/mnt/dpTop", oReadOnly), -1);
int kfd = vfsOpen("/mnt/dpTop/leaf", oReadOnly);
KT_GE(t, kfd, 0);
vfsClose(kfd);
KT_EQ(t, vfsChmod("/mnt/dpTop", fsModeDefaultDir), 0);
KT_EQ(t, vfsUnlink("/mnt/dpTop/leaf"), 0);
KT_EQ(t, vfsRmdir("/mnt/dpTop"), 0);
}
+89 -1
View File
@@ -7,6 +7,15 @@
#define elfTypeExec 2 #define elfTypeExec 2
#define elfMachineX8664 62 #define elfMachineX8664 62
/* Standard ELF64 Elf64ProgramHeader.flags bits (p_flags) -- PF_X/PF_W/PF_R
by their real numeric values, not this kernel's own invention. PF_R is
never consulted below: every mapping pagingMapUserRangeExact creates is
already readable by virtue of pageUser alone (x86_64 has no separate
read-disable outside SMAP/PKU, neither of which this kernel uses), so a
segment missing PF_R would be indistinguishable from one that has it. */
#define elfFlagExecute 1
#define elfFlagWrite 2
static bool validHeader(const Elf64Header* header) static bool validHeader(const Elf64Header* header)
{ {
if (header->ident[0] != 0x7F || header->ident[1] != 'E' || if (header->ident[0] != 0x7F || header->ident[1] != 'E' ||
@@ -21,6 +30,26 @@ static bool validHeader(const Elf64Header* header)
return true; return true;
} }
/* Translates one ELF64 program header's own p_flags into the extra
paging.h leaf-PTE bits pagingMapUserRangeExact needs to map that exact
segment with real per-segment W^X: writable iff PF_W was set, and
pageNoExecute UNLESS PF_X was set (never both writable and executable
at once for a segment that asked for neither, or for one that asked
for only PF_W -- data/bss segments -- which is the whole point).
Pulled out as its own pure function, no paging/scheduler/VFS involved,
so elfTest.c can exercise every PF_X/PF_W combination deterministically
before the scheduler even starts, the same way this file's other pure
address-arithmetic helpers (elfSlotBase and friends) already are. */
uint64 elfSegmentPagingFlags(uint32 segmentFlags)
{
uint64 flags = 0;
if (segmentFlags & elfFlagWrite)
flags |= pageWrite;
if (!(segmentFlags & elfFlagExecute))
flags |= pageNoExecute;
return flags;
}
uint64 elfLoad(const void* image, uint64 imageSize) uint64 elfLoad(const void* image, uint64 imageSize)
{ {
const Elf64Header* header = (const Elf64Header*)image; const Elf64Header* header = (const Elf64Header*)image;
@@ -39,7 +68,8 @@ uint64 elfLoad(const void* image, uint64 imageSize)
if (segment->type != programTypeLoad) if (segment->type != programTypeLoad)
continue; continue;
if (!pagingMapUserRange(segment->virtualAddr, segment->memorySize)) { uint64 mapFlags = elfSegmentPagingFlags(segment->flags);
if (!pagingMapUserRangeExact(segment->virtualAddr, segment->memorySize, mapFlags)) {
kprintf("[elf] failed to map segment at %p\n", (void*)segment->virtualAddr); kprintf("[elf] failed to map segment at %p\n", (void*)segment->virtualAddr);
return 0; return 0;
} }
@@ -49,3 +79,61 @@ uint64 elfLoad(const void* image, uint64 imageSize)
return header->entry; return header->entry;
} }
uint64 elfSlotBase(uint64 entry)
{
return entry & ~(elfSlotSize - 1);
}
uint64 elfSlotStackTop(uint64 entry)
{
return elfSlotBase(entry) + elfSlotStackOffset;
}
uint64 elfSlotBrkStart(uint64 entry)
{
return elfSlotBase(entry) + elfSlotBrkOffset;
}
bool elfBuildInitialStack(uint64 stackTop, uint64 stackSize, const char* const* argv,
int argc, uint64* outRsp)
{
if (argc < 0 || argc > elfMaxArgs)
return false;
uint64 stackBottom = stackTop - stackSize;
uint64 argvAddr[elfMaxArgs];
uint64 cursor = stackTop;
/* Strings first, growing downward from the top of the stack -- each
one's address is remembered so the pointer array below can point at
it once every string is placed. */
for (int i = argc - 1; i >= 0; i--) {
uint64 len = strlen(argv[i]) + 1;
if (cursor - stackBottom < len)
return false;
cursor -= len;
memcpy((void*)cursor, argv[i], len);
argvAddr[i] = cursor;
}
/* argc slot + argv[0..argc-1] + a NULL terminator, SysV-style. Rounded
down to 16 bytes because that is where _start (crt.S) expects rsp to
sit at process entry (so that its own later `call main` leaves main
looking at the usual post-call rsp%16==8). */
uint64 fixedAreaBytes = (uint64)(argc + 2) * sizeof(uint64);
if (cursor - stackBottom < fixedAreaBytes)
return false;
uint64 base = (cursor - fixedAreaBytes) & ~15ull;
if (base < stackBottom)
return false;
uint64* slots = (uint64*)base;
slots[0] = (uint64)argc;
for (int i = 0; i < argc; i++)
slots[1 + i] = argvAddr[i];
slots[1 + argc] = 0;
*outRsp = base;
return true;
}
+179
View File
@@ -0,0 +1,179 @@
#include "ktest.h"
#include "elf.h"
#include "heap.h"
#include "string.h"
#include "paging.h"
/*
* elfSlotBase/elfSlotStackTop/elfSlotBrkStart are pure address arithmetic
* (no VFS, no scheduler, no paging), so unlike exec()/spawn() themselves
* (which only run for real via a live boot demo -- see proctest.c and
* tools/ci-test.sh needles execstart-ok/spawn-wait-ok/exec-pid-preserved-ok)
* this math is fully coverable here, deterministically, before the
* scheduler even starts.
*/
KTEST(elfSlotBaseFloorsToSlotBoundary)
{
KT_EQU(t, elfSlotBase(0x40000000ull), 0x40000000ull);
KT_EQU(t, elfSlotBase(0x40001000ull), 0x40000000ull);
KT_EQU(t, elfSlotBase(0x4FFFFFFFull), 0x40000000ull);
KT_EQU(t, elfSlotBase(0x50000000ull), 0x50000000ull);
KT_EQU(t, elfSlotBase(0xB0000ABCull), 0xB0000000ull);
}
KTEST(elfSlotStackAndBrkMatchBootTimeConvention)
{
/* kmain.c's spawnUser calls hardcode stackTop=base+0x08000000 and
brk=base+0x04000000 for every binary already spawned at boot (e.g.
sbrktest: base 0x60000000, stackTop 0x68000000, brk 0x64000000) --
the derived values must agree exactly, since exec()/spawn() reuse
the same slots and a mismatch would map a fresh stack on top of
code/data or vice versa. */
KT_EQU(t, elfSlotStackTop(0x60001000ull), 0x68000000ull);
KT_EQU(t, elfSlotBrkStart(0x60001000ull), 0x64000000ull);
KT_EQU(t, elfSlotStackTop(0x40000abcull), 0x48000000ull);
KT_EQU(t, elfSlotBrkStart(0x40000abcull), 0x44000000ull);
/* An entry point right at the top of its slot still derives the same
slot-relative stack/brk as one near the bottom. */
KT_EQU(t, elfSlotStackTop(0x0FFFFFFFull), 0x08000000ull);
KT_EQU(t, elfSlotBrkStart(0x0FFFFFFFull), 0x04000000ull);
}
/*
* elfBuildInitialStack (added alongside argv/argc support for exec()/
* spawn(): copyUserArgv in syscall.c, crt.S's _start now reading argc/argv
* off the stack instead of calling main() with nothing) does not care
* whether the memory it writes into is a real user slot or not -- it just
* needs [stackTop-stackSize, stackTop) to already be valid, writable
* memory, which a plain kmalloc buffer satisfies just as well as a
* pagingMapUserRange'd user stack. That makes the exact byte layout fully
* testable here, deterministically, without any live process at all --
* the live proof (kmain.c's proctest.c spawning argtest.c with a specific
* argv, needles "proctest: spawn with argv ok"/"argtest: argv-ok" in
* tools/ci-test.sh) only proves crt.S reads the same layout back
* correctly, not what the layout itself has to look like.
*/
KTEST(elfBuildInitialStackLaysOutArgcArgvAndStringsCorrectly)
{
uint64 stackSize = 4096;
uint8* stack = (uint8*)kmalloc(stackSize);
uint64 stackTop = (uint64)stack + stackSize;
const char* argv[3] = { "/bin/argtest", "alpha", "beta" };
uint64 rsp = 0;
KT_TRUE(t, elfBuildInitialStack(stackTop, stackSize, argv, 3, &rsp));
/* SysV process entry: rsp must land inside the stack it was given and
be 16-byte aligned so _start's own `call main` leaves main looking
at the usual post-call rsp%16==8. */
KT_GE(t, rsp, (uint64)stack);
KT_LT(t, rsp, stackTop);
KT_EQU(t, rsp % 16, 0);
const uint64* slots = (const uint64*)rsp;
KT_EQ(t, (int64)slots[0], 3);
KT_STREQ(t, (const char*)slots[1], "/bin/argtest");
KT_STREQ(t, (const char*)slots[2], "alpha");
KT_STREQ(t, (const char*)slots[3], "beta");
KT_EQU(t, slots[4], 0);
kfree(stack);
}
KTEST(elfBuildInitialStackWithZeroArgcStillTerminatesArgv)
{
uint64 stackSize = 256;
uint8* stack = (uint8*)kmalloc(stackSize);
uint64 stackTop = (uint64)stack + stackSize;
uint64 rsp = 0;
KT_TRUE(t, elfBuildInitialStack(stackTop, stackSize, nullptr, 0, &rsp));
KT_EQU(t, rsp % 16, 0);
const uint64* slots = (const uint64*)rsp;
KT_EQ(t, (int64)slots[0], 0);
KT_EQU(t, slots[1], 0);
kfree(stack);
}
KTEST(elfBuildInitialStackRejectsArgcAboveElfMaxArgs)
{
uint64 stackSize = 4096;
uint8* stack = (uint8*)kmalloc(stackSize);
uint64 stackTop = (uint64)stack + stackSize;
const char* argv[1] = { "x" };
uint64 rsp = 0;
KT_FALSE(t, elfBuildInitialStack(stackTop, stackSize, argv, elfMaxArgs + 1, &rsp));
KT_FALSE(t, elfBuildInitialStack(stackTop, stackSize, argv, -1, &rsp));
kfree(stack);
}
KTEST(elfBuildInitialStackRejectsArgvTooBigForStack)
{
/* Deliberately far too small to hold even the fixed argc/argv/NULL
area for a single argument, let alone the string itself -- must fail
cleanly (false, *outRsp untouched) rather than write below
stackTop-stackSize. */
uint64 stackSize = 8;
uint8* stack = (uint8*)kmalloc(stackSize);
uint64 stackTop = (uint64)stack + stackSize;
const char* argv[1] = { "/bin/argtest" };
uint64 rsp = 0xDEADBEEFull;
KT_FALSE(t, elfBuildInitialStack(stackTop, stackSize, argv, 1, &rsp));
KT_EQU(t, rsp, 0xDEADBEEFull);
kfree(stack);
}
/*
* elfSegmentPagingFlags is elfLoad's own per-segment W^X decision reduced
* to pure arithmetic on a p_flags value -- no paging table, no live
* process, so every PF_X/PF_W combination a real ELF linker could ever
* emit is checked here directly, deterministically, rather than only
* inferred indirectly from whether user/elftest.c's live boot demo (below)
* happens to fault where expected.
*/
KTEST(elfSegmentPagingFlagsReadExecuteIsWritableNeverAndAlwaysExecutable)
{
/* A real .text/.rodata PT_LOAD segment: PF_R|PF_X (5). Must come back
with neither pageWrite nor pageNoExecute -- read-only, executable. */
KT_EQU(t, elfSegmentPagingFlags(5), 0ull);
}
KTEST(elfSegmentPagingFlagsReadWriteIsWritableAndNeverExecutable)
{
/* A real .data/.bss PT_LOAD segment: PF_R|PF_W (6). Must come back
with pageWrite set and pageNoExecute set -- the actual W^X split
this session's change exists to enforce. */
KT_EQU(t, elfSegmentPagingFlags(6), pageWrite | pageNoExecute);
}
KTEST(elfSegmentPagingFlagsReadOnlyIsNeitherWritableNorExecutable)
{
/* PF_R alone (4) -- a real ELF can emit a read-only, non-executable
segment (e.g. .rodata split into its own PT_LOAD by some linkers,
even though this kernel's own user.ld keeps it with .text). Must
still come back non-writable AND non-executable -- PF_X is the only
bit that ever clears pageNoExecute, never PF_R. */
KT_EQU(t, elfSegmentPagingFlags(4), pageNoExecute);
}
KTEST(elfSegmentPagingFlagsReadWriteExecuteIsWritableAndExecutable)
{
/* PF_R|PF_W|PF_X (7) -- every real linker refuses to emit this for a
normal PT_LOAD, but the function must still do the honest thing
with it rather than assume it can never happen: writable (PF_W)
and executable (PF_X), i.e. neither hardening bit set. Confirms
PF_W and PF_X are decided independently of one another, not "if
not exactly PF_X, assume data". */
KT_EQU(t, elfSegmentPagingFlags(7), pageWrite);
}
+123
View File
@@ -0,0 +1,123 @@
#include "hmap.h"
#include "heap.h"
#include "string.h"
#include "klib.h"
uint64 hmapHashString(const char* key)
{
uint64 hash = 1469598103934665603UL; /* FNV-1a 64-bit offset basis */
for (const unsigned char* p = (const unsigned char*)key; *p; p++) {
hash ^= (uint64)*p;
hash *= 1099511628211UL; /* FNV-1a 64-bit prime */
}
return hash;
}
bool hmapInit(Hmap* h, usize bucketCount)
{
if (bucketCount == 0)
bucketCount = 16;
h->buckets = (HmapEntry**)kmalloc(bucketCount * sizeof(HmapEntry*));
if (!h->buckets)
return false;
for (usize i = 0; i < bucketCount; i++)
h->buckets[i] = nullptr;
h->bucketCount = bucketCount;
h->count = 0;
return true;
}
void hmapFree(Hmap* h)
{
for (usize i = 0; i < h->bucketCount; i++) {
HmapEntry* entry = h->buckets[i];
while (entry) {
HmapEntry* next = entry->next;
kfree(entry->key);
kfree(entry);
entry = next;
}
}
kfree(h->buckets);
h->buckets = nullptr;
h->bucketCount = 0;
h->count = 0;
}
static HmapEntry** findSlot(const Hmap* h, const char* key)
{
usize bucket = hmapHashString(key) % h->bucketCount;
HmapEntry** slot = &h->buckets[bucket];
while (*slot && strcmp((*slot)->key, key) != 0)
slot = &(*slot)->next;
return slot;
}
bool hmapSet(Hmap* h, const char* key, void* value)
{
HmapEntry** slot = findSlot(h, key);
if (*slot) {
(*slot)->value = value;
return true;
}
HmapEntry* entry = (HmapEntry*)kmalloc(sizeof(HmapEntry));
if (!entry)
return false;
usize keyLen = strlen(key);
entry->key = (char*)kmalloc(keyLen + 1);
if (!entry->key) {
kfree(entry);
return false;
}
strcpy(entry->key, key);
entry->value = value;
entry->next = nullptr;
*slot = entry;
h->count++;
return true;
}
void* hmapGet(const Hmap* h, const char* key)
{
HmapEntry** slot = findSlot(h, key);
return *slot ? (*slot)->value : nullptr;
}
bool hmapHas(const Hmap* h, const char* key)
{
return *findSlot(h, key) != nullptr;
}
bool hmapRemove(Hmap* h, const char* key)
{
HmapEntry** slot = findSlot(h, key);
if (!*slot)
return false;
HmapEntry* entry = *slot;
*slot = entry->next;
kfree(entry->key);
kfree(entry);
h->count--;
return true;
}
usize hmapCount(const Hmap* h)
{
return h->count;
}
void hmapForEach(const Hmap* h, bool (*visit)(const char* key, void* value, void* userData), void* userData)
{
for (usize i = 0; i < h->bucketCount; i++) {
for (HmapEntry* entry = h->buckets[i]; entry; entry = entry->next) {
if (!visit(entry->key, entry->value, userData))
return;
}
}
}
+98
View File
@@ -0,0 +1,98 @@
#include "ktest.h"
#include "hmap.h"
KTEST(hmapSetGetOverwrite)
{
Hmap h;
KT_TRUE(t, hmapInit(&h, 8));
KT_EQ(t, hmapCount(&h), 0);
int a = 1, b = 2;
KT_TRUE(t, hmapSet(&h, "alpha", &a));
KT_TRUE(t, hmapSet(&h, "beta", &b));
KT_EQ(t, hmapCount(&h), 2);
KT_EQP(t, hmapGet(&h, "alpha"), &a);
KT_EQP(t, hmapGet(&h, "beta"), &b);
KT_NULL(t, hmapGet(&h, "gamma"));
KT_TRUE(t, hmapHas(&h, "alpha"));
KT_FALSE(t, hmapHas(&h, "gamma"));
int c = 3;
KT_TRUE(t, hmapSet(&h, "alpha", &c)); /* overwrite, not a new entry */
KT_EQ(t, hmapCount(&h), 2);
KT_EQP(t, hmapGet(&h, "alpha"), &c);
hmapFree(&h);
}
KTEST(hmapRemove)
{
Hmap h;
hmapInit(&h, 4);
int v = 42;
hmapSet(&h, "key", &v);
KT_TRUE(t, hmapRemove(&h, "key"));
KT_EQ(t, hmapCount(&h), 0);
KT_NULL(t, hmapGet(&h, "key"));
KT_FALSE(t, hmapRemove(&h, "key")); /* already gone */
hmapFree(&h);
}
KTEST(hmapCollisionsChain)
{
/* force collisions with a tiny bucket count so every key lands in the
same chain, exercising the linked-list walk in findSlot */
Hmap h;
hmapInit(&h, 1);
int values[10];
char keys[10][8];
for (int i = 0; i < 10; i++) {
values[i] = i * 10;
keys[i][0] = 'k';
keys[i][1] = (char)('0' + i);
keys[i][2] = 0;
KT_TRUE(t, hmapSet(&h, keys[i], &values[i]));
}
KT_EQ(t, hmapCount(&h), 10);
for (int i = 0; i < 10; i++)
KT_EQP(t, hmapGet(&h, keys[i]), &values[i]);
hmapFree(&h);
}
KTEST(hmapHashIsDeterministicAndDistributes)
{
KT_EQU(t, hmapHashString("same"), hmapHashString("same"));
KT_NE(t, hmapHashString("alpha"), hmapHashString("beta"));
KT_NE(t, hmapHashString(""), hmapHashString("a"));
}
static bool sumVisitor(const char* key, void* value, void* userData)
{
(void)key;
int64* sum = (int64*)userData;
*sum += *(int*)value;
return true;
}
KTEST(hmapForEachVisitsAll)
{
Hmap h;
hmapInit(&h, 4);
int a = 1, b = 2, c = 3;
hmapSet(&h, "a", &a);
hmapSet(&h, "b", &b);
hmapSet(&h, "c", &c);
int64 sum = 0;
hmapForEach(&h, sumVisitor, &sum);
KT_EQ(t, sum, 6);
hmapFree(&h);
}
+125 -11
View File
@@ -5,6 +5,8 @@
#include "keyboard.h" #include "keyboard.h"
#include "sched.h" #include "sched.h"
#include "ports.h" #include "ports.h"
#include "signal.h"
#include "mmap.h"
static const char* exceptionNames[32] = { static const char* exceptionNames[32] = {
"divide by zero", "divide by zero",
@@ -52,24 +54,136 @@ static void panicOnException(Registers* regs)
__asm__ volatile("cli; hlt"); __asm__ volatile("cli; hlt");
} }
/* Runs at every single return-to-ring3 point this kernel has (a completed
syscall, a timer reschedule, any other IRQ resuming whoever it
interrupted) -- the one choke point isrDispatch funnels its result
through below, regardless of which of those three paths produced it.
`rsp` is whatever Registers* the caller is about to resume; if that
context is not ring 3 (a kernel thread, e.g. workerA/shell -- see
sched.h's userCodeSelector|3 vs kernelCodeSelector), signals are not a
concept that applies to it at all and this is a no-op. Otherwise
currentThread() is guaranteed to be exactly the owner of `rsp` (schedule()
already updated currentIndex before returning it; the syscall/other-IRQ
paths never changed currentIndex away from whoever trapped in), so
sigConsumePending always inspects the right thread's pending/blocked
state. A default-disposition Terminate (or job-control Stop, sigTstp's
default -- see signal.h) signal cannot be handled here directly -- it
needs a NEW thread's rsp, which only schedule() can produce -- so the
loop below reuses the exact same "flip state, then reschedule" pattern
sysExit/sysSleep already use (threadExit for Terminate, threadStop for
Stop), and loops again in case the freshly chosen thread also has a
signal of its own waiting (e.g. two threads both made Runnable by the
same timer tick and both signaled). */
static uint64 deliverPendingSignals(uint64 rsp)
{
for (;;) {
Registers* regs = (Registers*)rsp;
if ((regs->cs & 3) != 3)
return rsp;
Thread* thread = currentThread();
int signum = 0;
SigOutcome outcome = sigConsumePending(thread, regs, &signum);
if (outcome == sigOutcomeNone)
return rsp;
if (outcome == sigOutcomeHandler)
return (uint64)regs;
if (outcome == sigOutcomeStop) {
kprintf("[sig] pid=%u '%s' stopped by signal %d\n",
thread->id, thread->name, signum);
threadStop(thread->id);
rsp = schedule(rsp);
continue;
}
kprintf("[sig] pid=%u '%s' terminated by uncaught signal %d\n",
thread->id, thread->name, signum);
threadExit(128u + (uint64)signum);
rsp = schedule(rsp);
}
}
/* Attempts to resolve a ring-3 #PF through mmap.c's own demand-paging
machinery: cr2 is the real faulting address (same register the panic
path above already reads), errCode bit 1 (0x2) is the CPU's own
"this was a write access" flag -- the exact distinction
mmapFaultHandler needs to refuse a write against a read-only mapping
instead of silently allowing it. Returns whatever mmapFaultHandler
itself returns: true means a real frame is now mapped and the faulting
instruction can simply be retried (the CPU never advanced rip past a
#PF to begin with); false means this address is not a mapping this
kernel's mmap.c even knows about, or the access violates its own prot
bits -- the caller (isrDispatch below) turns that into a real SIGSEGV
rather than the kernel-wide panic every OTHER exception still gets. */
static bool tryResolveUserPageFault(Registers* regs)
{
/* errCode bit 4 (I/D) is the CPU's own "this fault was an instruction
fetch" flag -- only ever set once paging.h's pageNoExecute is real
hardware enforcement (IA32_EFER.NXE, see paging.c's
pagingNxSupported/boot.S), meaning a PRESENT page's own NX bit
refused execution. This can never be something mmapFaultHandler's
demand-paging logic fixes: there is no lazy version of "make this
page executable" the way there is for a not-yet-faulted-in page, and
mmapFaultHandler's own "pagingResolve != 0 -> already resolved, just
retry" shortcut (mmap.c) would otherwise see the very same present
page, report success, and send the CPU straight back to retry the
identical faulting instruction fetch -- an infinite fault loop that
hangs the whole kernel rather than the one offending thread. Refuse
outright instead; isrDispatch below turns this into a real SIGSEGV
exactly like any other refusal, whether the address belongs to an
mmap Vma, or (mmapFind would never even find it) a stack/brk page
mapped NX by kmain.c/syscall.c/jobctl.c/sched.c. */
if (regs->errCode & 0x10)
return false;
uint64 faultAddress;
__asm__ volatile("mov %%cr2, %0" : "=r"(faultAddress));
bool forWrite = (regs->errCode & 0x2) != 0;
return mmapFaultHandler(currentThread(), faultAddress, forWrite);
}
uint64 isrDispatch(Registers* regs) uint64 isrDispatch(Registers* regs)
{ {
/* A ring-3 (user-mode) page fault gets one real chance at recovery
before falling through to the unconditional panic below: an
instruction inside SOME OTHER thread's buggy user program should
never be able to take the whole machine down with it, exactly like
a real kernel's own per-process fault isolation -- this kernel's
flat, single-shared-address-space design (see paging.c's own file
comments) cannot isolate a bad ACCESS the way separate page tables
would, but it can still isolate the CONSEQUENCE, by killing only the
one thread that caused it. A kernel-mode fault (ring 0, cs&3==0 --
this kernel's own bug, not a user program's) still panics
unconditionally below, exactly as before this existed. */
if (regs->intNo == 14 && (regs->cs & 3) == 3) {
if (!tryResolveUserPageFault(regs))
sigRaiseInfo(currentThreadId(), sigSegv, siCodeKernel, 0, 0);
return deliverPendingSignals((uint64)regs);
}
if (regs->intNo < 32) if (regs->intNo < 32)
panicOnException(regs); panicOnException(regs);
if (regs->intNo == 128) uint64 nextRsp;
return syscallDispatch(regs);
uint8 irq = (uint8)(regs->intNo - 32); if (regs->intNo == 128) {
uint64 nextRsp = (uint64)regs; nextRsp = syscallDispatch(regs);
} else {
uint8 irq = (uint8)(regs->intNo - 32);
nextRsp = (uint64)regs;
if (irq == 0) { if (irq == 0) {
pitTick(); pitTick();
nextRsp = schedule((uint64)regs); nextRsp = schedule((uint64)regs);
} else if (irq == 1) { } else if (irq == 1) {
keyboardHandler(); keyboardHandler();
}
picSendEoi(irq);
} }
picSendEoi(irq); return deliverPendingSignals(nextRsp);
return nextRsp;
} }
+429
View File
@@ -0,0 +1,429 @@
#include "jobctl.h"
#include "sched.h"
#include "vfs.h"
#include "fs.h"
#include "elf.h"
#include "paging.h"
#include "keyboard.h"
#include "pipe.h"
#include "string.h"
/* Matches kmain.c's userStackSize and syscall.c's execStackSize -- all
three name the exact same "one slot's worth of stack" constant (elf.h's
elfSlotStackOffset leaves 0x8000000 bytes below the stack top before the
next thing in the slot, brk, starts; nothing in this kernel has ever
needed anywhere near that much real stack). Kept as jobctl.c's own copy
rather than sharing a header-level define with the other two call sites,
the same "each caller of this pattern owns its own named copy" choice
kmain.c and syscall.c already made independently of each other. */
#define jobctlStackSize 0x4000
/* Shared by both functions below: resolves `path` through the VFS, loads
it at its own fixed ELF-linked slot (elfLoad picks the address, same as
syscall.c's loadImageByPath), and spawns it as a brand new process group
leader -- the one behavioral difference from doSpawn (syscall.c), which
leaves a freshly spawned child in its parent's existing group instead.
Returns threadInvalidId, having created nothing, on any failure (missing
binary, no execute permission, bad ELF, stack mapping failure, or a full
thread table). */
static uint32 jobctlSpawn(const char* path, const char* const* argv, int argc)
{
VfsNode* node = vfsLookup(path);
if (!node)
return threadInvalidId;
/* Real EACCES-shaped refusal, same check and same reasoning as
syscall.c's loadImageByPath (sysExec/sysSpawn's own parallel loading
path) -- a file the CALLING thread's identity is not permitted to
exec (vfsPermitted, owner-vs-other-vs-root) can never be spawned this
way either. Collapsed into the same undifferentiated threadInvalidId
failure as every other reason jobctlSpawn already refuses (missing
binary, bad ELF, ...), matching this function's own existing doc
comment. */
if (!vfsPermitted(vfsMode(node), vfsUid(node), vfsGid(node), currentThreadUid(), currentThreadGid(), fsModeOwnerExec))
return threadInvalidId;
const void* image = vfsFileData(node);
uint64 size = vfsSize(node);
uint64 entry = elfLoad(image, size);
if (entry == 0)
return threadInvalidId;
uint64 stackTop = elfSlotStackTop(entry);
if (!pagingMapUserRangeFlags(stackTop - jobctlStackSize, jobctlStackSize, pageNoExecute))
return threadInvalidId;
uint64 initialRsp;
if (!elfBuildInitialStack(stackTop, jobctlStackSize, argv, argc, &initialRsp))
return threadInvalidId;
uint32 pid = threadCreateUser(path, entry, initialRsp);
if (pid == threadInvalidId)
return threadInvalidId;
threadSetBrk(pid, elfSlotBrkStart(entry));
/* A real shell's fork()+setpgid(child, child): every job started
through jobctl gets its own fresh group regardless of the caller's
own pgid, so a later Ctrl-C aimed at THIS job (via
keyboardSetForegroundPgid below) can never also reach some earlier,
unrelated job that happened to share the caller's group. */
threadSetPgid(pid, pid);
return pid;
}
uint32 jobctlSpawnPiped(const char* path, const char* const* argv, int argc,
int stdinHandle, int stdoutHandle, uint32 joinPgid)
{
uint32 pid = jobctlSpawn(path, argv, argc);
if (pid == threadInvalidId)
return threadInvalidId;
/* jobctlSpawn above already made this its own new group leader --
overridden here, after the fact, only for a pipeline's second-and-
later stage, which needs to join the FIRST stage's group instead so
the two are scoped together for signals (threadKillGroup/
sigRaiseGroup, sched.h/signal.h) exactly like a real shell's
fork()+setpgid(child, leaderPid) for every stage after the first. */
if (joinPgid != threadInvalidId)
threadSetPgid(pid, joinPgid);
/* Bakes the requested pipe ends into fd 0/fd 1 of a thread that has not
run a single instruction yet (threadCreateUser, inside jobctlSpawn
above, already set its state Runnable -- see that function's own
field-by-field ordering -- but nothing yields between here and
jobctlSpawn's return, the same timing this file's own
threadSetBrk/threadSetPgid calls already rely on). fd 0's default
console reader (vfsFdInit, vfs.c) is released first so redirecting it
never leaks that reader slot -- exactly the resource reclaimOpenFds
(sched.c) already frees for a thread that dies still holding it,
just done here up front instead of at exit. fd 1 has no such
resource to release: its own default (fdConsole, no reader) owns
nothing. */
FileDescriptor* fds = threadAt(pid)->fds;
if (stdinHandle >= 0) {
keyboardCloseReader(fds[0].pipeHandle);
vfsPipeAttachInto(fds, 0, stdinHandle, false);
}
if (stdoutHandle >= 0)
vfsPipeAttachInto(fds, 1, stdoutHandle, true);
return pid;
}
uint32 jobctlSpawnForeground(const char* path, const char* const* argv, int argc)
{
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId);
if (pid == threadInvalidId)
return threadInvalidId;
keyboardSetForegroundPgid(pid);
return pid;
}
int64 jobctlWaitForeground(uint32 pid)
{
/* Read BEFORE threadJoin, not after: this is the WAITER's own group
(the shell, still very much alive), not the job's -- reading it
after threadJoin would still be correct today since nothing else
changes the calling thread's own pgid in between, but reading it
first keeps this function correct even if that ever changes, and
costs nothing. */
uint32 callerPgid = threadPgid(currentThreadId());
int64 code = threadJoin(pid);
/* Reclaim the terminal for the CALLER's own group the instant the job
exits -- deliberately not "whatever pgid happened to own the
terminal right before jobctlSpawnForeground ran" (which could itself
be some earlier, now-dead job's stale id if this is the very first
foreground job the caller has ever run): a real shell always gets
its own prompt's terminal back when a foreground job quits, so a
later Ctrl-C typed at the prompt reaches the shell's NEXT job, not a
group nothing will ever answer for again. */
keyboardSetForegroundPgid(callerPgid);
return code;
}
int64 jobctlWaitPipelineStages(const uint32* pids, int count)
{
/* Same "read before threadJoin" reasoning as jobctlWaitForeground
above. */
uint32 callerPgid = threadPgid(currentThreadId());
/* Grants the terminal to the pipeline's shared group (pids[0] IS that
group's id -- jobctlSpawnPiped's own joinPgid contract already made
every other pid a member of it) once, for the whole chain together,
rather than per-spawn -- a Ctrl-C typed while the pipeline runs
reaches every stage at once, exactly like jobctlSpawnForeground
already does for a single job. */
keyboardSetForegroundPgid(pids[0]);
/* threadJoin blocks on a SPECIFIC pid regardless of which stage
actually exits first -- joining every one, in this fixed order,
reaps all of them correctly no matter the real completion order, the
same way a real shell's own wait() for each of a pipeline's known
children would. */
int64 lastCode = 0;
for (int i = 0; i < count; i++)
lastCode = threadJoin(pids[i]);
keyboardSetForegroundPgid(callerPgid);
return lastCode;
}
bool jobctlRunBackground(const char* path, const char* const* argv, int argc,
uint32* outPid)
{
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId);
if (pid == threadInvalidId)
return false;
/* Deliberately no keyboardSetForegroundPgid call at all -- a
backgrounded job never owns the terminal, so a Ctrl-C typed while it
runs reaches whatever WAS already in the foreground instead (the
shell itself, if nothing else is running in the foreground), never
this job. */
*outPid = pid;
return true;
}
bool jobctlApplyRedirects(uint32 pid, const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend)
{
FileDescriptor* fds = threadAt(pid)->fds;
if (inPath) {
/* Same "release the default console reader before overwriting fd 0"
step jobctlSpawnPiped already does for a piped stdin above --
keyboardCloseReader is a safe no-op on an already-closed/invalid
handle (readerAt's own bounds+used check, keyboard.c), so this
is correct even if fd 0 already holds something else entirely
(e.g. a pipe end from an earlier jobctlSpawnPiped call on this
same pid, in runPipeline's left-side case below). */
keyboardCloseReader(fds[0].pipeHandle);
if (vfsOpenInto(fds, 0, inPath, oReadOnly) < 0)
return false;
}
if (outPath) {
uint32 flags = oCreate | oWriteOnly | (outAppend ? oAppend : oTrunc);
if (vfsOpenInto(fds, 1, outPath, flags) < 0)
return false;
}
if (errPath) {
uint32 flags = oCreate | oWriteOnly | (errAppend ? oAppend : oTrunc);
if (vfsOpenInto(fds, 2, errPath, flags) < 0)
return false;
}
return true;
}
uint32 jobctlSpawnForegroundRedirected(const char* path, const char* const* argv, int argc,
const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend)
{
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId);
if (pid == threadInvalidId)
return threadInvalidId;
if (!jobctlApplyRedirects(pid, inPath, outPath, outAppend, errPath, errAppend)) {
threadKill(pid);
threadJoin(pid);
return threadInvalidId;
}
keyboardSetForegroundPgid(pid);
return pid;
}
bool jobctlRunBackgroundRedirected(const char* path, const char* const* argv, int argc,
const char* inPath, const char* outPath, bool outAppend,
const char* errPath, bool errAppend,
uint32* outPid)
{
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, -1, threadInvalidId);
if (pid == threadInvalidId)
return false;
if (!jobctlApplyRedirects(pid, inPath, outPath, outAppend, errPath, errAppend)) {
threadKill(pid);
threadJoin(pid);
return false;
}
*outPid = pid;
return true;
}
/* Generalizes jobctlSpawnPiped's own fd-baking-before-first-instruction
machinery to also here-string-feed stdin: spawns `path` with fd 0 wired
to a brand-new pipe AND fd 1/pgid wired exactly as `stdoutHandle`/
`joinPgid` ask (passed straight through to jobctlSpawnPiped, unlike the
fixed -1/threadInvalidId every earlier here-string caller in this file
used), then feeds `text` plus a trailing newline into the new pipe's
write end and closes it -- all BEFORE returning, so the child can never
observe the write end still open. Ordering matters: the write end is
only added/used/closed AFTER jobctlSpawnPiped has already attached the
read end (pipeAddReader, via vfsPipeAttachInto in vfs.c) -- pipe.c's
pipeReleaseIfUnused only frees a pipe's slot once BOTH readers and
writers reach 0, so closing the write end while the read end is still
attached (readers==1) leaves the pipe, and the data already sitting in
its ring buffer, alive for the child to actually read; closing it
BEFORE the read end attached (readers==0) would instead free the whole
slot out from under the still-buffered payload the instant this
function's own pipeCloseWrite ran, handing the child a dangling handle.
`text`'s own length plus one newline byte is always far under
pipeBufferSize (256, pipe.h) for any line this shell's own lineCapacity
(128, shell.c) could ever have typed it from, so the non-blocking
pipeWrite below can never see "buffer full" and needs no retry loop the
way a general-purpose writer would. Returns threadInvalidId, having
spawned and written nothing, on the same load/spawn failures as
jobctlSpawnPiped, or if the pipe table itself is exhausted
(vfsPipeCreateHandle). */
uint32 jobctlSpawnPipedHereString(const char* path, const char* const* argv, int argc,
const char* text, int stdoutHandle, uint32 joinPgid)
{
int handle = vfsPipeCreateHandle();
if (handle < 0)
return threadInvalidId;
uint32 pid = jobctlSpawnPiped(path, argv, argc, handle, stdoutHandle, joinPgid);
if (pid == threadInvalidId) {
pipeDestroyIfUnattached(handle);
return threadInvalidId;
}
pipeAddWriter(handle);
uint64 length = strlen(text);
if (length > 0)
pipeWrite(handle, text, length);
pipeWrite(handle, "\n", 1);
pipeCloseWrite(handle);
return pid;
}
uint32 jobctlSpawnForegroundHereString(const char* path, const char* const* argv, int argc,
const char* text, const char* outPath, bool outAppend,
const char* errPath, bool errAppend)
{
uint32 pid = jobctlSpawnPipedHereString(path, argv, argc, text, -1, threadInvalidId);
if (pid == threadInvalidId)
return threadInvalidId;
/* inPath is always nullptr here: fd 0 is already the here-string pipe
jobctlSpawnPipedHereString just wired up, exactly like a piped
stdin's slot in runPipeline (shell.c) is never ALSO given a file
redirect on the same fd. */
if (!jobctlApplyRedirects(pid, nullptr, outPath, outAppend, errPath, errAppend)) {
threadKill(pid);
threadJoin(pid);
return threadInvalidId;
}
keyboardSetForegroundPgid(pid);
return pid;
}
bool jobctlRunBackgroundHereString(const char* path, const char* const* argv, int argc,
const char* text, const char* outPath, bool outAppend,
const char* errPath, bool errAppend,
uint32* outPid)
{
uint32 pid = jobctlSpawnPipedHereString(path, argv, argc, text, -1, threadInvalidId);
if (pid == threadInvalidId)
return false;
if (!jobctlApplyRedirects(pid, nullptr, outPath, outAppend, errPath, errAppend)) {
threadKill(pid);
threadJoin(pid);
return false;
}
*outPid = pid;
return true;
}
/* The mirror image of jobctlSpawnPipedHereString above: that one feeds a
fixed string INTO a freshly spawned child's stdin (the shell is the
writer), this one reads a freshly spawned child's stdout back OUT as a
string (the shell is the reader) -- shell.c's new `$(cmd)` command
substitution. Attaches this thread as the pipe's reader (pipeAddReader)
BEFORE jobctlSpawnPiped ever attaches the child as its writer, the exact
opposite order of jobctlSpawnPipedHereString's own writer-after-reader
ordering but for the same reason: whichever end never gets attached at
all must not let pipeCloseRead/pipeCloseWrite release the slot out from
under the other end while it is still in use. stdin is left at its plain
default (whatever fd 0 already is for this kernel thread, exactly like
jobctlRunBackground's own unpiped stdin) -- an honestly-scoped limitation
matching runPipelineStages' "no stage may be a builtin": a substituted
command that genuinely needs to read stdin itself has nothing useful
wired to it here, since command substitution has no argv-supplied input
of its own to give it.
Joins the child (threadJoin) BEFORE ever reading a single byte, then
drains whatever it wrote with plain non-blocking pipeRead calls -- the
opposite order of "read until EOF, then join" would deadlock in this
kernel every time: reclaimOpenFds (sched.c), which closes a dead
thread's own fd 1 and so is what actually makes pipeRead ever report
real end-of-file (writers reaching 0), runs ONLY from inside threadReap
(threadJoin's own last step), never from threadExit/threadKill
themselves -- a thread that has already died but not yet been reaped
still counts, from the pipe's own point of view, as holding fd 1 open.
Reading first would therefore block forever waiting for an EOF that can
only ever arrive AFTER the very threadJoin call that would come next.
Joining first instead means the child's own write(s) must already have
completed (and fit in the pipe's ring buffer without blocking on free
space, exactly like jobctlSpawnPipedHereString's own here-string write
already assumes for the identical pipeBufferSize=256 constant) by the
time it actually exits -- an honestly-scoped limitation this function
shares with that one: a substituted command whose own combined stdout
exceeds pipeBufferSize bytes would block on a full buffer waiting for a
reader that will not start draining until this function's own
threadJoin below returns, which itself requires the write to unblock
first -- the same ordering deadlock in the opposite direction, avoided
here simply by staying within the same generous margin every other
pipeBufferSize-sized fixed buffer in this kernel already relies on.
Copies at most outCap-1 bytes into outBuf (NUL-terminated); any further
buffered bytes past that are still drained (into a small scratch
buffer, discarded) rather than left sitting in the pipe. *outStatus
receives the child's real exit code. Returns false, touching neither
outBuf nor *outStatus, on the same load/spawn failures as
jobctlSpawnPiped, or if the pipe table itself is exhausted
(vfsPipeCreateHandle). */
bool jobctlCaptureOutput(const char* path, const char* const* argv, int argc,
char* outBuf, int outCap, int64* outStatus)
{
int handle = vfsPipeCreateHandle();
if (handle < 0)
return false;
pipeAddReader(handle);
uint32 pid = jobctlSpawnPiped(path, argv, argc, -1, handle, threadInvalidId);
if (pid == threadInvalidId) {
pipeCloseRead(handle);
return false;
}
*outStatus = threadJoin(pid);
int used = 0;
char scratch[64];
for (;;) {
int64 n = (used < outCap - 1)
? pipeRead(handle, outBuf + used, (uint64)(outCap - 1 - used))
: pipeRead(handle, scratch, sizeof(scratch));
if (n <= 0)
break;
if (used < outCap - 1)
used += (int)n;
}
outBuf[used] = 0;
pipeCloseRead(handle);
return true;
}
+169 -27
View File
@@ -1,8 +1,26 @@
#include "keyboard.h" #include "keyboard.h"
#include "ports.h" #include "ports.h"
#include "ring.h"
#include "sched.h"
#include "signal.h"
#define keyboardData 0x60 #define keyboardData 0x60
/* Left-Ctrl's own PS/2 Set 1 make/break scancodes -- exactly the same
convention keyboardHandleScancode already had for Shift (0x2A/0x36 make,
0xAA/0xB6 break): held state tracked in a single bool, never forwarded
to scancodeLower/scancodeUpper (there is no printable character for a
modifier key on its own). */
#define scancodeLeftCtrlMake 0x1D
#define scancodeLeftCtrlBreak 0x9D
static bool ctrlHeld;
/* Real tcsetpgrp(3)'s target -- see keyboard.h's own doc comment for why 0
(keyboardInit's initial value) safely means "nobody yet" with no extra
sentinel needed. */
static uint32 foregroundPgid;
static const char scancodeLower[128] = { static const char scancodeLower[128] = {
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', 0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
@@ -19,32 +37,138 @@ static const char scancodeUpper[128] = {
'*', 0, ' ', 0 '*', 0, ' ', 0
}; };
#define keyBufferSize 128
static volatile char keyBuffer[keyBufferSize];
static volatile uint32 bufferHead;
static volatile uint32 bufferTail;
static bool shiftHeld; static bool shiftHeld;
#define consoleReaderBufferSize 32
typedef struct ConsoleReader {
RingBuffer ring;
uint8 backing[consoleReaderBufferSize];
bool used;
} ConsoleReader;
static ConsoleReader consoleReaders[maxConsoleReaders];
void keyboardInit(void) void keyboardInit(void)
{ {
bufferHead = 0;
bufferTail = 0;
shiftHeld = false; shiftHeld = false;
ctrlHeld = false;
foregroundPgid = 0;
for (int i = 0; i < maxConsoleReaders; i++)
consoleReaders[i].used = false;
} }
static void pushChar(char c) void keyboardSetForegroundPgid(uint32 pgid)
{ {
uint32 next = (bufferHead + 1) % keyBufferSize; foregroundPgid = pgid;
if (next != bufferTail) { }
keyBuffer[bufferHead] = c;
bufferHead = next; uint32 keyboardForegroundPgid(void)
{
return foregroundPgid;
}
int keyboardOpenReader(void)
{
for (int i = 0; i < maxConsoleReaders; i++) {
if (!consoleReaders[i].used) {
ringInit(&consoleReaders[i].ring, consoleReaders[i].backing, consoleReaderBufferSize);
consoleReaders[i].used = true;
return i;
}
}
return consoleReaderInvalid;
}
static ConsoleReader* readerAt(int handle)
{
if (handle < 0 || handle >= maxConsoleReaders)
return nullptr;
if (!consoleReaders[handle].used)
return nullptr;
return &consoleReaders[handle];
}
void keyboardCloseReader(int handle)
{
ConsoleReader* r = readerAt(handle);
if (!r)
return;
r->used = false;
}
int64 keyboardReaderRead(int handle, void* buffer, uint64 length)
{
ConsoleReader* r = readerAt(handle);
if (!r)
return -1;
if (length == 0)
return 0;
if (ringIsEmpty(&r->ring))
return -1;
usize got = ringRead(&r->ring, (uint8*)buffer, (usize)length);
return (int64)got;
}
int64 keyboardReaderReadBlocking(int handle, void* buffer, uint64 length)
{
ConsoleReader* r = readerAt(handle);
if (!r)
return -1;
for (;;) {
int64 result = keyboardReaderRead(handle, buffer, length);
if (result != -1)
return result;
schedBlockCurrent((const void*)r);
halt();
} }
} }
void keyboardHandler(void) const void* keyboardReaderChannel(int handle)
{ {
uint8 scancode = inb(keyboardData); return (const void*)readerAt(handle);
}
bool keyboardReaderHasData(int handle)
{
ConsoleReader* r = readerAt(handle);
if (!r)
return false;
return !ringIsEmpty(&r->ring);
}
/*
* Real cooked-mode tty line discipline's INTR/SUSP characters (termios's
* VINTR/VSUSP, both Ctrl-<letter> by default): keyboardHandleScancode now
* tracks left-Ctrl as a THIRD modifier alongside Shift, and when it is held
* for a 'c'/'C' or 'z'/'Z' keypress, the resulting sigInt/sigTstp is
* delivered to the WHOLE foreground process group (sigRaiseGroup, signal.h)
* -- keyboardSetForegroundPgid's target, real tcsetpgrp(3)'s own job --
* instead of the raw byte ever reaching any consoleReaders fan-out below.
* This is the scope real Ctrl-C actually has: every process in the
* foreground job, not "whichever process happens to be reading fd 0 right
* now" -- a BACKGROUND job (a different pgid, one keyboardSetForegroundPgid
* was never told about) must never be disturbed by a keypress aimed at
* whatever IS currently in the foreground, the same isolation a real shell's
* job control gives. kmain.c's pgroupfg.c/pgroupbg.c is the live ring-3
* proof: pgroupfg (foreground, no handler installed) actually dies to the
* injected Ctrl-C's default-Terminate disposition, while pgroupbg
* (deliberately left in a DIFFERENT group) proves itself still alive and
* responsive afterwards.
*/
void keyboardHandleScancode(uint8 scancode)
{
if (scancode == scancodeLeftCtrlMake) {
ctrlHeld = true;
return;
}
if (scancode == scancodeLeftCtrlBreak) {
ctrlHeld = false;
return;
}
if (scancode == 0x2A || scancode == 0x36) { if (scancode == 0x2A || scancode == 0x36) {
shiftHeld = true; shiftHeld = true;
return; return;
@@ -57,21 +181,39 @@ void keyboardHandler(void)
return; return;
char c = shiftHeld ? scancodeUpper[scancode] : scancodeLower[scancode]; char c = shiftHeld ? scancodeUpper[scancode] : scancodeLower[scancode];
if (c) if (!c)
pushChar(c); return;
/* Consumed here, never forwarded as data below -- exactly like a real
cooked-mode terminal driver never hands a reading program the raw
0x03/0x1A byte either. siCodeKernel/0: no sending PROCESS raised
this, the keyboard itself did, the same "kernel, not a process, is
the source" reasoning schedule()'s own SIGALRM raise already uses
for siCodeKernel. Harmless no-op (sigRaiseGroup's own contract) if
foregroundPgid currently names no live group at all. */
if (ctrlHeld && (c == 'c' || c == 'C')) {
sigRaiseGroup(foregroundPgid, sigInt, siCodeKernel, 0);
return;
}
if (ctrlHeld && (c == 'z' || c == 'Z')) {
sigRaiseGroup(foregroundPgid, sigTstp, siCodeKernel, 0);
return;
}
for (int i = 0; i < maxConsoleReaders; i++) {
if (!consoleReaders[i].used)
continue;
ringPush(&consoleReaders[i].ring, (uint8)c);
/* Mirrors pipeWrite's wake-after-write: harmless no-op (schedWakeChannel's
own contract) if nobody is currently blocked on this particular
reader's channel. */
schedWakeChannel((const void*)&consoleReaders[i]);
}
schedWakeChannel(schedPollChannel());
} }
bool keyboardHasChar(void) void keyboardHandler(void)
{ {
return bufferHead != bufferTail; uint8 scancode = inb(keyboardData);
} keyboardHandleScancode(scancode);
char keyboardReadChar(void)
{
while (bufferHead == bufferTail)
__asm__ volatile("sti; hlt");
char c = keyBuffer[bufferTail];
bufferTail = (bufferTail + 1) % keyBufferSize;
return c;
} }
+216
View File
@@ -0,0 +1,216 @@
#include "ktest.h"
#include "keyboard.h"
#include "string.h"
/*
* Coverage for keyboard.c's per-reader console input queues (keyboard.h) --
* the fan-out/blocking-channel machinery added this wave to close the gap
* called out at the end of the previous one: a single global keyboard
* buffer shared by every consumer, with no per-process queueing and no
* scheduler-visible blocking. What is pure state machine (open/close,
* scancode decoding, fan-out to N independent queues, exhaustion/reuse)
* lives here, driven directly through keyboardHandleScancode -- the same
* seam the live PS/2 IRQ handler (keyboardHandler) calls into, just fed a
* synthetic scancode instead of a real inb() -- exactly the way
* pipeShareTest.c drives pipe.c's state machine without a live scheduler.
* The genuine cross-process park/wake handshake (real blocking read() on
* fd 0 from two independent ring-3 processes, woken by a real keystroke)
* only runs for real at boot (kmain.c's consolereader1/consolereader2 demo,
* needles "consolereader1: got byte" / "consolereader2: got byte" in
* tools/ci-test.sh), matching how every other blocking-I/O subsystem in
* this kernel splits its test coverage.
*
* Scancode reference (set 1, US QWERTY, keyboard.c's own tables): 0x25 is
* 'k'/'K', 0x2A/0xAA are left-shift make/break.
*/
#define scancodeK 0x25
#define scancodeLeftShiftMake 0x2A
#define scancodeLeftShiftBreak 0xAA
KTEST(keyboardOpenReaderReturnsDistinctHandlesAndEmptyReadWouldBlock)
{
int a = keyboardOpenReader();
int b = keyboardOpenReader();
KT_NE(t, a, consoleReaderInvalid);
KT_NE(t, b, consoleReaderInvalid);
KT_NE(t, a, b);
char buffer[4];
KT_EQ(t, keyboardReaderRead(a, buffer, sizeof(buffer)), -1);
KT_EQ(t, keyboardReaderRead(b, buffer, sizeof(buffer)), -1);
keyboardCloseReader(a);
keyboardCloseReader(b);
}
KTEST(keyboardReaderReadRejectsInvalidHandlesAndZeroLength)
{
KT_EQ(t, keyboardReaderRead(consoleReaderInvalid, nullptr, 4), -1);
KT_EQ(t, keyboardReaderRead(9999, nullptr, 4), -1);
int h = keyboardOpenReader();
KT_NE(t, h, consoleReaderInvalid);
KT_EQ(t, keyboardReaderRead(h, nullptr, 0), 0);
keyboardCloseReader(h);
/* Once closed, the handle is dead -- not silently reusable by the
caller who just gave it up. */
KT_EQ(t, keyboardReaderRead(h, nullptr, 0), -1);
}
KTEST(keyboardHandleScancodeBroadcastsToEveryOpenReaderIndependently)
{
int a = keyboardOpenReader();
int b = keyboardOpenReader();
KT_NE(t, a, consoleReaderInvalid);
KT_NE(t, b, consoleReaderInvalid);
keyboardHandleScancode(scancodeK);
char ca = 0, cb = 0;
KT_EQ(t, keyboardReaderRead(a, &ca, 1), 1);
KT_EQ(t, keyboardReaderRead(b, &cb, 1), 1);
KT_EQ(t, ca, 'k');
KT_EQ(t, cb, 'k');
/* Each reader's copy is independent: draining a's queue must not touch
b's, and vice versa -- proven above by both still having their own
byte to read rather than one starving the other. Both are empty now. */
KT_EQ(t, keyboardReaderRead(a, &ca, 1), -1);
KT_EQ(t, keyboardReaderRead(b, &cb, 1), -1);
keyboardCloseReader(a);
keyboardCloseReader(b);
}
KTEST(keyboardHandleScancodeAppliesShiftMakeAndBreak)
{
int h = keyboardOpenReader();
KT_NE(t, h, consoleReaderInvalid);
keyboardHandleScancode(scancodeK);
char c = 0;
KT_EQ(t, keyboardReaderRead(h, &c, 1), 1);
KT_EQ(t, c, 'k');
keyboardHandleScancode(scancodeLeftShiftMake);
keyboardHandleScancode(scancodeK);
KT_EQ(t, keyboardReaderRead(h, &c, 1), 1);
KT_EQ(t, c, 'K');
keyboardHandleScancode(scancodeLeftShiftBreak);
keyboardHandleScancode(scancodeK);
KT_EQ(t, keyboardReaderRead(h, &c, 1), 1);
KT_EQ(t, c, 'k');
keyboardCloseReader(h);
}
KTEST(keyboardHandleScancodeIgnoresKeyReleaseAndUnmappedCodes)
{
int h = keyboardOpenReader();
KT_NE(t, h, consoleReaderInvalid);
/* Key-release (high bit set) for 'k' must queue nothing. */
keyboardHandleScancode(scancodeK | 0x80);
char c;
KT_EQ(t, keyboardReaderRead(h, &c, 1), -1);
/* Scancode 0 maps to 0 (no character) in both tables. */
keyboardHandleScancode(0);
KT_EQ(t, keyboardReaderRead(h, &c, 1), -1);
keyboardCloseReader(h);
}
KTEST(keyboardReaderReadDeliversQueuedBytesInFifoOrderAcrossPartialReads)
{
int h = keyboardOpenReader();
KT_NE(t, h, consoleReaderInvalid);
/* "kkk" via three separate scancode events, like three real keystrokes. */
keyboardHandleScancode(scancodeK);
keyboardHandleScancode(scancodeK);
keyboardHandleScancode(scancodeK);
char buffer[8];
memset(buffer, 0, sizeof(buffer));
/* Ask for only 2 of the 3 queued bytes -- must not block or fail, and
must leave the third for the next call, same partial-transfer
contract ring.h's ringRead already documents. */
int64 got = keyboardReaderRead(h, buffer, 2);
KT_EQ(t, got, 2);
KT_EQ(t, buffer[0], 'k');
KT_EQ(t, buffer[1], 'k');
got = keyboardReaderRead(h, buffer, 8);
KT_EQ(t, got, 1);
KT_EQ(t, buffer[0], 'k');
KT_EQ(t, keyboardReaderRead(h, buffer, 1), -1);
keyboardCloseReader(h);
}
KTEST(keyboardReaderChannelIsNullForInvalidAndDistinctPerReader)
{
KT_NULL(t, keyboardReaderChannel(consoleReaderInvalid));
KT_NULL(t, keyboardReaderChannel(9999));
int a = keyboardOpenReader();
int b = keyboardOpenReader();
KT_NE(t, a, consoleReaderInvalid);
KT_NE(t, b, consoleReaderInvalid);
const void* channelA = keyboardReaderChannel(a);
const void* channelB = keyboardReaderChannel(b);
KT_NOTNULL(t, channelA);
KT_NOTNULL(t, channelB);
KT_NE(t, (int64)(uint64)channelA, (int64)(uint64)channelB);
/* Stable across repeated calls -- schedBlockCurrent/schedWakeChannel
depend on the same reader always producing the same identity. */
KT_EQP(t, keyboardReaderChannel(a), channelA);
/* A closed reader's channel must go back to null immediately -- a
syscall handler using this as its retry condition must never park a
thread on a channel nobody can ever wake again. */
keyboardCloseReader(a);
KT_NULL(t, keyboardReaderChannel(a));
keyboardCloseReader(b);
}
KTEST(keyboardOpenReaderExhaustionThenCloseFreesSlotForReuse)
{
/* Does not assume the table starts fully empty -- by the time ktests
run, schedInit() has already given the idle thread (id 0) its own fd
0 reader (vfs.c's vfsFdInit), same convention vfsFifoTest.c's own
pipe-table exhaustion test uses for the shared pipes[] table. */
int handles[maxConsoleReaders];
int madeCount = 0;
while (madeCount < maxConsoleReaders) {
int h = keyboardOpenReader();
if (h == consoleReaderInvalid)
break;
handles[madeCount++] = h;
}
KT_GT(t, madeCount, 0);
/* Table is now genuinely full: one more must fail cleanly. */
KT_EQ(t, keyboardOpenReader(), consoleReaderInvalid);
keyboardCloseReader(handles[0]);
int reused = keyboardOpenReader();
KT_NE(t, reused, consoleReaderInvalid);
KT_EQ(t, reused, handles[0]);
/* Leave the shared reader table exactly as free as it was found, so
later boot-time console users (the shell, and the
consolereader1/consolereader2 live demo) are never starved by this
test having run. */
keyboardCloseReader(reused);
for (int i = 1; i < madeCount; i++)
keyboardCloseReader(handles[i]);
}
+362
View File
@@ -0,0 +1,362 @@
#include "klib.h"
#include "string.h"
/* ---- character classification -------------------------------------- */
bool isDigit(int c) { return c >= '0' && c <= '9'; }
bool isUpper(int c) { return c >= 'A' && c <= 'Z'; }
bool isLower(int c) { return c >= 'a' && c <= 'z'; }
bool isAlpha(int c) { return isUpper(c) || isLower(c); }
bool isAlnum(int c) { return isAlpha(c) || isDigit(c); }
bool isSpace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f'; }
bool isHexDigit(int c) { return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
int toUpper(int c) { return isLower(c) ? c - 'a' + 'A' : c; }
int toLower(int c) { return isUpper(c) ? c - 'A' + 'a' : c; }
/* ---- string manipulation --------------------------------------------- */
char* strcpy(char* dest, const char* src)
{
char* out = dest;
while ((*out++ = *src++) != 0)
;
return dest;
}
char* strncpy(char* dest, const char* src, usize count)
{
usize i = 0;
for (; i < count && src[i]; i++)
dest[i] = src[i];
for (; i < count; i++)
dest[i] = 0;
return dest;
}
char* strcat(char* dest, const char* src)
{
strcpy(dest + strlen(dest), src);
return dest;
}
char* strncat(char* dest, const char* src, usize count)
{
char* end = dest + strlen(dest);
usize i = 0;
for (; i < count && src[i]; i++)
end[i] = src[i];
end[i] = 0;
return dest;
}
char* strchr(const char* str, int c)
{
for (;; str++) {
if (*str == (char)c)
return (char*)str;
if (*str == 0)
return nullptr;
}
}
char* strrchr(const char* str, int c)
{
const char* last = nullptr;
for (;; str++) {
if (*str == (char)c)
last = str;
if (*str == 0)
return (char*)last;
}
}
char* strstr(const char* haystack, const char* needle)
{
if (!*needle)
return (char*)haystack;
usize needleLen = strlen(needle);
for (; *haystack; haystack++) {
if (memcmp(haystack, needle, needleLen) == 0)
return (char*)haystack;
}
return nullptr;
}
usize strspn(const char* str, const char* accept)
{
usize count = 0;
for (; str[count]; count++) {
if (!strchr(accept, str[count]))
break;
}
return count;
}
usize strcspn(const char* str, const char* reject)
{
usize count = 0;
for (; str[count]; count++) {
if (strchr(reject, str[count]))
break;
}
return count;
}
char* strTokR(char* str, const char* delim, char** savePtr)
{
char* cursor = str ? str : *savePtr;
if (cursor == nullptr)
return nullptr;
cursor += strspn(cursor, delim);
if (*cursor == 0) {
*savePtr = nullptr;
return nullptr;
}
char* tokenEnd = cursor + strcspn(cursor, delim);
if (*tokenEnd) {
*tokenEnd = 0;
*savePtr = tokenEnd + 1;
} else {
*savePtr = nullptr;
}
return cursor;
}
/* ---- number parsing ---------------------------------------------------- */
static int digitValue(int c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'z')
return c - 'a' + 10;
if (c >= 'A' && c <= 'Z')
return c - 'A' + 10;
return -1;
}
uint64 strtoul(const char* str, char** endPtr, int base)
{
const char* cursor = str;
while (isSpace((unsigned char)*cursor))
cursor++;
bool negative = false;
if (*cursor == '+' || *cursor == '-') {
negative = (*cursor == '-');
cursor++;
}
if ((base == 0 || base == 16) && cursor[0] == '0' && (cursor[1] == 'x' || cursor[1] == 'X')) {
cursor += 2;
base = 16;
} else if (base == 0 && cursor[0] == '0') {
base = 8;
} else if (base == 0) {
base = 10;
}
uint64 value = 0;
const char* firstDigit = cursor;
for (;; cursor++) {
int digit = digitValue((unsigned char)*cursor);
if (digit < 0 || digit >= base)
break;
value = value * (uint64)base + (uint64)digit;
}
if (endPtr)
*endPtr = (char*)(cursor == firstDigit ? str : cursor);
return negative ? (uint64)(-(int64)value) : value;
}
int64 strtol(const char* str, char** endPtr, int base)
{
/* strtoul already skips whitespace, consumes an optional sign and
applies it (via unsigned two's-complement negation) before we ever
see the result, so reinterpreting its bits as signed is correct
on its own -- negating again here would flip the sign twice. */
return (int64)strtoul(str, endPtr, base);
}
int32 atoi(const char* str) { return (int32)strtol(str, nullptr, 10); }
int64 atol(const char* str) { return strtol(str, nullptr, 10); }
/* ---- formatted output --------------------------------------------------- */
typedef struct SnprintfSink {
char* buf;
usize size; /* capacity including the room for a nul terminator */
usize written; /* total chars that would be written, unbounded */
} SnprintfSink;
static void sinkPutChar(SnprintfSink* sink, char c)
{
if (sink->written + 1 < sink->size)
sink->buf[sink->written] = c;
sink->written++;
}
static void sinkPutString(SnprintfSink* sink, const char* str)
{
while (*str)
sinkPutChar(sink, *str++);
}
static void sinkPutUnsigned(SnprintfSink* sink, uint64 value, uint32 base, bool upper, int minWidth, char pad)
{
static const char* lower = "0123456789abcdef";
static const char* higher = "0123456789ABCDEF";
const char* digits = upper ? higher : lower;
char buffer[32];
int length = 0;
if (value == 0)
buffer[length++] = '0';
while (value > 0) {
buffer[length++] = digits[value % base];
value /= base;
}
while (length < minWidth && length < (int)sizeof(buffer))
buffer[length++] = pad;
while (length > 0)
sinkPutChar(sink, buffer[--length]);
}
static void sinkPutSigned(SnprintfSink* sink, int64 value, int minWidth, char pad)
{
if (value < 0) {
sinkPutChar(sink, '-');
sinkPutUnsigned(sink, (uint64)(-value), 10, false, minWidth > 0 ? minWidth - 1 : 0, pad);
} else {
sinkPutUnsigned(sink, (uint64)value, 10, false, minWidth, pad);
}
}
int vsnprintf(char* buf, usize size, const char* format, va_list args)
{
SnprintfSink sink = { buf, size, 0 };
for (usize i = 0; format[i]; i++) {
if (format[i] != '%') {
sinkPutChar(&sink, format[i]);
continue;
}
i++;
int width = 0;
char pad = ' ';
if (format[i] == '0') {
pad = '0';
i++;
}
while (format[i] >= '0' && format[i] <= '9') {
width = width * 10 + (format[i] - '0');
i++;
}
int longCount = 0;
while (format[i] == 'l') {
longCount++;
i++;
}
switch (format[i]) {
case 'c':
sinkPutChar(&sink, (char)va_arg(args, int));
break;
case 's':
sinkPutString(&sink, va_arg(args, const char*));
break;
case 'd':
case 'i':
sinkPutSigned(&sink, longCount ? va_arg(args, int64) : (int64)va_arg(args, int), width, pad);
break;
case 'u':
sinkPutUnsigned(&sink, longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 10, false, width, pad);
break;
case 'x':
sinkPutUnsigned(&sink, longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 16, false, width, pad);
break;
case 'X':
sinkPutUnsigned(&sink, longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 16, true, width, pad);
break;
case 'p':
sinkPutString(&sink, "0x");
sinkPutUnsigned(&sink, (uint64)va_arg(args, void*), 16, false, 16, '0');
break;
case '%':
sinkPutChar(&sink, '%');
break;
default:
sinkPutChar(&sink, '%');
if (format[i])
sinkPutChar(&sink, format[i]);
break;
}
}
if (size > 0)
buf[sink.written < size ? sink.written : size - 1] = 0;
return (int)sink.written;
}
int snprintf(char* buf, usize size, const char* format, ...)
{
va_list args;
va_start(args, format);
int result = vsnprintf(buf, size, format, args);
va_end(args);
return result;
}
/* ---- generic sorting / searching ---------------------------------------- */
static void rawSwap(uint8* a, uint8* b, usize size)
{
for (usize i = 0; i < size; i++) {
uint8 tmp = a[i];
a[i] = b[i];
b[i] = tmp;
}
}
/* Insertion sort: O(n^2) but allocation-free and simple to prove correct;
fine for the small, bounded arrays a freestanding kernel actually sorts
(dirents in a directory, a handful of scheduler entries, ...). */
void qsort(void* base, usize count, usize size, int (*cmp)(const void*, const void*))
{
uint8* items = (uint8*)base;
for (usize i = 1; i < count; i++) {
usize j = i;
while (j > 0 && cmp(items + j * size, items + (j - 1) * size) < 0) {
rawSwap(items + j * size, items + (j - 1) * size, size);
j--;
}
}
}
void* bsearch(const void* key, const void* base, usize count, usize size,
int (*cmp)(const void*, const void*))
{
const uint8* items = (const uint8*)base;
usize low = 0;
usize high = count;
while (low < high) {
usize mid = low + (high - low) / 2;
const uint8* candidate = items + mid * size;
int result = cmp(key, candidate);
if (result == 0)
return (void*)candidate;
if (result < 0)
high = mid;
else
low = mid + 1;
}
return nullptr;
}
+166
View File
@@ -0,0 +1,166 @@
#include "ktest.h"
#include "klib.h"
#include "string.h"
KTEST(klibCtype)
{
KT_TRUE(t, isDigit('0'));
KT_TRUE(t, isDigit('9'));
KT_FALSE(t, isDigit('a'));
KT_TRUE(t, isAlpha('a'));
KT_TRUE(t, isAlpha('Z'));
KT_FALSE(t, isAlpha('5'));
KT_TRUE(t, isAlnum('7'));
KT_TRUE(t, isAlnum('q'));
KT_TRUE(t, isSpace(' '));
KT_TRUE(t, isSpace('\t'));
KT_FALSE(t, isSpace('x'));
KT_TRUE(t, isHexDigit('f'));
KT_TRUE(t, isHexDigit('F'));
KT_TRUE(t, isHexDigit('9'));
KT_FALSE(t, isHexDigit('g'));
KT_EQ(t, toUpper('a'), 'A');
KT_EQ(t, toUpper('A'), 'A');
KT_EQ(t, toLower('Z'), 'z');
KT_EQ(t, toLower('5'), '5');
}
KTEST(klibStrCopyCat)
{
char buf[32];
strcpy(buf, "frost");
KT_STREQ(t, buf, "frost");
strcat(buf, "byte");
KT_STREQ(t, buf, "frostbyte");
char nbuf[8];
memset(nbuf, 'X', sizeof(nbuf));
strncpy(nbuf, "hi", sizeof(nbuf));
KT_EQ(t, nbuf[0], 'h');
KT_EQ(t, nbuf[1], 'i');
KT_EQ(t, nbuf[2], 0);
KT_EQ(t, nbuf[7], 0); /* strncpy pads the remainder with nul */
char abuf[16];
strcpy(abuf, "ab");
strncat(abuf, "cdefgh", 3);
KT_STREQ(t, abuf, "abcde");
}
KTEST(klibStrSearch)
{
const char* s = "the quick brown fox";
KT_EQP(t, strchr(s, 'q'), s + 4);
KT_NULL(t, strchr(s, 'Z'));
KT_EQP(t, strrchr(s, 'o'), s + 17);
KT_NOTNULL(t, strstr(s, "brown"));
KT_STREQ(t, strstr(s, "brown"), "brown fox");
KT_NULL(t, strstr(s, "purple"));
KT_STREQ(t, strstr(s, ""), s); /* empty needle matches at start */
KT_EQ(t, strspn("aaabbbccc", "ab"), 6);
KT_EQ(t, strcspn("hello, world", ", "), 5);
}
KTEST(klibStrTokR)
{
char input[32];
strcpy(input, "one,two,,four");
char* save = nullptr;
char* tok = strTokR(input, ",", &save);
KT_STREQ(t, tok, "one");
tok = strTokR(nullptr, ",", &save);
KT_STREQ(t, tok, "two");
tok = strTokR(nullptr, ",", &save);
KT_STREQ(t, tok, "four"); /* empty field between two commas is skipped */
tok = strTokR(nullptr, ",", &save);
KT_NULL(t, tok);
}
KTEST(klibNumberParsing)
{
KT_EQ(t, atoi("42"), 42);
KT_EQ(t, atoi("-42"), -42);
KT_EQ(t, atoi(" 7"), 7);
KT_EQ(t, atol("123456789012"), 123456789012LL);
char* end = nullptr;
KT_EQ(t, strtol("2026", &end, 10), 2026);
KT_EQ(t, *end, 0);
KT_EQ(t, strtol("-15", nullptr, 10), -15);
KT_EQ(t, strtol("ff", nullptr, 16), 255);
KT_EQ(t, strtol("0x1A", nullptr, 0), 26);
KT_EQ(t, strtol("010", nullptr, 0), 8); /* auto-detected octal */
KT_EQ(t, strtol("42", nullptr, 0), 42);
KT_EQU(t, strtoul("ffff", nullptr, 16), 65535);
end = nullptr;
strtol("nope", &end, 10);
KT_STREQ(t, end, "nope"); /* no digits consumed: endptr == start */
}
static int intCmp(const void* a, const void* b)
{
int ia = *(const int*)a;
int ib = *(const int*)b;
return (ia > ib) - (ia < ib);
}
KTEST(klibQsortBsearch)
{
int values[8] = { 5, 3, 8, 1, 9, 2, 7, 4 };
qsort(values, 8, sizeof(int), intCmp);
for (int i = 0; i < 7; i++)
KT_LE(t, values[i], values[i + 1]);
KT_EQ(t, values[0], 1);
KT_EQ(t, values[7], 9);
int key = 7;
int* found = (int*)bsearch(&key, values, 8, sizeof(int), intCmp);
KT_NOTNULL(t, found);
KT_EQ(t, *found, 7);
int missing = 42;
KT_NULL(t, bsearch(&missing, values, 8, sizeof(int), intCmp));
}
KTEST(klibSnprintf)
{
char buf[64];
int n = snprintf(buf, sizeof(buf), "%s=%d", "answer", 42);
KT_STREQ(t, buf, "answer=42");
KT_EQ(t, n, 9);
snprintf(buf, sizeof(buf), "%05d", 42);
KT_STREQ(t, buf, "00042");
snprintf(buf, sizeof(buf), "%05d", -42);
KT_STREQ(t, buf, "-0042");
snprintf(buf, sizeof(buf), "%x-%X", 255, 255);
KT_STREQ(t, buf, "ff-FF");
snprintf(buf, sizeof(buf), "%lu", 9876543210ULL);
KT_STREQ(t, buf, "9876543210");
snprintf(buf, sizeof(buf), "%c%c%c", 'a', 'b', 'c');
KT_STREQ(t, buf, "abc");
snprintf(buf, sizeof(buf), "100%%");
KT_STREQ(t, buf, "100%");
/* truncation: buffer smaller than the formatted result still nul
terminates, and reports the length that would have been written */
char small[4];
int wanted = snprintf(small, sizeof(small), "hello");
KT_STREQ(t, small, "hel");
KT_EQ(t, wanted, 5);
/* size == 0 must not touch the buffer at all, and still reports length */
int wantedZero = snprintf(nullptr, 0, "hello");
KT_EQ(t, wantedZero, 5);
}
+1874 -15
View File
File diff suppressed because it is too large. Load diff
+59
View File
@@ -0,0 +1,59 @@
#include "ktest.h"
#include "console.h"
/* Bracketed by the linker script (see linker.ld, .rodata output section):
the ".ktests" input section holds one const KtestEntry* per registered
test. Iterating [start, end) visits every test in link order. */
extern const KtestEntry* const ktestArrayStart[];
extern const KtestEntry* const ktestArrayEnd[];
bool ktestStrEq(const char* a, const char* b)
{
if (a == nullptr || b == nullptr)
return a == b;
while (*a && (*a == *b)) {
a++;
b++;
}
return *a == *b;
}
bool ktestCheck(KtestCtx* t, bool ok, const char* expr, const char* file, int line)
{
t->checks++;
if (!ok) {
t->failures++;
kprintf(" FAIL %s:%d %s\n", file, line, expr);
}
return ok;
}
uint64 ktestRunAll(void)
{
uint64 totalTests = 0;
uint64 totalChecks = 0;
uint64 totalFailures = 0;
uint64 failedTests = 0;
kprintf("\n==== ktest: running in-kernel self-tests ====\n");
for (const KtestEntry* const* p = ktestArrayStart; p < ktestArrayEnd; p++) {
const KtestEntry* e = *p;
KtestCtx t = { e->name, 0, 0 };
e->fn(&t);
totalTests++;
totalChecks += t.checks;
totalFailures += t.failures;
if (t.failures) {
failedTests++;
kprintf(" [FAIL] %s (%lu/%lu checks failed)\n", e->name, t.failures, t.checks);
} else {
kprintf(" [ ok ] %s (%lu checks)\n", e->name, t.checks);
}
}
kprintf("==== ktest done: %lu of %lu tests failed ====\n", failedTests, totalTests);
kprintf("KTEST SUMMARY: %lu tests, %lu checks, %lu failed\n",
totalTests, totalChecks, totalFailures);
return totalFailures;
}
+61
View File
@@ -0,0 +1,61 @@
#include "list.h"
void listInit(ListNode* head)
{
head->next = head;
head->prev = head;
}
bool listEmpty(const ListNode* head)
{
return head->next == head;
}
void listInsertAfter(ListNode* pos, ListNode* newNode)
{
newNode->prev = pos;
newNode->next = pos->next;
pos->next->prev = newNode;
pos->next = newNode;
}
void listInsertBefore(ListNode* pos, ListNode* newNode)
{
listInsertAfter(pos->prev, newNode);
}
void listPushBack(ListNode* head, ListNode* newNode)
{
listInsertBefore(head, newNode);
}
void listPushFront(ListNode* head, ListNode* newNode)
{
listInsertAfter(head, newNode);
}
void listRemove(ListNode* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
node->next = node;
node->prev = node;
}
usize listLen(const ListNode* head)
{
usize count = 0;
for (const ListNode* cursor = head->next; cursor != head; cursor = cursor->next)
count++;
return count;
}
ListNode* listFront(const ListNode* head)
{
return listEmpty(head) ? nullptr : head->next;
}
ListNode* listBack(const ListNode* head)
{
return listEmpty(head) ? nullptr : head->prev;
}
+112
View File
@@ -0,0 +1,112 @@
#include "ktest.h"
#include "list.h"
typedef struct TestItem {
int value;
ListNode node;
} TestItem;
KTEST(listEmptyInit)
{
ListNode head;
listInit(&head);
KT_TRUE(t, listEmpty(&head));
KT_EQ(t, listLen(&head), 0);
KT_NULL(t, listFront(&head));
KT_NULL(t, listBack(&head));
}
KTEST(listPushBackOrder)
{
ListNode head;
listInit(&head);
TestItem items[4];
for (int i = 0; i < 4; i++) {
items[i].value = i;
listPushBack(&head, &items[i].node);
}
KT_FALSE(t, listEmpty(&head));
KT_EQ(t, listLen(&head), 4);
int expected = 0;
ListNode* cursor;
listForEach(cursor, &head) {
TestItem* item = listEntry(cursor, TestItem, node);
KT_EQ(t, item->value, expected);
expected++;
}
KT_EQ(t, expected, 4);
TestItem* front = listEntry(listFront(&head), TestItem, node);
TestItem* back = listEntry(listBack(&head), TestItem, node);
KT_EQ(t, front->value, 0);
KT_EQ(t, back->value, 3);
}
KTEST(listPushFrontOrder)
{
ListNode head;
listInit(&head);
TestItem items[3];
for (int i = 0; i < 3; i++) {
items[i].value = i;
listPushFront(&head, &items[i].node);
}
/* pushing 0,1,2 to the front in order yields 2,1,0 */
int expected[3] = { 2, 1, 0 };
int idx = 0;
ListNode* cursor;
listForEach(cursor, &head) {
TestItem* item = listEntry(cursor, TestItem, node);
KT_EQ(t, item->value, expected[idx]);
idx++;
}
}
KTEST(listRemoveMiddle)
{
ListNode head;
listInit(&head);
TestItem items[3];
for (int i = 0; i < 3; i++) {
items[i].value = i;
listPushBack(&head, &items[i].node);
}
listRemove(&items[1].node);
KT_EQ(t, listLen(&head), 2);
int idx = 0;
int expected[2] = { 0, 2 };
ListNode* cursor;
listForEach(cursor, &head) {
TestItem* item = listEntry(cursor, TestItem, node);
KT_EQ(t, item->value, expected[idx]);
idx++;
}
/* a removed node becomes its own trivial list */
KT_TRUE(t, listEmpty(&items[1].node));
}
KTEST(listRemoveAllEmptiesList)
{
ListNode head;
listInit(&head);
TestItem items[2];
items[0].value = 10;
items[1].value = 20;
listPushBack(&head, &items[0].node);
listPushBack(&head, &items[1].node);
listRemove(&items[0].node);
listRemove(&items[1].node);
KT_TRUE(t, listEmpty(&head));
KT_EQ(t, listLen(&head), 0);
}
+259
View File
@@ -0,0 +1,259 @@
#include "mmap.h"
#include "sched.h"
#include "elf.h"
#include "paging.h"
#include "pmm.h"
#include "vfs.h"
#include "string.h"
/*
* Real demand-paged mmap(2)/munmap(2) on top of this kernel's single
* shared page table: a Vma only ever RESERVES address space (mmapReserve)
* -- no physical frame is allocated and no file byte is read until the
* very first access to a given page actually traps into isr.c's #PF
* handler, which resolves it here (mmapFaultHandler). This is the same
* "lazy" contract pagingMapUserRange's own eager sibling does NOT have
* (that one maps and zeroes every page up front, for an ELF image/stack
* that is about to be fully used anyway) -- mmap's whole point is a
* mapping the caller may only ever touch a fraction of.
*
* Every mapping is placed inside the calling thread's own elfSlotSize
* slot (elf.h), specifically the sub-range [userSlotBase +
* elfSlotStackOffset, userSlotBase + elfSlotSize) -- everything from the
* top of the small, fixed-size user stack up to the end of the slot, 128
* MiB that no ELF LOAD segment or brk-growth ever reaches in this
* kernel's own binaries (elfSlotBrkOffset caps brk's own growth at 64
* MiB in, elfSlotStackOffset itself at 128 MiB in -- see elf.h). There is
* no address-space-wide mmap arena the way a real kernel's own mm_struct
* has one: with a single page table shared by every thread in the
* system, "the whole address space" is not a per-thread concept to begin
* with, so mmap here is scoped exactly the way brk/the stack already are.
*/
static uint64 pageAlignDown(uint64 x)
{
return x & ~(uint64)(frameSize - 1);
}
static uint64 pageAlignUp(uint64 x)
{
return (x + frameSize - 1) & ~(uint64)(frameSize - 1);
}
static uint64 mmapRegionStart(struct Thread* thread)
{
return thread->userSlotBase + elfSlotStackOffset;
}
static uint64 mmapRegionEnd(struct Thread* thread)
{
return thread->userSlotBase + elfSlotSize;
}
static bool rangesOverlap(uint64 aStart, uint64 aEnd, uint64 bStart, uint64 bEnd)
{
return aStart < bEnd && bStart < aEnd;
}
Vma* mmapFind(struct Thread* thread, uint64 addr)
{
for (int i = 0; i < maxVmasPerThread; i++) {
Vma* v = &thread->vmas[i];
if (v->used && addr >= v->start && addr < v->end)
return v;
}
return nullptr;
}
static Vma* findFreeVmaSlot(struct Thread* thread)
{
for (int i = 0; i < maxVmasPerThread; i++)
if (!thread->vmas[i].used)
return &thread->vmas[i];
return nullptr;
}
static bool regionFree(struct Thread* thread, uint64 start, uint64 end)
{
for (int i = 0; i < maxVmasPerThread; i++) {
Vma* v = &thread->vmas[i];
if (v->used && rangesOverlap(start, end, v->start, v->end))
return false;
}
return true;
}
/* First-fit search for `length` free bytes inside thread's own mmap
region: candidate gaps are the region's own start plus every existing
Vma's own end, sorted into address order (a plain insertion sort --
maxVmasPerThread's small fixed bound makes anything fancier pointless),
then the first candidate `length` bytes actually fits at wins. */
static uint64 findFreeRegion(struct Thread* thread, uint64 length)
{
uint64 regionStart = mmapRegionStart(thread);
uint64 regionEnd = mmapRegionEnd(thread);
uint64 candidates[maxVmasPerThread + 1];
int candidateCount = 0;
candidates[candidateCount++] = regionStart;
for (int i = 0; i < maxVmasPerThread; i++)
if (thread->vmas[i].used)
candidates[candidateCount++] = thread->vmas[i].end;
for (int i = 1; i < candidateCount; i++) {
uint64 key = candidates[i];
int j = i - 1;
while (j >= 0 && candidates[j] > key) {
candidates[j + 1] = candidates[j];
j--;
}
candidates[j + 1] = key;
}
for (int i = 0; i < candidateCount; i++) {
uint64 candidate = candidates[i];
if (candidate < regionStart)
continue;
if (candidate + length < candidate || candidate + length > regionEnd)
continue;
if (regionFree(thread, candidate, candidate + length))
return candidate;
}
return 0;
}
bool mmapReserve(struct Thread* thread, uint64 addr, uint64 length, uint32 prot,
bool anonymous, int fd, uint64 fileOffset, uint64* outAddr)
{
if (length == 0)
return false;
if (addr % frameSize != 0 || length % frameSize != 0)
return false;
if (!anonymous && fileOffset % frameSize != 0)
return false;
uint64 start = addr;
if (start == 0) {
start = findFreeRegion(thread, length);
if (start == 0)
return false;
}
uint64 end = start + length;
if (end < start)
return false;
if (start < mmapRegionStart(thread) || end > mmapRegionEnd(thread))
return false;
if (!regionFree(thread, start, end))
return false;
Vma* slot = findFreeVmaSlot(thread);
if (!slot)
return false;
slot->used = true;
slot->start = start;
slot->end = end;
slot->prot = prot;
slot->anonymous = anonymous;
slot->fd = anonymous ? -1 : fd;
slot->fileOffset = anonymous ? 0 : fileOffset;
*outAddr = start;
return true;
}
bool mmapRelease(struct Thread* thread, uint64 addr, uint64 length)
{
if (length == 0 || addr % frameSize != 0)
return false;
uint64 unmapStart = addr;
uint64 unmapEnd = addr + pageAlignUp(length);
if (unmapEnd < unmapStart)
return false;
/* Up-front check: refuse the WHOLE call if releasing this range would
need to split a Vma in the middle (touching neither end) and no free
slot exists to hold the resulting upper half. A thread's own Vmas
never overlap each other, so at most one can ever need splitting. */
for (int i = 0; i < maxVmasPerThread; i++) {
Vma* v = &thread->vmas[i];
if (v->used && v->start < unmapStart && v->end > unmapEnd && !findFreeVmaSlot(thread))
return false;
}
for (int i = 0; i < maxVmasPerThread; i++) {
Vma* v = &thread->vmas[i];
if (!v->used || !rangesOverlap(v->start, v->end, unmapStart, unmapEnd))
continue;
uint64 freeFrom = v->start > unmapStart ? v->start : unmapStart;
uint64 freeTo = v->end < unmapEnd ? v->end : unmapEnd;
pagingUnmapUserRange(freeFrom, freeTo - freeFrom);
if (v->start >= unmapStart && v->end <= unmapEnd) {
v->used = false;
} else if (v->start < unmapStart && v->end > unmapEnd) {
Vma* upper = findFreeVmaSlot(thread);
*upper = *v;
upper->start = unmapEnd;
v->end = unmapStart;
} else if (v->start < unmapStart) {
v->end = unmapStart;
} else {
uint64 trimmed = unmapEnd - v->start;
if (!v->anonymous)
v->fileOffset += trimmed;
v->start = unmapEnd;
}
}
return true;
}
bool mmapFaultHandler(struct Thread* thread, uint64 faultAddr, bool forWrite)
{
Vma* v = mmapFind(thread, faultAddr);
if (!v)
return false;
if (forWrite && !(v->prot & mmapProtWrite))
return false;
if (!forWrite && !(v->prot & mmapProtRead))
return false;
uint64 pageAddr = pageAlignDown(faultAddr);
if (pagingResolve(pageAddr) != 0)
return true;
uint64 frame = pmmAlloc();
if (frame == 0)
return false;
memset((void*)frame, 0, frameSize);
if (!v->anonymous) {
FileDescriptor* fdEntry = &thread->fds[v->fd];
if (fdEntry->kind == fdFree) {
pmmFree(frame);
return false;
}
uint64 savedOffset = fdEntry->offset;
fdEntry->offset = v->fileOffset + (pageAddr - v->start);
vfsRead(v->fd, (void*)frame, frameSize);
fdEntry->offset = savedOffset;
}
uint64 flags = pageUser | ((v->prot & mmapProtWrite) ? pageWrite : 0);
if (!(v->prot & mmapProtExec))
flags |= pageNoExecute;
if (!pagingMap(pageAddr, frame, flags)) {
pmmFree(frame);
return false;
}
return true;
}
void mmapReleaseAll(struct Thread* thread)
{
memset(thread->vmas, 0, sizeof(thread->vmas));
}
+320
View File
@@ -0,0 +1,320 @@
#include "ktest.h"
#include "mmap.h"
#include "sched.h"
#include "elf.h"
#include "paging.h"
#include "pmm.h"
#include "vfs.h"
#include "fs.h"
#include "string.h"
/*
* Coverage for mmap.c's real demand-paged mmap(2)/munmap(2) -- run entirely
* on the idle thread (KTEST always runs there, before schedStart(), see
* ktest.h's own file comment), whose real userSlotBase is 0 (a kernel
* thread owns no user slot). Every test below temporarily repoints it at
* mmapTestSlotBase, a 256 MiB (elfSlotSize)-aligned scratch address far
* above any real binary's USER_BASE (Makefile's highest is currently under
* 0x600000000, ~24 GiB) and far above pagingTest.c's own scratch range
* (0x200000000-0x202000000, ~8 GiB) so the two files can never collide even
* if their tests interleave, then restores it to 0 before returning --
* exactly the "explicit, restorable state" every other KTEST here already
* insists on rather than depending on hidden global setup.
*/
#define mmapTestSlotBase 0x300000000000ull
static uint64 setupSlot(Thread* t)
{
uint64 previous = t->userSlotBase;
t->userSlotBase = mmapTestSlotBase;
mmapReleaseAll(t);
return previous;
}
static void teardownSlot(Thread* t, uint64 previous)
{
pagingUnmapUserRange(mmapTestSlotBase, elfSlotSize);
mmapReleaseAll(t);
t->userSlotBase = previous;
}
KTEST(mmapReserveRejectsMisalignedZeroAndOverlap)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_FALSE(t, mmapReserve(self, 1, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, 0, 0, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, mmapTestSlotBase + elfSlotStackOffset, frameSize + 1, mmapProtRead, true, -1, 0, &addr));
uint64 base = mmapTestSlotBase + elfSlotStackOffset;
KT_TRUE(t, mmapReserve(self, base, 2 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_EQU(t, addr, base);
/* Overlapping the just-reserved [base, base+2*frameSize) from either
side must be refused -- mmapReserve never silently merges/replaces. */
KT_FALSE(t, mmapReserve(self, base + frameSize, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, base - frameSize, 2 * frameSize, mmapProtRead, true, -1, 0, &addr));
/* Outside the mmap region entirely (below stackTop, where the real
stack itself lives) is refused regardless of alignment. */
KT_FALSE(t, mmapReserve(self, mmapTestSlotBase, frameSize, mmapProtRead, true, -1, 0, &addr));
teardownSlot(self, previous);
}
KTEST(mmapReserveAutoPlacementIsFirstFitAndNonOverlapping)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 firstAddr, secondAddr;
KT_TRUE(t, mmapReserve(self, 0, 3 * frameSize, mmapProtRead, true, -1, 0, &firstAddr));
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_EQU(t, firstAddr, mmapTestSlotBase + elfSlotStackOffset);
KT_EQU(t, secondAddr, firstAddr + 3 * frameSize);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerAnonymousDemandZeroFillThenWritable)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
/* Nothing mapped yet -- mmapReserve never eagerly allocates. */
KT_EQ(t, pagingResolve(addr), 0);
KT_TRUE(t, mmapFaultHandler(self, addr + 42, false));
KT_NE(t, pagingResolve(addr), 0);
/* Demand-zero: untouched content must actually be zero, not whatever
garbage the frame's previous owner (if any) left behind. */
uint8* bytes = (uint8*)addr;
for (int i = 0; i < 64; i++)
KT_EQ(t, bytes[i], 0);
/* A write fault on an already-present page (mmapProtWrite granted at
reserve time) is simply a no-op success -- the page was already
mapped pageWrite by the read fault above. */
KT_TRUE(t, mmapFaultHandler(self, addr, true));
bytes[0] = 0xAB;
KT_EQ(t, bytes[0], 0xAB);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerSetsNoExecuteUnlessProtExecGranted)
{
if (!pagingNxSupported())
return;
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 noExecAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &noExecAddr));
KT_TRUE(t, mmapFaultHandler(self, noExecAddr, false));
KT_FALSE(t, pagingIsExecutable(noExecAddr));
uint64 execAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite | mmapProtExec, true, -1, 0, &execAddr));
KT_TRUE(t, mmapFaultHandler(self, execAddr, false));
KT_TRUE(t, pagingIsExecutable(execAddr));
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesWriteAgainstReadOnlyMapping)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
/* A read fault is fine (mmapProtRead granted)... */
KT_TRUE(t, mmapFaultHandler(self, addr, false));
/* ...but a write fault against the SAME already-reserved range, on a
fresh page that never got mapped by the read above, must be refused
-- this is exactly the case isr.c turns into a real SIGSEGV. */
uint64 secondAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_FALSE(t, mmapFaultHandler(self, secondAddr, true));
KT_EQ(t, pagingResolve(secondAddr), 0);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesAddressOutsideAnyVma)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
KT_FALSE(t, mmapFaultHandler(self, mmapTestSlotBase + elfSlotStackOffset + 5 * frameSize, false));
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerFileBackedReadsRealFileContent)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
const char* payload = "mmapTest file-backed page content, byte-for-byte.";
int fd = vfsOpen("/tmp/mmaptest-src.txt", oReadWrite | oCreate | oTrunc);
KT_TRUE(t, fd >= 0);
KT_EQ(t, vfsWrite(fd, payload, strlen(payload)), (int64)strlen(payload));
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, false, fd, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, memcmp((const void*)addr, payload, strlen(payload)) == 0);
/* Past end-of-file within the same page: zero-padded, not garbage. */
KT_EQ(t, ((const uint8*)addr)[strlen(payload)], 0);
/* The fd's own seek offset must be exactly what it was before the
fault -- mmapFaultHandler saves/restores it around its own read, so
a caller's ordinary vfsRead/vfsLseek use of the same fd is never
disturbed by an unrelated page fault. */
KT_EQ(t, vfsLseek(fd, 0, seekCur), (int64)strlen(payload));
vfsClose(fd);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesFileBackedReadAfterFdClosed)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
int fd = vfsOpen("/tmp/mmaptest-closed.txt", oReadWrite | oCreate | oTrunc);
KT_TRUE(t, fd >= 0);
KT_EQ(t, vfsWrite(fd, "x", 1), 1);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, false, fd, 0, &addr));
vfsClose(fd);
/* Honestly scoped limitation (see mmap.h's own Vma doc comment): this
mapping cannot outlive the fd that backs it. A fault against it once
the fd is gone must fail cleanly, never silently serve zeroed or
stale data as if it had succeeded. */
KT_FALSE(t, mmapFaultHandler(self, addr, false));
KT_EQ(t, pagingResolve(addr), 0);
teardownSlot(self, previous);
}
KTEST(mmapReleaseFreesFramesAndAllowsReReservation)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 framesBefore = pmmFreeFrames();
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, 2 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, mmapFaultHandler(self, addr + frameSize, false));
KT_LE(t, pmmFreeFrames(), framesBefore - 2);
KT_TRUE(t, mmapRelease(self, addr, 2 * frameSize));
KT_EQU(t, pmmFreeFrames(), framesBefore);
KT_EQ(t, pagingResolve(addr), 0);
KT_EQ(t, pagingResolve(addr + frameSize), 0);
KT_NULL(t, mmapFind(self, addr));
/* The address range is genuinely free again, not just unmapped. */
uint64 secondAddr;
KT_TRUE(t, mmapReserve(self, addr, 2 * frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_EQU(t, secondAddr, addr);
teardownSlot(self, previous);
}
KTEST(mmapReleaseOnNeverMappedRangeIsSafeNoop)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 framesBefore = pmmFreeFrames();
uint64 probe = mmapTestSlotBase + elfSlotStackOffset + 10 * frameSize;
KT_TRUE(t, mmapRelease(self, probe, 3 * frameSize));
KT_EQU(t, pmmFreeFrames(), framesBefore);
teardownSlot(self, previous);
}
KTEST(mmapReleaseSplitsMiddleOfAVmaIntoTwo)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, 3 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, mmapFaultHandler(self, addr + frameSize, false));
KT_TRUE(t, mmapFaultHandler(self, addr + 2 * frameSize, false));
/* Unmap only the middle page -- the low and high pages must survive,
untouched, as two now-independent Vmas. */
KT_TRUE(t, mmapRelease(self, addr + frameSize, frameSize));
KT_NE(t, pagingResolve(addr), 0);
KT_EQ(t, pagingResolve(addr + frameSize), 0);
KT_NE(t, pagingResolve(addr + 2 * frameSize), 0);
KT_NOTNULL(t, mmapFind(self, addr));
KT_NULL(t, mmapFind(self, addr + frameSize));
KT_NOTNULL(t, mmapFind(self, addr + 2 * frameSize));
/* The middle gap is genuinely free address space again. */
uint64 refilled;
KT_TRUE(t, mmapReserve(self, addr + frameSize, frameSize, mmapProtRead, true, -1, 0, &refilled));
teardownSlot(self, previous);
}
KTEST(mmapReserveFailsOnceVmaTableIsFull)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
for (int i = 0; i < maxVmasPerThread; i++)
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
teardownSlot(self, previous);
}
KTEST(mmapReleaseAllClearsBookkeepingOnly)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_NE(t, pagingResolve(addr), 0);
mmapReleaseAll(self);
KT_NULL(t, mmapFind(self, addr));
/* mmapReleaseAll deliberately does not touch the page table -- callers
that need the frame back reclaim it their own way first (doExec).
The frame is still really mapped; teardownSlot's own whole-region
pagingUnmapUserRange (not mmapReleaseAll again) is what reclaims it
for this test. */
KT_NE(t, pagingResolve(addr), 0);
teardownSlot(self, previous);
}
+161 -3
View File
@@ -6,6 +6,20 @@
#define entryPageSize 0x80 #define entryPageSize 0x80
#define addressMask 0x000FFFFFFFFFF000ull #define addressMask 0x000FFFFFFFFFF000ull
/* Set by boot.S once, before kmain ever runs: 1 if CPUID.80000001H:EDX.NX
was reported AND IA32_EFER.NXE was actually written, 0 otherwise (either
the CPU never claimed the feature, or -- unreachable on any CPU that
passed this kernel's own checkLongMode, but checked independently and
honestly anyway -- some hypervisor quirk refused it). A plain uint32,
not this codebase's own enum bool, so boot.S's 32-bit `movl` store has
an unambiguous, non-implementation-defined width to write into. */
extern uint32 cpuNxSupported;
bool pagingNxSupported(void)
{
return cpuNxSupported != 0;
}
static uint64 readCr3(void) static uint64 readCr3(void)
{ {
uint64 value; uint64 value;
@@ -65,11 +79,72 @@ bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags)
if (!pt) if (!pt)
return false; return false;
pt[ptIndex] = (physicalAddr & addressMask) | (flags & 0xFFF) | pagePresent; /* flags & 0xFFF keeps the low-12-bits PTE flags (pagePresent/pageWrite/
pageUser, plus whatever hardware-ignored bits a caller might pass);
pageNoExecute (bit 63) is a completely separate mask so it survives
that truncation -- silently dropped rather than written when
pagingNxSupported() is false, since bit 63 of a PTE is architecturally
reserved (must be 0) whenever IA32_EFER.NXE is clear, and setting a
reserved bit faults. */
uint64 leafFlags = (flags & 0xFFF) | pagePresent;
if ((flags & pageNoExecute) && pagingNxSupported())
leafFlags |= pageNoExecute;
pt[ptIndex] = (physicalAddr & addressMask) | leafFlags;
invalidatePage(virtualAddr); invalidatePage(virtualAddr);
return true; return true;
} }
bool pagingIsExecutable(uint64 virtualAddr)
{
uint64 pml4Index = (virtualAddr >> 39) & 0x1FF;
uint64 pdptIndex = (virtualAddr >> 30) & 0x1FF;
uint64 pdIndex = (virtualAddr >> 21) & 0x1FF;
uint64 ptIndex = (virtualAddr >> 12) & 0x1FF;
uint64* pml4 = tableFrom(readCr3());
uint64* pdpt = nextTable(pml4, pml4Index, 0, false);
if (!pdpt)
return false;
uint64* pd = nextTable(pdpt, pdptIndex, 0, false);
if (!pd)
return false;
if (pd[pdIndex] & entryPageSize)
return (pd[pdIndex] & entryPresent) && !(pd[pdIndex] & pageNoExecute);
uint64* pt = nextTable(pd, pdIndex, 0, false);
if (!pt)
return false;
if (!(pt[ptIndex] & entryPresent))
return false;
return !(pt[ptIndex] & pageNoExecute);
}
bool pagingIsWritable(uint64 virtualAddr)
{
uint64 pml4Index = (virtualAddr >> 39) & 0x1FF;
uint64 pdptIndex = (virtualAddr >> 30) & 0x1FF;
uint64 pdIndex = (virtualAddr >> 21) & 0x1FF;
uint64 ptIndex = (virtualAddr >> 12) & 0x1FF;
uint64* pml4 = tableFrom(readCr3());
uint64* pdpt = nextTable(pml4, pml4Index, 0, false);
if (!pdpt)
return false;
uint64* pd = nextTable(pdpt, pdptIndex, 0, false);
if (!pd)
return false;
if (pd[pdIndex] & entryPageSize)
return (pd[pdIndex] & entryPresent) && (pd[pdIndex] & pageWrite);
uint64* pt = nextTable(pd, pdIndex, 0, false);
if (!pt)
return false;
if (!(pt[ptIndex] & entryPresent))
return false;
return (pt[ptIndex] & pageWrite) != 0;
}
uint64 pagingResolve(uint64 virtualAddr) uint64 pagingResolve(uint64 virtualAddr)
{ {
uint64 pml4Index = (virtualAddr >> 39) & 0x1FF; uint64 pml4Index = (virtualAddr >> 39) & 0x1FF;
@@ -95,7 +170,7 @@ uint64 pagingResolve(uint64 virtualAddr)
return (pt[ptIndex] & addressMask) + (virtualAddr & 0xFFF); return (pt[ptIndex] & addressMask) + (virtualAddr & 0xFFF);
} }
bool pagingMapUserRange(uint64 virtualAddr, uint64 size) bool pagingMapUserRangeExact(uint64 virtualAddr, uint64 size, uint64 flags)
{ {
uint64 first = virtualAddr & ~(uint64)(frameSize - 1); uint64 first = virtualAddr & ~(uint64)(frameSize - 1);
uint64 last = (virtualAddr + size + frameSize - 1) & ~(uint64)(frameSize - 1); uint64 last = (virtualAddr + size + frameSize - 1) & ~(uint64)(frameSize - 1);
@@ -106,9 +181,92 @@ bool pagingMapUserRange(uint64 virtualAddr, uint64 size)
uint64 frame = pmmAlloc(); uint64 frame = pmmAlloc();
if (frame == 0) if (frame == 0)
return false; return false;
if (!pagingMap(page, frame, pageUser | pageWrite)) if (!pagingMap(page, frame, pageUser | flags))
return false; return false;
memset((void*)page, 0, frameSize); memset((void*)page, 0, frameSize);
} }
return true; return true;
} }
bool pagingMapUserRangeFlags(uint64 virtualAddr, uint64 size, uint64 extraFlags)
{
return pagingMapUserRangeExact(virtualAddr, size, pageWrite | extraFlags);
}
bool pagingMapUserRange(uint64 virtualAddr, uint64 size)
{
return pagingMapUserRangeFlags(virtualAddr, size, 0);
}
/* True if every one of a table's 512 entries is not-present -- used to
decide whether a page-table page that unmapping just emptied out can be
given back to the pmm itself, not just the leaf data frame it pointed
to. Must scan the whole table, not just the entry this call touched: a
PD table in particular can be shared by several unrelated elfSlotSize
slots at once (a PDPT entry spans 1GB, four times a 256 MiB slot), so
only a table that is genuinely empty end-to-end is safe to free. */
static bool tableEmpty(const uint64* table)
{
for (uint64 i = 0; i < 512; i++)
if (table[i] & entryPresent)
return false;
return true;
}
static void unmapOnePage(uint64 virtualAddr)
{
uint64 pml4Index = (virtualAddr >> 39) & 0x1FF;
uint64 pdptIndex = (virtualAddr >> 30) & 0x1FF;
uint64 pdIndex = (virtualAddr >> 21) & 0x1FF;
uint64 ptIndex = (virtualAddr >> 12) & 0x1FF;
uint64* pml4 = tableFrom(readCr3());
uint64* pdpt = nextTable(pml4, pml4Index, 0, false);
if (!pdpt)
return;
uint64* pd = nextTable(pdpt, pdptIndex, 0, false);
if (!pd)
return;
if (pd[pdIndex] & entryPageSize)
return;
uint64* pt = nextTable(pd, pdIndex, 0, false);
if (!pt)
return;
if (!(pt[ptIndex] & entryPresent))
return;
pmmFree(pt[ptIndex] & addressMask);
pt[ptIndex] = 0;
invalidatePage(virtualAddr);
/* Bottom-up structural cleanup: a PT/PD page-table page is itself just
a pmm frame (see nextTable's create path), and every page
pagingMapUserRange ever maps allocates one on demand -- leaving them
behind once the last leaf entry they held is gone would be a second,
smaller leak underneath the one this function exists to fix.
Deliberately stops at PD: reclaiming a PDPT or the PML4 itself would
mean tearing down a structure shared by up to 512GB / 1GB of
unrelated address space respectively, which nothing in this kernel's
single shared page table can ever actually make fully empty (the
kernel's own mappings always live somewhere under the same PML4
entry as every user slot). */
if (tableEmpty(pt)) {
pmmFree((uint64)pt);
pd[pdIndex] = 0;
if (tableEmpty(pd)) {
pmmFree((uint64)pd);
pdpt[pdptIndex] = 0;
}
}
}
void pagingUnmapUserRange(uint64 virtualAddr, uint64 size)
{
uint64 first = virtualAddr & ~(uint64)(frameSize - 1);
uint64 last = (virtualAddr + size + frameSize - 1) & ~(uint64)(frameSize - 1);
for (uint64 page = first; page < last; page += frameSize)
unmapOnePage(page);
}
+150
View File
@@ -0,0 +1,150 @@
#include "ktest.h"
#include "paging.h"
#include "pmm.h"
/*
* Coverage for pagingUnmapUserRange (paging.c) -- the inverse of
* pagingMapUserRange, added so threadReap/threadDiscard (sched.c) and
* doExec (syscall.c) can actually give a dead or replaced process's
* physical frames back to the pmm instead of leaking them forever (every
* spawn/exec before this used to permanently consume frames, since
* frostbyte's single shared page table is never touched on thread death).
*
* Addresses here are picked far above the highest real USER_BASE slot any
* binary is linked at (0x40000000..0xF8000000, see Makefile/kmain.c) so
* these tests can never collide with a real program's mapping, and each
* test uses its own base so they cannot interfere with one another even
* if run out of link order.
*/
#define pagingTestBaseA 0x200000000ull
#define pagingTestBaseB 0x201000000ull
#define pagingTestBaseC 0x202000000ull
#define pagingTestBaseD 0x203000000ull
#define pagingTestBaseE 0x204000000ull
KTEST(pagingMapUserRangeThenUnmapUserRangeReturnsFramesToPmm)
{
uint64 before = pmmFreeFrames();
/* At least 3 frames for the data pages themselves -- pagingTestBaseA is
virgin address space, so the map also has to pay for whatever PDPT/
PD/PT page-table pages do not exist yet along the way (nextTable's
create path, paging.c), which this test does not try to predict the
exact count of. The unmap side below is the precise assertion: every
frame spent here, data or structural, must come all the way back. */
KT_TRUE(t, pagingMapUserRange(pagingTestBaseA, 3 * frameSize));
KT_LE(t, pmmFreeFrames(), before - 3);
for (uint64 i = 0; i < 3; i++)
KT_NE(t, pagingResolve(pagingTestBaseA + i * frameSize), 0);
pagingUnmapUserRange(pagingTestBaseA, 3 * frameSize);
KT_EQU(t, pmmFreeFrames(), before);
for (uint64 i = 0; i < 3; i++)
KT_EQ(t, pagingResolve(pagingTestBaseA + i * frameSize), 0);
}
KTEST(pagingUnmapUserRangeOnNeverMappedRangeIsSafeNoop)
{
uint64 before = pmmFreeFrames();
pagingUnmapUserRange(pagingTestBaseB, 5 * frameSize);
KT_EQU(t, pmmFreeFrames(), before);
for (uint64 i = 0; i < 5; i++)
KT_EQ(t, pagingResolve(pagingTestBaseB + i * frameSize), 0);
}
KTEST(pagingUnmapUserRangeOnlyFreesPagesActuallyPresent)
{
uint64 before = pmmFreeFrames();
uint64 frame0 = pmmAlloc();
uint64 frame2 = pmmAlloc();
KT_NE(t, frame0, 0);
KT_NE(t, frame2, 0);
KT_TRUE(t, pagingMap(pagingTestBaseC, frame0, pageUser | pageWrite));
KT_TRUE(t, pagingMap(pagingTestBaseC + 2 * frameSize, frame2, pageUser | pageWrite));
/* pagingTestBaseC + 1*frameSize and +3*frameSize deliberately left
unmapped -- a stand-in for the gaps a real elfSlotSize slot is full
of (a small program's image plus its stack cover only a handful of
the 65536 pages a whole slot spans). before already accounts for the
two explicit pmmAlloc calls above; pagingMap may spend a couple more
on fresh PD/PT structure for this virgin address, same caveat as the
first test in this file -- the unmap side is the exact assertion. */
KT_LE(t, pmmFreeFrames(), before - 2);
pagingUnmapUserRange(pagingTestBaseC, 4 * frameSize);
KT_EQU(t, pmmFreeFrames(), before);
for (uint64 i = 0; i < 4; i++)
KT_EQ(t, pagingResolve(pagingTestBaseC + i * frameSize), 0);
}
/*
* Coverage for pageNoExecute (paging.h) and its plumbing: pagingMap's own
* fix to stop masking bit 63 out of the leaf PTE it writes, and
* pagingMapUserRangeFlags/pagingIsExecutable, the two entry points
* mmap.c/kmain.c/syscall.c/jobctl.c/sched.c actually use. Only meaningful
* when pagingNxSupported() is true (boot.S's IA32_EFER.NXE, gated on
* CPUID.80000001H:EDX.NX) -- every QEMU/TCG target this project's own gate
* (tools/ci-test.sh) boots under reports the feature, but a KTEST skipping
* itself on hardware that genuinely lacks it is more honest than a KTEST
* that would otherwise assert a bit was set when pagingMap deliberately,
* correctly, refused to write it.
*/
KTEST(pagingMapUserRangeFlagsAppliesNoExecuteWhenNxSupported)
{
if (!pagingNxSupported())
return;
KT_TRUE(t, pagingMapUserRangeFlags(pagingTestBaseD, frameSize, 0));
KT_TRUE(t, pagingIsExecutable(pagingTestBaseD));
pagingUnmapUserRange(pagingTestBaseD, frameSize);
KT_TRUE(t, pagingMapUserRangeFlags(pagingTestBaseD, frameSize, pageNoExecute));
KT_FALSE(t, pagingIsExecutable(pagingTestBaseD));
pagingUnmapUserRange(pagingTestBaseD, frameSize);
}
KTEST(pagingIsExecutableIsFalseForUnmappedAddress)
{
KT_FALSE(t, pagingIsExecutable(pagingTestBaseD + 7 * frameSize));
}
/*
* Coverage for pagingMapUserRangeExact and pagingIsWritable: unlike
* pagingMapUserRangeFlags (which always forces pageWrite on), Exact is the
* one entry point that can produce a genuinely READ-ONLY user mapping --
* the primitive elf.c's elfLoad needs for a binary's own .text/.rodata
* PT_LOAD segment. Proven directly here via pagingIsWritable, the same
* "deterministic introspection instead of only a live fault" approach
* pagingIsExecutable already established above; user/elfwritevictim.c is
* the live, real-SIGSEGV proof that a genuinely read-only mapping refuses
* a ring-3 write, not just that this bit got written.
*/
KTEST(pagingMapUserRangeExactOmitsWriteWhenNotRequested)
{
KT_TRUE(t, pagingMapUserRangeExact(pagingTestBaseE, frameSize, 0));
KT_FALSE(t, pagingIsWritable(pagingTestBaseE));
KT_TRUE(t, pagingIsExecutable(pagingTestBaseE));
pagingUnmapUserRange(pagingTestBaseE, frameSize);
}
KTEST(pagingMapUserRangeExactAppliesWriteAndNoExecuteTogether)
{
KT_TRUE(t, pagingMapUserRangeExact(pagingTestBaseE, frameSize, pageWrite | pageNoExecute));
KT_TRUE(t, pagingIsWritable(pagingTestBaseE));
if (pagingNxSupported())
KT_FALSE(t, pagingIsExecutable(pagingTestBaseE));
pagingUnmapUserRange(pagingTestBaseE, frameSize);
}
KTEST(pagingIsWritableIsFalseForUnmappedAddress)
{
KT_FALSE(t, pagingIsWritable(pagingTestBaseE + 7 * frameSize));
}
+67
View File
@@ -0,0 +1,67 @@
#include "pidfd.h"
#include "sched.h"
typedef struct PidfdEntry {
bool used;
uint32 targetId;
uint32 targetGeneration;
} PidfdEntry;
static PidfdEntry pidfds[maxPidfds];
static PidfdEntry* pidfdAt(int handle)
{
if (handle < 0 || handle >= (int)maxPidfds)
return nullptr;
if (!pidfds[handle].used)
return nullptr;
return &pidfds[handle];
}
static int allocPidfdSlot(void)
{
for (int i = 0; i < (int)maxPidfds; i++)
if (!pidfds[i].used)
return i;
return -1;
}
int pidfdOpen(uint32 targetId)
{
if (targetId == 0 || targetId >= threadCapacity())
return -1;
if (threadAt(targetId)->state == threadUnused)
return -1;
int slot = allocPidfdSlot();
if (slot < 0)
return -1;
pidfds[slot].used = true;
pidfds[slot].targetId = targetId;
pidfds[slot].targetGeneration = threadAt(targetId)->generation;
return slot;
}
uint32 pidfdTarget(int handle)
{
PidfdEntry* p = pidfdAt(handle);
if (!p)
return threadInvalidId;
/* targetId was range/liveness-checked once, at pidfdOpen time, and
never changes afterward -- threadAt is safe here without repeating
that check. A mismatch means the slot has since been freed
(threadReap/threadDiscard's memset zeroes Thread.generation back to
0) or handed to an entirely different, unrelated thread. */
if (threadAt(p->targetId)->generation != p->targetGeneration)
return threadInvalidId;
return p->targetId;
}
void pidfdClose(int handle)
{
PidfdEntry* p = pidfdAt(handle);
if (!p)
return;
p->used = false;
}
+167
View File
@@ -0,0 +1,167 @@
#include "ktest.h"
#include "sched.h"
#include "pidfd.h"
#include "ports.h"
/*
* Pure logic tests for pidfd.h/pidfd.c: no live scheduler needed (same
* "runs before schedStart(), current thread is idle, every created thread
* stays Runnable/never-run for its whole life" setup schedTest.c's own
* threadCreate/threadDiscard tests already use), since every property here
* is about sched.c's Thread.generation bookkeeping, not about actually
* scheduling anything.
*
* The marquee test below (pidfdTargetDetectsSlotRecycledByAnUnrelatedThread)
* is the deterministic, pure-logic reproduction of the exact scenario
* killtest.c's own long-standing comment warns about but never guards
* against ("killing it again must fail cleanly, not resurrect a slot that
* has since been reused") and sched.c's own allocSlot doc comment
* documents as a real, previously-unfixed race: a stale handle must never
* silently act on whichever unrelated, live thread has since been handed
* the exact same numeric slot. A live boot cannot reliably force this
* exact collision on demand (allocSlot's round-robin cursor needs the
* WHOLE table full before the very next create is forced back onto one
* specific freed slot, see its own doc comment) -- exactly why this test
* uses the same fill-the-table technique schedTest.c's own
* threadCreateReusesTheOnlyFreedSlotOnceTheTableIsFull already established
* for proving deterministic slot reuse, rather than a live demo (see
* user/pidfdtest.c instead for the real ring-3 syscall-path proof of
* post-reap staleness detection alone).
*/
static void noopThreadEntry(void)
{
for (;;)
halt();
}
KTEST(pidfdOpenRejectsIdleOutOfRangeAndUnusedTargets)
{
KT_EQ(t, pidfdOpen(0), -1);
KT_EQ(t, pidfdOpen(threadCapacity()), -1);
KT_EQ(t, pidfdOpen(threadCapacity() + 1000), -1);
/* Same "freed slot stays genuinely Unused until the round-robin cursor
wraps back around to it" setup as schedTest.c's own
threadCreateSkipsAFreshlyFreedLowSlotInFavorOfMovingForward. */
uint32 a = threadCreate("pfA", noopThreadEntry);
uint32 b = threadCreate("pfB", noopThreadEntry);
uint32 c = threadCreate("pfC", noopThreadEntry);
KT_TRUE(t, threadDiscard(b));
KT_EQ(t, pidfdOpen(b), -1);
KT_TRUE(t, threadDiscard(a));
KT_TRUE(t, threadDiscard(c));
}
KTEST(pidfdOpenAndTargetRoundtripThenClose)
{
uint32 x = threadCreate("pfX", noopThreadEntry);
int handle = pidfdOpen(x);
KT_GE(t, handle, 0);
KT_EQU(t, pidfdTarget(handle), x);
pidfdClose(handle);
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
/* Closing twice must not corrupt anything else's slot. */
pidfdClose(handle);
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
KT_TRUE(t, threadDiscard(x));
}
KTEST(pidfdTargetDetectsDiscardedTarget)
{
uint32 x = threadCreate("pfDiscarded", noopThreadEntry);
int handle = pidfdOpen(x);
KT_GE(t, handle, 0);
KT_TRUE(t, threadDiscard(x));
/* threadDiscard's memset reset Thread.generation for slot x back to 0
-- pidfdTarget must notice immediately, with no new thread ever
having claimed the slot yet. */
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
pidfdClose(handle);
}
KTEST(pidfdTargetDetectsSlotRecycledByAnUnrelatedThread)
{
uint32 capacity = threadCapacity();
uint32 created[capacity];
uint32 count = 0;
while (count < capacity) {
uint32 id = threadCreate("pfFill", noopThreadEntry);
if (id == threadInvalidId)
break;
created[count++] = id;
}
KT_EQU(t, threadCreate("pfShouldFail", noopThreadEntry), threadInvalidId);
uint32 victim = created[count / 2];
int handle = pidfdOpen(victim);
KT_GE(t, handle, 0);
KT_EQU(t, pidfdTarget(handle), victim);
/* victim is the ONLY free slot afterward -- the very next create is
forced onto it regardless of allocSlot's own cursor position, the
exact same proof schedTest.c's
threadCreateReusesTheOnlyFreedSlotOnceTheTableIsFull already
establishes for the underlying thread-table mechanics alone. */
KT_TRUE(t, threadDiscard(victim));
uint32 impostor = threadCreate("pfImpostor", noopThreadEntry);
KT_EQ(t, impostor, victim);
/* The old handle must NOT follow the slot to its new, completely
unrelated occupant -- this is the entire point of pidfd.h over a
raw cached pid, which would now (harmlessly, in this test, but not
in general) apply any further kill/wait/sigSend straight to
impostor while believing it was still acting on victim. */
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
/* A FRESH pidfd opened on the same numeric id now correctly targets
the new occupant -- the slot itself is not broken, only the OLD
handle's own pinned incarnation is (rightly) stale. */
int freshHandle = pidfdOpen(impostor);
KT_GE(t, freshHandle, 0);
KT_EQU(t, pidfdTarget(freshHandle), impostor);
pidfdClose(handle);
pidfdClose(freshHandle);
for (uint32 i = 0; i < count; i++) {
if (created[i] != victim)
KT_TRUE(t, threadDiscard(created[i]));
}
KT_TRUE(t, threadDiscard(impostor));
}
KTEST(pidfdOpenExhaustsThenClosesFreeSlotsBackForReuse)
{
uint32 x = threadCreate("pfTableFill", noopThreadEntry);
int handles[maxPidfds];
int count = 0;
while (count < maxPidfds) {
int h = pidfdOpen(x);
if (h < 0)
break;
handles[count++] = h;
}
KT_EQ(t, count, maxPidfds);
KT_EQ(t, pidfdOpen(x), -1);
pidfdClose(handles[0]);
int reused = pidfdOpen(x);
KT_GE(t, reused, 0);
KT_EQU(t, pidfdTarget(reused), x);
pidfdClose(reused);
for (int i = 1; i < count; i++)
pidfdClose(handles[i]);
KT_TRUE(t, threadDiscard(x));
}
+291
View File
@@ -0,0 +1,291 @@
#include "pipe.h"
#include "ring.h"
#include "sched.h"
#include "ports.h"
typedef struct Pipe {
RingBuffer ring;
uint8 backing[pipeBufferSize];
uint32 readers;
uint32 writers;
bool used;
/* Set once, permanently (mirrors Thread.everRan, sched.h), the first
time a reader/writer is ever attached. A pipeCreateDetached handle
starts with both counts AND both of these false, so pipeRead/
pipeWrite can tell "the other end just has not attached yet" (keep
retrying) apart from "it attached and then went away" (true EOF /
broken pipe) -- pipeCreate sets both true immediately, since its
caller always installs both ends atomically, so its behavior is
completely unchanged. */
bool everHadReader;
bool everHadWriter;
/* Distinguishes THIS specific pipe living at a given slot from any other
pipe that has occupied (or will ever occupy) the same numeric handle
before or after -- pipeReleaseIfUnused frees a slot's `used` flag the
moment both ends are gone, so allocPipeSlot happily reuses that same
number for a completely unrelated pipeCreate/pipeCreateDetached right
after. A caller that caches a handle across time spanning such a
release (vfs.c's SFS-backed named-fifo bindings, see sfsFifoHandleFor
in vfs.c) needs more than pipeValid(handle) to tell "the same pipe I
bound to earlier" apart from "a different pipe that happens to have
been assigned the same number since" -- see pipeIncarnation. */
uint32 incarnation;
} Pipe;
static Pipe pipes[maxPipes];
/* Never 0 (see pipeIncarnation's "0 always means invalid" contract) -- next
value handed to whichever pipe allocPipeSlot's caller creates next,
regardless of which slot index it lands in. */
static uint32 nextPipeIncarnation = 1;
static Pipe* pipeAt(int handle)
{
if (handle < 0 || handle >= (int)maxPipes)
return nullptr;
if (!pipes[handle].used)
return nullptr;
return &pipes[handle];
}
static int allocPipeSlot(void)
{
for (int i = 0; i < (int)maxPipes; i++)
if (!pipes[i].used)
return i;
return -1;
}
int pipeCreate(void)
{
int i = allocPipeSlot();
if (i < 0)
return -1;
Pipe* p = &pipes[i];
ringInit(&p->ring, p->backing, pipeBufferSize);
p->readers = 1;
p->writers = 1;
p->everHadReader = true;
p->everHadWriter = true;
p->used = true;
p->incarnation = nextPipeIncarnation++;
return i;
}
int pipeCreateDetached(void)
{
int i = allocPipeSlot();
if (i < 0)
return -1;
Pipe* p = &pipes[i];
ringInit(&p->ring, p->backing, pipeBufferSize);
p->readers = 0;
p->writers = 0;
p->everHadReader = false;
p->everHadWriter = false;
p->used = true;
p->incarnation = nextPipeIncarnation++;
return i;
}
void pipeAddReader(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return;
p->readers++;
p->everHadReader = true;
}
void pipeAddWriter(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return;
p->writers++;
p->everHadWriter = true;
}
bool pipeWriteBroken(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return true;
return p->readers == 0 && p->everHadReader;
}
const void* pipeChannel(int handle)
{
return (const void*)pipeAt(handle);
}
bool pipeValid(int handle)
{
return pipeAt(handle) != nullptr;
}
uint32 pipeIncarnation(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return 0;
return p->incarnation;
}
int64 pipeRead(int handle, void* buffer, uint64 length)
{
Pipe* p = pipeAt(handle);
if (!p)
return -1;
if (length == 0)
return 0;
if (!ringIsEmpty(&p->ring)) {
usize got = ringRead(&p->ring, (uint8*)buffer, (usize)length);
/* Reading frees ring space: any writer blocked on "buffer full" may
now be able to proceed, so give it a chance to recheck. */
schedWakeChannel((const void*)p);
schedWakeChannel(schedPollChannel());
return (int64)got;
}
/* Only a writer that WAS attached and has since gone away means true
EOF -- a pipeCreateDetached handle nobody has written to yet (readers
0, everHadWriter false) has to keep reporting "try again" instead,
or a reader that races ahead of the writer's own vfsPipeAttach would
see spurious end-of-file on an otherwise perfectly live pipe. */
if (p->writers == 0 && p->everHadWriter)
return 0;
return -1;
}
int64 pipeWrite(int handle, const void* buffer, uint64 length)
{
Pipe* p = pipeAt(handle);
if (!p)
return -1;
/* Symmetric with pipeRead's EOF check above: only a reader that WAS
attached and left counts as a broken pipe. */
if (p->readers == 0 && p->everHadReader)
return -1;
if (length == 0)
return 0;
if (ringIsFull(&p->ring))
return -1;
usize put = ringWrite(&p->ring, (const uint8*)buffer, (usize)length);
/* Writing adds ring data: any reader blocked on "buffer empty" may now
be able to proceed, so give it a chance to recheck. */
schedWakeChannel((const void*)p);
schedWakeChannel(schedPollChannel());
return (int64)put;
}
bool pipeHasData(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return false;
return !ringIsEmpty(&p->ring) || (p->writers == 0 && p->everHadWriter);
}
bool pipeHasSpace(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return false;
return !ringIsFull(&p->ring);
}
int64 pipeReadBlocking(int handle, void* buffer, uint64 length)
{
Pipe* p = pipeAt(handle);
if (!p)
return -1;
for (;;) {
int64 result = pipeRead(handle, buffer, length);
if (result != -1)
return result;
schedBlockCurrent((const void*)p);
halt();
}
}
int64 pipeWriteBlocking(int handle, const void* buffer, uint64 length)
{
Pipe* p = pipeAt(handle);
if (!p)
return -1;
for (;;) {
int64 result = pipeWrite(handle, buffer, length);
if (result != -1)
return result;
/* A reader that attached and then left is a broken pipe: a terminal
condition no amount of waiting will ever resolve, unlike a merely
full buffer (or a pipeCreateDetached handle whose reader simply
has not attached yet). */
if (pipeWriteBroken(handle))
return -1;
schedBlockCurrent((const void*)p);
halt();
}
}
static void pipeReleaseIfUnused(int handle)
{
Pipe* p = &pipes[handle];
if (p->readers == 0 && p->writers == 0)
p->used = false;
}
void pipeCloseRead(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return;
if (p->readers > 0)
p->readers--;
/* A writer parked on "buffer full" must be woken to discover the pipe is
now broken instead of waiting forever for space that will never free
up on its own. */
if (p->readers == 0) {
schedWakeChannel((const void*)p);
schedWakeChannel(schedPollChannel());
}
pipeReleaseIfUnused(handle);
}
void pipeCloseWrite(int handle)
{
Pipe* p = pipeAt(handle);
if (!p)
return;
if (p->writers > 0)
p->writers--;
/* A reader parked on "buffer empty" must be woken to discover true
end-of-file instead of waiting forever for data that will never
arrive. */
if (p->writers == 0) {
schedWakeChannel((const void*)p);
schedWakeChannel(schedPollChannel());
}
pipeReleaseIfUnused(handle);
}
bool pipeDestroyIfUnattached(int handle)
{
Pipe* p = pipeAt(handle);
if (!p || p->readers != 0 || p->writers != 0)
return false;
p->used = false;
return true;
}
+113
View File
@@ -0,0 +1,113 @@
#include "ktest.h"
#include "pipe.h"
#include "string.h"
/*
* pipeReadBlocking/pipeWriteBlocking only return without parking the thread
* when the very first attempt already resolves (data/space/EOF/broken-pipe
* already true) - any scenario that would genuinely need to park calls
* halt(), which requires a live, interrupt-driven scheduler to ever resume
* from. ktestRunAll() runs before schedStart() (see ktest.h) on the sole
* execution context there is, so these tests deliberately only cover the
* "resolves immediately, never blocks" paths. The genuine cross-thread
* park/wake handshake is exercised for real at boot by the producer/consumer
* demo in kmain.c (needle "pipe demo: eof, count=10" in tools/ci-test.sh).
*/
KTEST(pipeReadBlockingReturnsQueuedDataImmediately)
{
int h = pipeCreate();
KT_GE(t, h, 0);
const char* msg = "already here";
KT_EQ(t, pipeWrite(h, msg, strlen(msg)), (int64)strlen(msg));
char buffer[32];
memset(buffer, 0, sizeof(buffer));
int64 got = pipeReadBlocking(h, buffer, sizeof(buffer));
KT_EQ(t, got, (int64)strlen(msg));
KT_TRUE(t, memcmp(buffer, msg, strlen(msg)) == 0);
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeReadBlockingReturnsEofWithoutParking)
{
int h = pipeCreate();
KT_GE(t, h, 0);
pipeCloseWrite(h);
char buffer[8];
KT_EQ(t, pipeReadBlocking(h, buffer, sizeof(buffer)), 0);
pipeCloseRead(h);
}
KTEST(pipeReadBlockingRejectsInvalidHandleWithoutParking)
{
char buffer[8];
KT_EQ(t, pipeReadBlocking(-1, buffer, sizeof(buffer)), -1);
KT_EQ(t, pipeReadBlocking(999, buffer, sizeof(buffer)), -1);
}
KTEST(pipeWriteBlockingSucceedsImmediatelyWhenRoomExists)
{
int h = pipeCreate();
KT_GE(t, h, 0);
const char* msg = "room to spare";
int64 written = pipeWriteBlocking(h, msg, strlen(msg));
KT_EQ(t, written, (int64)strlen(msg));
char buffer[32];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), (int64)strlen(msg));
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeWriteBlockingOnBrokenPipeReturnsWithoutParking)
{
int h = pipeCreate();
KT_GE(t, h, 0);
pipeCloseRead(h);
const char* msg = "nobody listening";
KT_EQ(t, pipeWriteBlocking(h, msg, strlen(msg)), -1);
pipeCloseWrite(h);
}
KTEST(pipeWriteBlockingRejectsInvalidHandleWithoutParking)
{
char buffer[4] = { 0 };
KT_EQ(t, pipeWriteBlocking(-1, buffer, sizeof(buffer)), -1);
KT_EQ(t, pipeWriteBlocking(999, buffer, sizeof(buffer)), -1);
}
KTEST(pipeWriteBlockingSucceedsRightAfterDrainFreesRoom)
{
int h = pipeCreate();
KT_GE(t, h, 0);
uint8 chunk[pipeBufferSize];
memset(chunk, 0x5A, sizeof(chunk));
KT_EQ(t, pipeWrite(h, chunk, sizeof(chunk)), (int64)sizeof(chunk));
/* buffer is completely full: a blocking write would have to park here if
nothing ever drained it, so proving this call returns at all (rather
than hanging the whole test run) is exactly the point. */
uint8 drained[16];
KT_EQ(t, pipeRead(h, drained, sizeof(drained)), (int64)sizeof(drained));
int64 written = pipeWriteBlocking(h, chunk, sizeof(drained));
KT_EQ(t, written, (int64)sizeof(drained));
pipeCloseRead(h);
pipeCloseWrite(h);
}
+171
View File
@@ -0,0 +1,171 @@
#include "ktest.h"
#include "pipe.h"
#include "vfs.h"
#include "string.h"
/*
* Coverage for the shared-pipe machinery added this wave (pipe.h's
* pipeCreateDetached/pipeAddReader/pipeAddWriter/pipeWriteBroken/
* pipeChannel, plus vfs.h's vfsPipeCreateHandle/vfsPipeAttach/
* vfsBlockingChannel) -- everything two independently spawned processes
* need to rendezvous over a real blocking pipe without fork(). The genuine
* cross-process park/wake handshake itself only runs for real at boot
* (fifomaster.c spawning fifowriter.c/fiforeader.c, needles
* "fifomaster: pipeshare ok"/"fiforeader: payload-ok" in tools/ci-test.sh),
* the same division of labor pipeBlockTest.c already uses for
* pipeReadBlocking/pipeWriteBlocking: what is deterministic and
* scheduler-free lives here, what genuinely needs two live execution
* contexts is proven live.
*/
KTEST(pipeCreateDetachedStartsWithNeitherEndAttached)
{
int h = pipeCreateDetached();
KT_GE(t, h, 0);
/* Nobody has attached a writer yet: a read must report "try again"
(-1), not spurious end-of-file (0) the way a writers-already-0
pipeCreate() pipe correctly would. */
char buffer[8];
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), -1);
/* Nobody has attached a reader yet either, but a write must still
succeed (buffered for whenever a reader shows up), not report a
broken pipe the way a readers-already-0 pipeCreate() pipe would. */
KT_FALSE(t, pipeWriteBroken(h));
KT_EQ(t, pipeWrite(h, "hi", 2), 2);
char got[8];
KT_EQ(t, pipeRead(h, got, sizeof(got)), 2);
KT_TRUE(t, memcmp(got, "hi", 2) == 0);
pipeAddReader(h);
pipeAddWriter(h);
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeAddWriterThenCloseMakesRealEofNotBeforehand)
{
int h = pipeCreateDetached();
KT_GE(t, h, 0);
pipeAddReader(h);
/* Still no writer attached: not EOF. */
char buffer[4];
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), -1);
pipeAddWriter(h);
KT_EQ(t, pipeWrite(h, "z", 1), 1);
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), 1);
/* Now that a writer really was attached and has left, an empty ring
means true end-of-file. */
pipeCloseWrite(h);
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), 0);
pipeCloseRead(h);
}
KTEST(pipeAddReaderThenCloseMakesRealBrokenPipeNotBeforehand)
{
int h = pipeCreateDetached();
KT_GE(t, h, 0);
pipeAddWriter(h);
/* No reader attached yet: writing still succeeds, not "broken". */
KT_FALSE(t, pipeWriteBroken(h));
KT_EQ(t, pipeWrite(h, "a", 1), 1);
pipeAddReader(h);
char buffer[4];
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), 1);
/* A reader really was attached and has now left: genuinely broken. */
pipeCloseRead(h);
KT_TRUE(t, pipeWriteBroken(h));
KT_EQ(t, pipeWrite(h, "b", 1), -1);
pipeCloseWrite(h);
}
KTEST(pipeWriteBrokenAndChannelRejectInvalidHandles)
{
KT_TRUE(t, pipeWriteBroken(-1));
KT_TRUE(t, pipeWriteBroken(999));
KT_NULL(t, pipeChannel(-1));
KT_NULL(t, pipeChannel(999));
int h = pipeCreateDetached();
KT_GE(t, h, 0);
KT_NOTNULL(t, pipeChannel(h));
int h2 = pipeCreateDetached();
KT_GE(t, h2, 0);
KT_NE(t, pipeChannel(h), pipeChannel(h2));
pipeAddReader(h);
pipeAddWriter(h);
pipeCloseRead(h);
pipeCloseWrite(h);
pipeAddReader(h2);
pipeAddWriter(h2);
pipeCloseRead(h2);
pipeCloseWrite(h2);
}
KTEST(vfsPipeCreateHandleAndAttachInstallBlockingFds)
{
int handle = vfsPipeCreateHandle();
KT_GE(t, handle, 0);
int readFd = vfsPipeAttach(handle, false);
int writeFd = vfsPipeAttach(handle, true);
KT_GE(t, readFd, 0);
KT_GE(t, writeFd, 0);
KT_NE(t, readFd, writeFd);
/* An empty, both-ends-attached shared pipe is exactly the "would
block, but retrying could resolve it" state sysRead's retry path
(syscall.c) depends on. */
KT_NOTNULL(t, vfsBlockingChannel(readFd, false));
KT_EQ(t, vfsWrite(writeFd, "shared", 6), 6);
char buffer[8];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(readFd, buffer, sizeof(buffer)), 6);
KT_TRUE(t, memcmp(buffer, "shared", 6) == 0);
/* Wrong direction / wrong fd kind must never hand back a channel. */
KT_NULL(t, vfsBlockingChannel(readFd, true));
KT_NULL(t, vfsBlockingChannel(writeFd, false));
KT_NULL(t, vfsBlockingChannel(-1, false));
vfsClose(readFd);
vfsClose(writeFd);
}
KTEST(vfsPipeAttachRejectsInvalidHandle)
{
KT_EQ(t, vfsPipeAttach(-1, false), -1);
KT_EQ(t, vfsPipeAttach(999, true), -1);
}
KTEST(vfsBlockingChannelIsNullForOrdinaryNonBlockingPipe)
{
/* vfsPipe's anonymous pipe (both ends in the caller's own table) must
keep reporting "try again" (-1) to the caller directly, the way
pipetest.c depends on -- vfsBlockingChannel must never treat it as
blockable, or sysRead/sysWrite would start parking a single-process
pipe reader/writer that expects an immediate -1 back. */
int readFd, writeFd;
KT_EQ(t, vfsPipe(&readFd, &writeFd), 0);
char probe[1];
KT_EQ(t, vfsRead(readFd, probe, 0), 0);
KT_NULL(t, vfsBlockingChannel(readFd, false));
KT_NULL(t, vfsBlockingChannel(writeFd, true));
vfsClose(readFd);
vfsClose(writeFd);
}
+183
View File
@@ -0,0 +1,183 @@
#include "ktest.h"
#include "pipe.h"
#include "string.h"
KTEST(pipeWriteThenReadRoundTrips)
{
int h = pipeCreate();
KT_GE(t, h, 0);
const char* msg = "hello pipe";
int64 written = pipeWrite(h, msg, strlen(msg));
KT_EQ(t, written, (int64)strlen(msg));
char buffer[32];
memset(buffer, 0, sizeof(buffer));
int64 got = pipeRead(h, buffer, sizeof(buffer));
KT_EQ(t, got, (int64)strlen(msg));
KT_TRUE(t, memcmp(buffer, msg, strlen(msg)) == 0);
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeReadOnEmptyOpenPipeWouldBlock)
{
int h = pipeCreate();
KT_GE(t, h, 0);
char buffer[8];
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), -1);
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeReadZeroLengthIsNoop)
{
int h = pipeCreate();
KT_GE(t, h, 0);
char buffer[1];
KT_EQ(t, pipeRead(h, buffer, 0), 0);
KT_EQ(t, pipeWrite(h, buffer, 0), 0);
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeFullWriteWouldBlockThenDrainsAndAcceptsMore)
{
int h = pipeCreate();
KT_GE(t, h, 0);
uint8 chunk[pipeBufferSize];
memset(chunk, 0xAB, sizeof(chunk));
/* this exactly fills the ring. */
int64 filled = pipeWrite(h, chunk, sizeof(chunk));
KT_EQ(t, filled, (int64)sizeof(chunk));
/* buffer is full: further write must report "would block" */
KT_EQ(t, pipeWrite(h, chunk, 1), -1);
/* drain some bytes, then more space is available */
uint8 drained[64];
int64 gotDrain = pipeRead(h, drained, sizeof(drained));
KT_EQ(t, gotDrain, (int64)sizeof(drained));
for (uint64 i = 0; i < sizeof(drained); i++)
KT_EQU(t, drained[i], 0xAB);
int64 moreWritten = pipeWrite(h, chunk, sizeof(drained));
KT_EQ(t, moreWritten, (int64)sizeof(drained));
pipeCloseRead(h);
pipeCloseWrite(h);
}
KTEST(pipeEofAfterWriterClosedAndBufferDrained)
{
int h = pipeCreate();
KT_GE(t, h, 0);
const char* msg = "last bytes";
KT_EQ(t, pipeWrite(h, msg, strlen(msg)), (int64)strlen(msg));
pipeCloseWrite(h);
/* data queued before close is still readable */
char buffer[32];
int64 got = pipeRead(h, buffer, sizeof(buffer));
KT_EQ(t, got, (int64)strlen(msg));
/* now drained and no writer left: true end-of-file, not "would block" */
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), 0);
KT_EQ(t, pipeRead(h, buffer, sizeof(buffer)), 0);
pipeCloseRead(h);
}
KTEST(pipeWriteAfterReaderClosedIsBrokenPipe)
{
int h = pipeCreate();
KT_GE(t, h, 0);
pipeCloseRead(h);
const char* msg = "nobody listening";
KT_EQ(t, pipeWrite(h, msg, strlen(msg)), -1);
pipeCloseWrite(h);
}
KTEST(pipeInvalidHandleIsRejected)
{
KT_FALSE(t, pipeValid(-1));
KT_FALSE(t, pipeValid(999));
char buffer[4];
KT_EQ(t, pipeRead(-1, buffer, sizeof(buffer)), -1);
KT_EQ(t, pipeWrite(-1, buffer, sizeof(buffer)), -1);
/* closing an invalid handle must not crash */
pipeCloseRead(-1);
pipeCloseWrite(999);
}
KTEST(pipeIndependentPipesDoNotInterfere)
{
int a = pipeCreate();
int b = pipeCreate();
KT_GE(t, a, 0);
KT_GE(t, b, 0);
KT_NE(t, a, b);
KT_EQ(t, pipeWrite(a, "AAAA", 4), 4);
KT_EQ(t, pipeWrite(b, "BB", 2), 2);
char bufA[8];
char bufB[8];
memset(bufA, 0, sizeof(bufA));
memset(bufB, 0, sizeof(bufB));
KT_EQ(t, pipeRead(b, bufB, sizeof(bufB)), 2);
KT_TRUE(t, memcmp(bufB, "BB", 2) == 0);
KT_EQ(t, pipeRead(a, bufA, sizeof(bufA)), 4);
KT_TRUE(t, memcmp(bufA, "AAAA", 4) == 0);
pipeCloseRead(a);
pipeCloseWrite(a);
pipeCloseRead(b);
pipeCloseWrite(b);
}
KTEST(pipeTableExhaustionAndReuse)
{
int handles[maxPipes + 1];
int created = 0;
while (created < maxPipes + 1) {
int h = pipeCreate();
if (h < 0)
break;
handles[created++] = h;
}
/* the table must run out before maxPipes+1 succeed */
KT_LT(t, created, maxPipes + 1);
KT_EQ(t, pipeCreate(), -1);
/* fully closing one pipe frees its slot for reuse */
pipeCloseRead(handles[0]);
pipeCloseWrite(handles[0]);
int reused = pipeCreate();
KT_GE(t, reused, 0);
pipeCloseRead(reused);
pipeCloseWrite(reused);
for (int i = 1; i < created; i++) {
pipeCloseRead(handles[i]);
pipeCloseWrite(handles[i]);
}
}
+21
View File
@@ -96,9 +96,30 @@ void pmmInit(uint64 multibootInfo, uint64 protectedEnd)
frameCount = highestAddr / frameSize; frameCount = highestAddr / frameSize;
/* The bitmap itself must never overlap live data this function still
needs to read (or that anything else placed physically-here still
needs): the loaded kernel image, the multiboot module the caller has
already claimed via protectedEnd (typically the initrd, see
kmain.c), AND the multiboot info structure this very function is
reading `mmap` out of via endAddr, computed above. That last one is
not academic -- GRUB places the multiboot2 info block immediately
after the last module it loads, so protectedEnd (the module's own
end) and multibootInfo's start can legitimately land on the exact
same address. Anchoring on protectedEnd alone would then place the
bitmap ON TOP of the live mmap tag, and the memset just below would
zero out every entry's baseAddr/length/type with 0xFF *before* the
markFree pass a few lines down ever reads them -- silently losing
the entire usable-memory picture and leaving every frame "used"
forever. Growing the initrd (adding another user program, say) is
exactly what nudges protectedEnd into colliding with endAddr, so
this is not a hypothetical: it reproduced on the very next boot after
tools/mktar's ramdisk grew past whatever margin happened to exist
before. */
uint64 bitmapAnchor = (uint64)kernelPhysEnd; uint64 bitmapAnchor = (uint64)kernelPhysEnd;
if (protectedEnd > bitmapAnchor) if (protectedEnd > bitmapAnchor)
bitmapAnchor = protectedEnd; bitmapAnchor = protectedEnd;
if (endAddr > bitmapAnchor)
bitmapAnchor = endAddr;
bitmap = (uint8*)alignUp(bitmapAnchor, frameSize); bitmap = (uint8*)alignUp(bitmapAnchor, frameSize);
uint64 bitmapBytes = (frameCount + 7) / 8; uint64 bitmapBytes = (frameCount + 7) / 8;
memset(bitmap, 0xFF, bitmapBytes); memset(bitmap, 0xFF, bitmapBytes);
+146
View File
@@ -0,0 +1,146 @@
#include "ktest.h"
#include "poll.h"
#include "vfs.h"
#include "pipe.h"
/*
* Pure, scheduler-free coverage for vfsPollReady (vfs.c) and its two
* non-consuming peek helpers (pipeHasData/pipeHasSpace, pipe.h;
* keyboardReaderHasData, keyboard.h) -- everything that can be driven
* single-threaded, exactly like pipeShareTest.c/vfsFifoTest.c already do
* for the machinery poll.h's sysPoll is built on top of. The genuine
* cross-process block-then-wake handshake (schedule()'s own pollDeadline
* expiry scan, sched.c; the shared schedPollChannel wake from a SECOND,
* independently scheduled writer) only runs for real at boot
* (user/polltest.c + user/pollwriter.c, needles in tools/ci-test.sh) --
* the same division of labor this file's neighbors already use, and
* schedTest.c's own doc comment explains why: ktestRunAll() runs before
* schedStart(), with no second execution context to switch into.
*/
KTEST(vfsPollReadyReportsNvalForClosedOrOutOfRangeFd)
{
int16 revents = 99;
KT_TRUE(t, vfsPollReady(-1, pollIn, &revents));
KT_EQ(t, revents, pollNval);
revents = 99;
KT_TRUE(t, vfsPollReady(9999, pollIn | pollOut, &revents));
KT_EQ(t, revents, pollNval);
}
KTEST(vfsPollReadyPipeReadEndTracksRealData)
{
int readFd, writeFd;
KT_EQ(t, vfsPipe(&readFd, &writeFd), 0);
int16 revents = 99;
KT_FALSE(t, vfsPollReady(readFd, pollIn, &revents));
KT_EQ(t, revents, 0);
KT_EQ(t, vfsWrite(writeFd, "hi", 2), 2);
revents = 0;
KT_TRUE(t, vfsPollReady(readFd, pollIn, &revents));
KT_EQ(t, revents, pollIn);
/* Asking for pollOut on a read-only end is never satisfiable, no
matter how much data is queued. */
revents = 99;
KT_FALSE(t, vfsPollReady(readFd, pollOut, &revents));
KT_EQ(t, revents, 0);
char buffer[8];
KT_EQ(t, vfsRead(readFd, buffer, sizeof(buffer)), 2);
vfsClose(readFd);
vfsClose(writeFd);
}
KTEST(vfsPollReadyPipeWriteEndTracksFreeSpaceThenFull)
{
int readFd, writeFd;
KT_EQ(t, vfsPipe(&readFd, &writeFd), 0);
int16 revents = 0;
KT_TRUE(t, vfsPollReady(writeFd, pollOut, &revents));
KT_EQ(t, revents, pollOut);
/* Fill the ring completely (pipeBufferSize, pipe.h) with non-blocking
writes -- vfsPipe's ends are exactly the unshared, "-1 means try
again" kind pipetest.c already depends on, so this can never park
the calling (only) thread the way a fdFlagBlocking end would. */
char chunk[64];
for (int i = 0; i < 64; i++)
chunk[i] = (char)i;
int64 written = 0;
for (int guard = 0; guard < 16; guard++) {
int64 got = vfsWrite(writeFd, chunk, sizeof(chunk));
if (got <= 0)
break;
written += got;
}
KT_TRUE(t, written > 0);
KT_EQ(t, vfsWrite(writeFd, chunk, sizeof(chunk)), -1);
revents = 99;
KT_FALSE(t, vfsPollReady(writeFd, pollOut, &revents));
KT_EQ(t, revents, 0);
/* Draining even a little frees space back up. */
char sink[16];
KT_EQ(t, vfsRead(readFd, sink, sizeof(sink)), 16);
revents = 0;
KT_TRUE(t, vfsPollReady(writeFd, pollOut, &revents));
KT_EQ(t, revents, pollOut);
vfsClose(readFd);
vfsClose(writeFd);
}
KTEST(vfsPollReadyReadEndReportsEofAfterWriterCloses)
{
int readFd, writeFd;
KT_EQ(t, vfsPipe(&readFd, &writeFd), 0);
int16 revents = 0;
KT_FALSE(t, vfsPollReady(readFd, pollIn, &revents));
vfsClose(writeFd);
/* A true EOF pipe is "readable" in real poll(2) too -- the next read
call returning 0 immediately is exactly what pollIn promises here. */
revents = 0;
KT_TRUE(t, vfsPollReady(readFd, pollIn, &revents));
KT_EQ(t, revents, pollIn);
char probe[4];
KT_EQ(t, vfsRead(readFd, probe, sizeof(probe)), 0);
vfsClose(readFd);
}
KTEST(vfsPollReadyConsoleWriteAlwaysReadyReadNeverWithoutInput)
{
/* fd 0 (console) is already live for the current (idle) thread by the
time ktestRunAll() runs -- schedInit() calls vfsFdInit before
kmain.c ever reaches ktestRunAll(). Every open reader (idle's fd 0
included) gets a copy of every keystroke keyboardHandleScancode ever
broadcasts (keyboard.h), including the synthetic ones OTHER test
files inject (keyboardTest.c's own coverage) -- KTEST execution
order across files is whatever the ".ktests" linker section happens
to lay them out in, so idle's fd 0 must be drained here rather than
assumed empty, or this test's own result would depend on that order. */
char drain[16];
while (vfsRead(0, drain, sizeof(drain)) > 0) { }
int16 revents = 99;
KT_FALSE(t, vfsPollReady(0, pollIn, &revents));
KT_EQ(t, revents, 0);
revents = 0;
KT_TRUE(t, vfsPollReady(0, pollOut, &revents));
KT_EQ(t, revents, pollOut);
revents = 0;
KT_TRUE(t, vfsPollReady(0, pollIn | pollOut, &revents));
KT_EQ(t, revents, pollOut);
}
+55
View File
@@ -0,0 +1,55 @@
#include "prioq.h"
static int compareNodes(const RbtNode* a, const RbtNode* b)
{
const PrioQNode* pa = rbtEntry(a, PrioQNode, node);
const PrioQNode* pb = rbtEntry(b, PrioQNode, node);
if (pa->vruntime != pb->vruntime)
return pa->vruntime < pb->vruntime ? -1 : 1;
if (pa->id != pb->id)
return pa->id < pb->id ? -1 : 1;
return 0;
}
void prioqInit(PrioQueue* q)
{
rbtInit(&q->tree);
}
void prioqInsert(PrioQueue* q, PrioQNode* n, uint64 vruntime, uint32 id)
{
n->vruntime = vruntime;
n->id = id;
bool inserted = rbtInsert(&q->tree, &n->node, compareNodes);
(void)inserted; /* unique id guarantees the key can never already exist */
}
PrioQNode* prioqPeekMin(const PrioQueue* q)
{
RbtNode* n = rbtFirst(&q->tree);
return n ? rbtEntry(n, PrioQNode, node) : nullptr;
}
PrioQNode* prioqPopMin(PrioQueue* q)
{
PrioQNode* min = prioqPeekMin(q);
if (min)
rbtRemove(&q->tree, &min->node);
return min;
}
void prioqRemove(PrioQueue* q, PrioQNode* n)
{
rbtRemove(&q->tree, &n->node);
}
usize prioqCount(const PrioQueue* q)
{
return rbtCount(&q->tree);
}
bool prioqIsEmpty(const PrioQueue* q)
{
return rbtIsEmpty(&q->tree);
}
+182
View File
@@ -0,0 +1,182 @@
#include "ktest.h"
#include "prioq.h"
#include "heap.h"
/* Fisher-Yates over a small LCG so insertion order is scrambled but
reproducible across runs (same generator as rbtTest.c's shuffle). */
static void shuffle(uint32* arr, int32 n, uint32 seed)
{
uint32 state = seed;
for (int32 i = n - 1; i > 0; i--) {
state = state * 1664525u + 1013904223u;
int32 j = (int32)(state % (uint32)(i + 1));
uint32 tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
KTEST(prioqEmptyQueue)
{
PrioQueue q;
prioqInit(&q);
KT_TRUE(t, prioqIsEmpty(&q));
KT_EQ(t, prioqCount(&q), 0);
KT_NULL(t, prioqPeekMin(&q));
KT_NULL(t, prioqPopMin(&q));
}
KTEST(prioqOrdersByVruntimeAscending)
{
PrioQueue q;
prioqInit(&q);
PrioQNode nodes[5];
uint64 vruntimes[] = { 300, 100, 500, 200, 400 };
for (uint32 i = 0; i < 5; i++)
prioqInsert(&q, &nodes[i], vruntimes[i], i);
KT_EQ(t, prioqCount(&q), 5);
uint64 expected[] = { 100, 200, 300, 400, 500 };
for (uint32 i = 0; i < 5; i++) {
PrioQNode* min = prioqPopMin(&q);
KT_NOTNULL(t, min);
if (min)
KT_EQU(t, min->vruntime, expected[i]);
}
KT_TRUE(t, prioqIsEmpty(&q));
}
KTEST(prioqPeekDoesNotRemove)
{
PrioQueue q;
prioqInit(&q);
PrioQNode a, b;
prioqInsert(&q, &a, 50, 1);
prioqInsert(&q, &b, 10, 2);
PrioQNode* peeked = prioqPeekMin(&q);
KT_NOTNULL(t, peeked);
if (peeked)
KT_EQ(t, peeked->id, 2);
KT_EQ(t, prioqCount(&q), 2);
PrioQNode* popped = prioqPopMin(&q);
KT_EQP(t, popped, peeked);
KT_EQ(t, prioqCount(&q), 1);
}
KTEST(prioqTiesBreakByIdAscending)
{
PrioQueue q;
prioqInit(&q);
PrioQNode nodes[4];
uint32 ids[] = { 7, 3, 9, 1 };
for (uint32 i = 0; i < 4; i++)
prioqInsert(&q, &nodes[i], 1000, ids[i]); /* identical vruntime */
uint32 expected[] = { 1, 3, 7, 9 };
for (uint32 i = 0; i < 4; i++) {
PrioQNode* min = prioqPopMin(&q);
KT_NOTNULL(t, min);
if (min)
KT_EQ(t, min->id, expected[i]);
}
}
KTEST(prioqRemoveArbitraryNodePreservesOrder)
{
PrioQueue q;
prioqInit(&q);
PrioQNode nodes[4];
uint64 vruntimes[] = { 10, 20, 30, 40 };
for (uint32 i = 0; i < 4; i++)
prioqInsert(&q, &nodes[i], vruntimes[i], i);
prioqRemove(&q, &nodes[1]); /* drop vruntime=20 before it is ever popped */
KT_EQ(t, prioqCount(&q), 3);
uint64 expected[] = { 10, 30, 40 };
for (uint32 i = 0; i < 3; i++) {
PrioQNode* min = prioqPopMin(&q);
KT_NOTNULL(t, min);
if (min)
KT_EQU(t, min->vruntime, expected[i]);
}
KT_TRUE(t, prioqIsEmpty(&q));
}
KTEST(prioqNodeStorageIsReusableAcrossQueueLifetimes)
{
PrioQNode node;
PrioQueue first;
prioqInit(&first);
prioqInsert(&first, &node, 500, 42);
PrioQNode* popped = prioqPopMin(&first);
KT_EQP(t, popped, &node);
/* Same PrioQNode storage, brand new queue: rbtInsert must not be
tripped up by stale left/right/parent pointers left over from the
first tree. */
PrioQueue second;
prioqInit(&second);
prioqInsert(&second, &node, 5, 1);
KT_EQ(t, prioqCount(&second), 1);
PrioQNode* min = prioqPeekMin(&second);
KT_NOTNULL(t, min);
if (min)
KT_EQU(t, min->vruntime, 5);
}
#define prioqStressN 300
KTEST(prioqStressDrainsInFullyAscendingOrder)
{
PrioQNode* pool = (PrioQNode*)kmalloc(sizeof(PrioQNode) * prioqStressN);
uint32* order = (uint32*)kmalloc(sizeof(uint32) * prioqStressN);
KT_NOTNULL(t, pool);
KT_NOTNULL(t, order);
if (!pool || !order) {
kfree(pool);
kfree(order);
return;
}
for (uint32 i = 0; i < prioqStressN; i++)
order[i] = i;
shuffle(order, prioqStressN, 0xFEEDFACEu);
PrioQueue q;
prioqInit(&q);
for (uint32 i = 0; i < prioqStressN; i++) {
uint32 id = order[i];
/* vruntime = id * 3 keeps a strict, checkable total order while
still exercising ids that arrive in scrambled sequence. */
prioqInsert(&q, &pool[id], (uint64)id * 3, id);
}
KT_EQ(t, prioqCount(&q), prioqStressN);
uint64 lastVruntime = 0;
uint32 visited = 0;
for (;;) {
PrioQNode* min = prioqPopMin(&q);
if (!min)
break;
if (visited > 0)
KT_TRUE(t, min->vruntime >= lastVruntime);
lastVruntime = min->vruntime;
KT_EQU(t, min->vruntime, (uint64)min->id * 3);
visited++;
}
KT_EQ(t, visited, prioqStressN);
KT_TRUE(t, prioqIsEmpty(&q));
kfree(pool);
kfree(order);
}
+313
View File
@@ -0,0 +1,313 @@
#include "rbt.h"
static bool isRed(const RbtNode* n)
{
return n != nullptr && n->red;
}
static void setChild(RbTree* t, RbtNode* parent, RbtNode* oldChild, RbtNode* newChild)
{
if (parent == nullptr)
t->root = newChild;
else if (parent->left == oldChild)
parent->left = newChild;
else
parent->right = newChild;
}
static void rotateLeft(RbTree* t, RbtNode* x)
{
RbtNode* y = x->right;
x->right = y->left;
if (y->left != nullptr)
y->left->parent = x;
y->parent = x->parent;
setChild(t, x->parent, x, y);
y->left = x;
x->parent = y;
}
static void rotateRight(RbTree* t, RbtNode* x)
{
RbtNode* y = x->left;
x->left = y->right;
if (y->right != nullptr)
y->right->parent = x;
y->parent = x->parent;
setChild(t, x->parent, x, y);
y->right = x;
x->parent = y;
}
static void insertFixup(RbTree* t, RbtNode* z)
{
while (isRed(z->parent)) {
RbtNode* parent = z->parent;
RbtNode* grandparent = parent->parent;
if (parent == grandparent->left) {
RbtNode* uncle = grandparent->right;
if (isRed(uncle)) {
parent->red = false;
uncle->red = false;
grandparent->red = true;
z = grandparent;
} else {
if (z == parent->right) {
z = parent;
rotateLeft(t, z);
parent = z->parent;
}
parent->red = false;
grandparent->red = true;
rotateRight(t, grandparent);
}
} else {
RbtNode* uncle = grandparent->left;
if (isRed(uncle)) {
parent->red = false;
uncle->red = false;
grandparent->red = true;
z = grandparent;
} else {
if (z == parent->left) {
z = parent;
rotateRight(t, z);
parent = z->parent;
}
parent->red = false;
grandparent->red = true;
rotateLeft(t, grandparent);
}
}
}
t->root->red = false;
}
static void removeFixup(RbTree* t, RbtNode* x, RbtNode* xParent)
{
while (x != t->root && !isRed(x)) {
if (x == xParent->left) {
RbtNode* w = xParent->right;
if (isRed(w)) {
w->red = false;
xParent->red = true;
rotateLeft(t, xParent);
w = xParent->right;
}
if (!isRed(w->left) && !isRed(w->right)) {
w->red = true;
x = xParent;
xParent = x->parent;
} else {
if (!isRed(w->right)) {
if (w->left != nullptr)
w->left->red = false;
w->red = true;
rotateRight(t, w);
w = xParent->right;
}
w->red = xParent->red;
xParent->red = false;
if (w->right != nullptr)
w->right->red = false;
rotateLeft(t, xParent);
x = t->root;
xParent = nullptr;
}
} else {
RbtNode* w = xParent->left;
if (isRed(w)) {
w->red = false;
xParent->red = true;
rotateRight(t, xParent);
w = xParent->left;
}
if (!isRed(w->left) && !isRed(w->right)) {
w->red = true;
x = xParent;
xParent = x->parent;
} else {
if (!isRed(w->left)) {
if (w->right != nullptr)
w->right->red = false;
w->red = true;
rotateLeft(t, w);
w = xParent->left;
}
w->red = xParent->red;
xParent->red = false;
if (w->left != nullptr)
w->left->red = false;
rotateRight(t, xParent);
x = t->root;
xParent = nullptr;
}
}
}
if (x != nullptr)
x->red = false;
}
void rbtInit(RbTree* t)
{
t->root = nullptr;
t->count = 0;
}
bool rbtInsert(RbTree* t, RbtNode* node, RbtCompareFn compare)
{
RbtNode* parent = nullptr;
RbtNode* cur = t->root;
bool goLeft = false;
while (cur != nullptr) {
int c = compare(node, cur);
if (c == 0)
return false;
parent = cur;
goLeft = c < 0;
cur = goLeft ? cur->left : cur->right;
}
node->left = nullptr;
node->right = nullptr;
node->parent = parent;
node->red = true;
if (parent == nullptr)
t->root = node;
else if (goLeft)
parent->left = node;
else
parent->right = node;
t->count++;
insertFixup(t, node);
return true;
}
void rbtRemove(RbTree* t, RbtNode* z)
{
RbtNode* y = z;
bool yOriginalRed = y->red;
RbtNode* x;
RbtNode* xParent;
if (z->left == nullptr) {
x = z->right;
xParent = z->parent;
setChild(t, z->parent, z, z->right);
if (z->right != nullptr)
z->right->parent = z->parent;
} else if (z->right == nullptr) {
x = z->left;
xParent = z->parent;
setChild(t, z->parent, z, z->left);
if (z->left != nullptr)
z->left->parent = z->parent;
} else {
y = z->right;
while (y->left != nullptr)
y = y->left;
yOriginalRed = y->red;
x = y->right;
if (y->parent == z) {
xParent = y;
} else {
xParent = y->parent;
setChild(t, y->parent, y, y->right);
if (y->right != nullptr)
y->right->parent = y->parent;
y->right = z->right;
y->right->parent = y;
}
setChild(t, z->parent, z, y);
y->parent = z->parent;
y->left = z->left;
y->left->parent = y;
y->red = z->red;
}
t->count--;
if (!yOriginalRed)
removeFixup(t, x, xParent);
}
RbtNode* rbtFind(const RbTree* t, const void* key, RbtLookupFn lookup)
{
RbtNode* cur = t->root;
while (cur != nullptr) {
int c = lookup(key, cur);
if (c == 0)
return cur;
cur = c < 0 ? cur->left : cur->right;
}
return nullptr;
}
RbtNode* rbtFirst(const RbTree* t)
{
RbtNode* n = t->root;
if (n == nullptr)
return nullptr;
while (n->left != nullptr)
n = n->left;
return n;
}
RbtNode* rbtLast(const RbTree* t)
{
RbtNode* n = t->root;
if (n == nullptr)
return nullptr;
while (n->right != nullptr)
n = n->right;
return n;
}
RbtNode* rbtNext(const RbtNode* node)
{
if (node->right != nullptr) {
RbtNode* n = node->right;
while (n->left != nullptr)
n = n->left;
return n;
}
RbtNode* n = (RbtNode*)node;
RbtNode* parent = n->parent;
while (parent != nullptr && n == parent->right) {
n = parent;
parent = parent->parent;
}
return parent;
}
RbtNode* rbtPrev(const RbtNode* node)
{
if (node->left != nullptr) {
RbtNode* n = node->left;
while (n->right != nullptr)
n = n->right;
return n;
}
RbtNode* n = (RbtNode*)node;
RbtNode* parent = n->parent;
while (parent != nullptr && n == parent->left) {
n = parent;
parent = parent->parent;
}
return parent;
}
usize rbtCount(const RbTree* t)
{
return t->count;
}
bool rbtIsEmpty(const RbTree* t)
{
return t->root == nullptr;
}
+299
View File
@@ -0,0 +1,299 @@
#include "ktest.h"
#include "rbt.h"
#include "heap.h"
typedef struct IntNode {
RbtNode node;
int32 key;
} IntNode;
static int compareIntNode(const RbtNode* a, const RbtNode* b)
{
const IntNode* ia = rbtEntry(a, IntNode, node);
const IntNode* ib = rbtEntry(b, IntNode, node);
if (ia->key < ib->key)
return -1;
if (ia->key > ib->key)
return 1;
return 0;
}
static int lookupIntKey(const void* key, const RbtNode* node)
{
int32 k = *(const int32*)key;
const IntNode* in = rbtEntry(node, IntNode, node);
if (k < in->key)
return -1;
if (k > in->key)
return 1;
return 0;
}
/* Fisher-Yates over a small LCG so insertion/removal order is scrambled but
reproducible across runs -- the point is to exercise every rotation case,
not to be a real source of randomness. */
static void shuffle(int32* arr, int32 n, uint32 seed)
{
uint32 state = seed;
for (int32 i = n - 1; i > 0; i--) {
state = state * 1664525u + 1013904223u;
int32 j = (int32)(state % (uint32)(i + 1));
int32 tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
/* Returns the black-height of node (counting nullptr leaves as height 1) and
clears *valid if either red-black rule is broken anywhere in the subtree:
a red node with a red child, or unequal black-height between the two
sides of some node. */
static int32 checkSubtree(RbtNode* node, bool* valid)
{
if (node == nullptr)
return 1;
if (node->red) {
if ((node->left != nullptr && node->left->red) ||
(node->right != nullptr && node->right->red))
*valid = false;
}
if (node->left != nullptr && node->left->parent != node)
*valid = false;
if (node->right != nullptr && node->right->parent != node)
*valid = false;
int32 leftHeight = checkSubtree(node->left, valid);
int32 rightHeight = checkSubtree(node->right, valid);
if (leftHeight != rightHeight)
*valid = false;
return leftHeight + (node->red ? 0 : 1);
}
static bool checkInvariants(RbTree* tree)
{
if (tree->root != nullptr && tree->root->red)
return false;
bool valid = true;
checkSubtree(tree->root, &valid);
return valid;
}
KTEST(rbtEmptyTree)
{
RbTree tree;
rbtInit(&tree);
KT_TRUE(t, rbtIsEmpty(&tree));
KT_EQ(t, rbtCount(&tree), 0);
KT_NULL(t, rbtFirst(&tree));
KT_NULL(t, rbtLast(&tree));
int32 key = 42;
KT_NULL(t, rbtFind(&tree, &key, lookupIntKey));
}
KTEST(rbtBasicInsertFindOrder)
{
RbTree tree;
rbtInit(&tree);
int32 values[] = { 5, 3, 8, 1, 4, 7, 9 };
usize n = sizeof(values) / sizeof(values[0]);
IntNode nodes[7];
for (usize i = 0; i < n; i++) {
nodes[i].key = values[i];
KT_TRUE(t, rbtInsert(&tree, &nodes[i].node, compareIntNode));
}
KT_EQ(t, rbtCount(&tree), n);
KT_TRUE(t, checkInvariants(&tree));
int32 expected[] = { 1, 3, 4, 5, 7, 8, 9 };
usize i = 0;
for (RbtNode* c = rbtFirst(&tree); c != nullptr; c = rbtNext(c), i++)
KT_EQ(t, rbtEntry(c, IntNode, node)->key, expected[i]);
KT_EQ(t, i, n);
/* walking backwards from the max visits the same keys in reverse */
i = n;
for (RbtNode* c = rbtLast(&tree); c != nullptr; c = rbtPrev(c))
KT_EQ(t, rbtEntry(c, IntNode, node)->key, expected[--i]);
KT_EQ(t, i, 0);
int32 hitKey = 8;
RbtNode* found = rbtFind(&tree, &hitKey, lookupIntKey);
KT_NOTNULL(t, found);
if (found)
KT_EQ(t, rbtEntry(found, IntNode, node)->key, 8);
int32 missKey = 100;
KT_NULL(t, rbtFind(&tree, &missKey, lookupIntKey));
KT_EQ(t, rbtEntry(rbtFirst(&tree), IntNode, node)->key, 1);
KT_EQ(t, rbtEntry(rbtLast(&tree), IntNode, node)->key, 9);
}
KTEST(rbtRejectsDuplicateInsert)
{
RbTree tree;
rbtInit(&tree);
IntNode a, b;
a.key = 5;
b.key = 5;
KT_TRUE(t, rbtInsert(&tree, &a.node, compareIntNode));
KT_FALSE(t, rbtInsert(&tree, &b.node, compareIntNode));
KT_EQ(t, rbtCount(&tree), 1);
}
KTEST(rbtSingleRemoveRelinksNeighbors)
{
RbTree tree;
rbtInit(&tree);
IntNode nodes[5];
int32 values[] = { 10, 20, 30, 40, 50 };
for (int i = 0; i < 5; i++) {
nodes[i].key = values[i];
rbtInsert(&tree, &nodes[i].node, compareIntNode);
}
rbtRemove(&tree, &nodes[2].node); /* remove 30, the middle */
KT_EQ(t, rbtCount(&tree), 4);
KT_TRUE(t, checkInvariants(&tree));
int32 key30 = 30;
KT_NULL(t, rbtFind(&tree, &key30, lookupIntKey));
int32 expected[] = { 10, 20, 40, 50 };
usize i = 0;
for (RbtNode* c = rbtFirst(&tree); c != nullptr; c = rbtNext(c), i++)
KT_EQ(t, rbtEntry(c, IntNode, node)->key, expected[i]);
KT_EQ(t, i, 4);
}
#define rbtStressN 500
KTEST(rbtStressInsertMaintainsInvariants)
{
IntNode* pool = (IntNode*)kmalloc(sizeof(IntNode) * rbtStressN);
int32* order = (int32*)kmalloc(sizeof(int32) * rbtStressN);
KT_NOTNULL(t, pool);
KT_NOTNULL(t, order);
if (!pool || !order) {
kfree(pool);
kfree(order);
return;
}
for (int32 i = 0; i < rbtStressN; i++)
order[i] = i;
shuffle(order, rbtStressN, 0xC0FFEEu);
RbTree tree;
rbtInit(&tree);
for (int32 i = 0; i < rbtStressN; i++) {
int32 key = order[i];
pool[key].key = key;
KT_TRUE(t, rbtInsert(&tree, &pool[key].node, compareIntNode));
}
KT_EQ(t, rbtCount(&tree), rbtStressN);
KT_TRUE(t, checkInvariants(&tree));
int32 expect = 0;
usize visited = 0;
for (RbtNode* c = rbtFirst(&tree); c != nullptr; c = rbtNext(c)) {
KT_EQ(t, rbtEntry(c, IntNode, node)->key, expect);
expect++;
visited++;
}
KT_EQ(t, visited, rbtStressN);
kfree(pool);
kfree(order);
}
KTEST(rbtStressRemoveMaintainsInvariants)
{
IntNode* pool = (IntNode*)kmalloc(sizeof(IntNode) * rbtStressN);
int32* order = (int32*)kmalloc(sizeof(int32) * rbtStressN);
int32* removeOrder = (int32*)kmalloc(sizeof(int32) * rbtStressN);
KT_NOTNULL(t, pool);
KT_NOTNULL(t, order);
KT_NOTNULL(t, removeOrder);
if (!pool || !order || !removeOrder) {
kfree(pool);
kfree(order);
kfree(removeOrder);
return;
}
for (int32 i = 0; i < rbtStressN; i++) {
order[i] = i;
removeOrder[i] = i;
}
shuffle(order, rbtStressN, 0xDEADBEEFu);
shuffle(removeOrder, rbtStressN, 0x1234567u);
RbTree tree;
rbtInit(&tree);
for (int32 i = 0; i < rbtStressN; i++) {
int32 key = order[i];
pool[key].key = key;
rbtInsert(&tree, &pool[key].node, compareIntNode);
}
/* remove every even key, in scrambled order */
int32 removed = 0;
for (int32 i = 0; i < rbtStressN; i++) {
int32 key = removeOrder[i];
if (key % 2 != 0)
continue;
RbtNode* found = rbtFind(&tree, &key, lookupIntKey);
KT_NOTNULL(t, found);
if (found) {
rbtRemove(&tree, found);
removed++;
}
}
KT_EQ(t, rbtCount(&tree), rbtStressN - removed);
KT_TRUE(t, checkInvariants(&tree));
for (int32 key = 0; key < rbtStressN; key++) {
RbtNode* found = rbtFind(&tree, &key, lookupIntKey);
if (key % 2 == 0)
KT_NULL(t, found);
else
KT_NOTNULL(t, found);
}
int32 expect = 1;
usize visited = 0;
for (RbtNode* c = rbtFirst(&tree); c != nullptr; c = rbtNext(c)) {
KT_EQ(t, rbtEntry(c, IntNode, node)->key, expect);
expect += 2;
visited++;
}
KT_EQ(t, visited, rbtStressN - removed);
/* drain the rest, in the same scrambled order, down to nothing */
for (int32 i = 0; i < rbtStressN; i++) {
int32 key = removeOrder[i];
if (key % 2 == 0)
continue;
RbtNode* found = rbtFind(&tree, &key, lookupIntKey);
if (found)
rbtRemove(&tree, found);
}
KT_TRUE(t, rbtIsEmpty(&tree));
KT_EQ(t, rbtCount(&tree), 0);
KT_NULL(t, rbtFirst(&tree));
KT_NULL(t, rbtLast(&tree));
kfree(pool);
kfree(order);
kfree(removeOrder);
}
+58
View File
@@ -0,0 +1,58 @@
#include "ring.h"
void ringInit(RingBuffer* r, uint8* backingBuffer, usize capacity)
{
r->data = backingBuffer;
r->capacity = capacity;
r->head = 0;
r->count = 0;
}
void ringClear(RingBuffer* r)
{
r->head = 0;
r->count = 0;
}
bool ringIsEmpty(const RingBuffer* r) { return r->count == 0; }
bool ringIsFull(const RingBuffer* r) { return r->count == r->capacity; }
usize ringCount(const RingBuffer* r) { return r->count; }
usize ringCapacity(const RingBuffer* r) { return r->capacity; }
usize ringFreeSpace(const RingBuffer* r) { return r->capacity - r->count; }
bool ringPush(RingBuffer* r, uint8 byte)
{
if (ringIsFull(r))
return false;
usize tail = (r->head + r->count) % r->capacity;
r->data[tail] = byte;
r->count++;
return true;
}
bool ringPop(RingBuffer* r, uint8* out)
{
if (ringIsEmpty(r))
return false;
if (out)
*out = r->data[r->head];
r->head = (r->head + 1) % r->capacity;
r->count--;
return true;
}
usize ringWrite(RingBuffer* r, const uint8* data, usize len)
{
usize written = 0;
while (written < len && ringPush(r, data[written]))
written++;
return written;
}
usize ringRead(RingBuffer* r, uint8* data, usize len)
{
usize readCount = 0;
while (readCount < len && ringPop(r, &data[readCount]))
readCount++;
return readCount;
}
+117
View File
@@ -0,0 +1,117 @@
#include "ktest.h"
#include "ring.h"
KTEST(ringBasicPushPop)
{
uint8 backing[4];
RingBuffer r;
ringInit(&r, backing, sizeof(backing));
KT_TRUE(t, ringIsEmpty(&r));
KT_FALSE(t, ringIsFull(&r));
KT_EQ(t, ringCapacity(&r), 4);
KT_EQ(t, ringFreeSpace(&r), 4);
KT_TRUE(t, ringPush(&r, 'a'));
KT_TRUE(t, ringPush(&r, 'b'));
KT_EQ(t, ringCount(&r), 2);
KT_EQ(t, ringFreeSpace(&r), 2);
uint8 out;
KT_TRUE(t, ringPop(&r, &out));
KT_EQ(t, out, 'a');
KT_TRUE(t, ringPop(&r, &out));
KT_EQ(t, out, 'b');
KT_TRUE(t, ringIsEmpty(&r));
}
KTEST(ringFillsAndRejects)
{
uint8 backing[3];
RingBuffer r;
ringInit(&r, backing, sizeof(backing));
KT_TRUE(t, ringPush(&r, 1));
KT_TRUE(t, ringPush(&r, 2));
KT_TRUE(t, ringPush(&r, 3));
KT_TRUE(t, ringIsFull(&r));
KT_FALSE(t, ringPush(&r, 4)); /* rejected, buffer unchanged */
KT_EQ(t, ringCount(&r), 3);
uint8 out;
ringPop(&r, &out);
KT_EQ(t, out, 1);
KT_FALSE(t, ringIsFull(&r));
KT_TRUE(t, ringPush(&r, 4)); /* room again after the pop */
}
KTEST(ringWrapsAroundCorrectly)
{
uint8 backing[4];
RingBuffer r;
ringInit(&r, backing, sizeof(backing));
/* fill, drain two, push two more so the write wraps past the array end */
ringPush(&r, 1);
ringPush(&r, 2);
ringPush(&r, 3);
ringPush(&r, 4);
uint8 out;
ringPop(&r, &out);
KT_EQ(t, out, 1);
ringPop(&r, &out);
KT_EQ(t, out, 2);
ringPush(&r, 5);
ringPush(&r, 6);
ringPop(&r, &out);
KT_EQ(t, out, 3);
ringPop(&r, &out);
KT_EQ(t, out, 4);
ringPop(&r, &out);
KT_EQ(t, out, 5);
ringPop(&r, &out);
KT_EQ(t, out, 6);
KT_TRUE(t, ringIsEmpty(&r));
}
KTEST(ringBulkWriteRead)
{
uint8 backing[8];
RingBuffer r;
ringInit(&r, backing, sizeof(backing));
const uint8 msg[] = { 10, 20, 30, 40, 50 };
usize written = ringWrite(&r, msg, sizeof(msg));
KT_EQ(t, written, 5);
KT_EQ(t, ringCount(&r), 5);
/* only 3 more bytes fit before capacity 8 is reached */
const uint8 more[] = { 1, 2, 3, 4 };
usize written2 = ringWrite(&r, more, sizeof(more));
KT_EQ(t, written2, 3);
KT_TRUE(t, ringIsFull(&r));
uint8 outBuf[16];
usize readCount = ringRead(&r, outBuf, sizeof(outBuf));
KT_EQ(t, readCount, 8);
KT_EQ(t, outBuf[0], 10);
KT_EQ(t, outBuf[4], 50);
KT_EQ(t, outBuf[5], 1);
KT_EQ(t, outBuf[7], 3);
KT_TRUE(t, ringIsEmpty(&r));
}
KTEST(ringClearResets)
{
uint8 backing[4];
RingBuffer r;
ringInit(&r, backing, sizeof(backing));
ringPush(&r, 1);
ringPush(&r, 2);
ringClear(&r);
KT_TRUE(t, ringIsEmpty(&r));
KT_EQ(t, ringCount(&r), 0);
KT_EQ(t, ringFreeSpace(&r), 4);
}
+192
View File
@@ -0,0 +1,192 @@
#include "rtc.h"
#include "ports.h"
#include "string.h"
#define rtcPortIndex 0x70
#define rtcPortData 0x71
#define rtcRegSeconds 0x00
#define rtcRegMinutes 0x02
#define rtcRegHours 0x04
#define rtcRegDay 0x07
#define rtcRegMonth 0x08
#define rtcRegYear 0x09
#define rtcRegStatusA 0x0A
#define rtcRegStatusB 0x0B
#define rtcStatusAUip 0x80
#define rtcStatusBHourFormat24 0x02
#define rtcStatusBBinaryMode 0x04
#define rtcHourPmBit 0x80
/* Bounded like ata.c's ataPollTimeout -- a wedged or absent RTC (never
observed on real hardware or under QEMU, but this driver has no way to
independently confirm the chip exists the way ataInit's IDENTIFY does)
returns false/0 instead of spinning forever with interrupts disabled. */
#define rtcUipTimeout 100000u
#define rtcReadAttempts 8u
#define rtcAssumedCentury 20
static uint8 cmosRead(uint8 reg)
{
/* Bit 0x80 on the index port disables NMI for the duration of the
read, the same convention the OSDev wiki's own RTC driver uses --
harmless if the platform ignores it, correct if it does not. */
outb(rtcPortIndex, (uint8)(reg | 0x80));
ioWait();
return inb(rtcPortData);
}
static bool waitWhileUpdateInProgress(void)
{
for (uint32 i = 0; i < rtcUipTimeout; i++) {
if ((cmosRead(rtcRegStatusA) & rtcStatusAUip) == 0)
return true;
}
return false;
}
typedef struct RtcRaw {
uint8 seconds;
uint8 minutes;
uint8 hours;
uint8 day;
uint8 month;
uint8 year;
} RtcRaw;
static void readRawOnce(RtcRaw* out)
{
out->seconds = cmosRead(rtcRegSeconds);
out->minutes = cmosRead(rtcRegMinutes);
out->hours = cmosRead(rtcRegHours);
out->day = cmosRead(rtcRegDay);
out->month = cmosRead(rtcRegMonth);
out->year = cmosRead(rtcRegYear);
}
uint8 rtcBcdToBinary(uint8 bcd)
{
return (uint8)(((bcd >> 4) & 0x0F) * 10 + (bcd & 0x0F));
}
bool rtcReadDateTime(RtcDateTime* out)
{
RtcRaw a;
RtcRaw b;
bool agree = false;
for (uint32 attempt = 0; attempt < rtcReadAttempts && !agree; attempt++) {
if (!waitWhileUpdateInProgress())
return false;
readRawOnce(&a);
if (!waitWhileUpdateInProgress())
return false;
readRawOnce(&b);
agree = memcmp(&a, &b, sizeof(RtcRaw)) == 0;
}
if (!agree)
return false;
uint8 statusB = cmosRead(rtcRegStatusB);
bool binaryMode = (statusB & rtcStatusBBinaryMode) != 0;
bool is24Hour = (statusB & rtcStatusBHourFormat24) != 0;
bool pm = (a.hours & rtcHourPmBit) != 0;
uint8 rawHours = (uint8)(a.hours & (uint8)~rtcHourPmBit);
uint8 seconds = binaryMode ? a.seconds : rtcBcdToBinary(a.seconds);
uint8 minutes = binaryMode ? a.minutes : rtcBcdToBinary(a.minutes);
uint8 hours = binaryMode ? rawHours : rtcBcdToBinary(rawHours);
uint8 day = binaryMode ? a.day : rtcBcdToBinary(a.day);
uint8 month = binaryMode ? a.month : rtcBcdToBinary(a.month);
uint8 year = binaryMode ? a.year : rtcBcdToBinary(a.year);
if (!is24Hour) {
if (pm && hours != 12)
hours = (uint8)(hours + 12);
else if (!pm && hours == 12)
hours = 0;
}
out->year = rtcAssumedCentury * 100 + year;
out->month = month;
out->day = day;
out->hour = hours;
out->minute = minutes;
out->second = seconds;
return true;
}
/* Howard Hinnant's days_from_civil (proleptic Gregorian calendar), see
http://howardhinnant.github.io/date_algorithms.html -- converts a civil
calendar date into a day count relative to 1970-01-01 with no lookup
tables, correct for the whole int64 range this kernel's RtcDateTime.year
field can carry, including leap years computed structurally (divisible
by 4, not by 100 unless also by 400) rather than by a hand-rolled rule. */
static int64 daysFromCivil(int64 y, int64 m, int64 d)
{
y -= (m <= 2) ? 1 : 0;
int64 era = (y >= 0 ? y : y - 399) / 400;
int64 yoe = y - era * 400; /* [0, 399] */
int64 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; /* [0, 365] */
int64 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; /* [0, 146096] */
return era * 146097 + doe - 719468;
}
int64 rtcEpochSeconds(const RtcDateTime* dt)
{
int64 days = daysFromCivil(dt->year, dt->month, dt->day);
return days * 86400 + dt->hour * 3600 + dt->minute * 60 + dt->second;
}
int64 rtcNow(void)
{
RtcDateTime dt;
if (!rtcReadDateTime(&dt))
return 0;
return rtcEpochSeconds(&dt);
}
/* Howard Hinnant's civil_from_days, the exact inverse of daysFromCivil
above (same source) -- given a day count relative to 1970-01-01, returns
the proleptic Gregorian year/month/day it names. Structurally mirrors
daysFromCivil's era/yoe/doy decomposition in reverse, so the two stay
easy to cross-check against each other by inspection. */
static void civilFromDays(int64 z, int64* year, int64* month, int64* day)
{
z += 719468;
int64 era = (z >= 0 ? z : z - 146096) / 146097;
int64 doe = z - era * 146097; /* [0, 146096] */
int64 yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; /* [0, 399] */
int64 y = yoe + era * 400;
int64 doy = doe - (365 * yoe + yoe / 4 - yoe / 100); /* [0, 365] */
int64 mp = (5 * doy + 2) / 153; /* [0, 11] */
int64 d = doy - (153 * mp + 2) / 5 + 1; /* [1, 31] */
int64 m = mp + (mp < 10 ? 3 : -9); /* [1, 12] */
*year = y + (m <= 2 ? 1 : 0);
*month = m;
*day = d;
}
void rtcCivilFromEpoch(int64 epochSeconds, RtcDateTime* out)
{
int64 days = epochSeconds / 86400;
int64 secondsOfDay = epochSeconds % 86400;
/* C's integer division truncates toward zero, not toward negative
infinity -- for a negative epochSeconds that does not land exactly
on a day boundary, that rounds `days` the wrong way (toward 1970)
and leaves secondsOfDay negative. Both need shifting back by
exactly one day/86400s together, never one without the other. */
if (secondsOfDay < 0) {
secondsOfDay += 86400;
days -= 1;
}
civilFromDays(days, &out->year, &out->month, &out->day);
out->hour = secondsOfDay / 3600;
out->minute = (secondsOfDay % 3600) / 60;
out->second = secondsOfDay % 60;
}
+215
View File
@@ -0,0 +1,215 @@
#include "ktest.h"
#include "rtc.h"
/*
* rtc.c gets two different kinds of coverage here, the same split
* ata.c/ataTest.c already established: pure arithmetic (the BCD conversion
* and the civil-date-to-epoch math) is tested with known, hand-verified
* values that need no hardware at all, while rtcReadDateTime/rtcNow are
* exercised against whatever CMOS RTC QEMU actually emulates, so a wiring
* mistake (wrong port, wrong status bit, missed BCD/24-hour branch) shows
* up as an implausible or non-monotonic reading here, not a mocked
* assertion.
*/
KTEST(rtcBcdToBinaryKnownValues)
{
KT_EQ(t, rtcBcdToBinary(0x00), 0);
KT_EQ(t, rtcBcdToBinary(0x09), 9);
KT_EQ(t, rtcBcdToBinary(0x10), 10);
KT_EQ(t, rtcBcdToBinary(0x23), 23);
KT_EQ(t, rtcBcdToBinary(0x59), 59);
}
KTEST(rtcEpochSecondsUnixEpochIsZero)
{
RtcDateTime dt = { .year = 1970, .month = 1, .day = 1, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&dt), 0);
}
KTEST(rtcEpochSecondsTimeOfDayComponent)
{
RtcDateTime dt = { .year = 1970, .month = 1, .day = 1, .hour = 1, .minute = 2, .second = 3 };
KT_EQ(t, rtcEpochSeconds(&dt), 1 * 3600 + 2 * 60 + 3);
}
KTEST(rtcEpochSecondsOrdinaryDayIsOneDayApart)
{
RtcDateTime a = { .year = 2026, .month = 9, .day = 7, .hour = 0, .minute = 0, .second = 0 };
RtcDateTime b = { .year = 2026, .month = 9, .day = 8, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&b) - rtcEpochSeconds(&a), 86400);
}
/* The single most common bug in hand-rolled calendar math: getting the
leap-year rule wrong. 2024 is a leap year (divisible by 4, not by a
century that isn't also divisible by 400), so February has 29 days and
Feb 28 -> Mar 1 must be a two-day gap, not one. */
KTEST(rtcEpochSecondsLeapYearFebruaryHas29Days)
{
RtcDateTime feb28 = { .year = 2024, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 };
RtcDateTime mar1 = { .year = 2024, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&mar1) - rtcEpochSeconds(&feb28), 2 * 86400);
}
/* 2023 is not a leap year (not divisible by 4): the same Feb28 -> Mar1
comparison must collapse to exactly one day, proving the non-leap branch
is not just always adding the leap-year's extra day. */
KTEST(rtcEpochSecondsNonLeapYearFebruaryHas28Days)
{
RtcDateTime feb28 = { .year = 2023, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 };
RtcDateTime mar1 = { .year = 2023, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&mar1) - rtcEpochSeconds(&feb28), 86400);
}
/* A century year is a leap year only when also divisible by 400: 2000 is
(400 | 2000), 1900 is not. This is the structural part of the rule the
simpler "divisible by 4" tests above cannot distinguish on their own. */
KTEST(rtcEpochSecondsCenturyLeapRule)
{
RtcDateTime feb28_2000 = { .year = 2000, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 };
RtcDateTime mar1_2000 = { .year = 2000, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&mar1_2000) - rtcEpochSeconds(&feb28_2000), 2 * 86400);
RtcDateTime feb28_1900 = { .year = 1900, .month = 2, .day = 28, .hour = 0, .minute = 0, .second = 0 };
RtcDateTime mar1_1900 = { .year = 1900, .month = 3, .day = 1, .hour = 0, .minute = 0, .second = 0 };
KT_EQ(t, rtcEpochSeconds(&mar1_1900) - rtcEpochSeconds(&feb28_1900), 86400);
}
KTEST(rtcCivilFromEpochUnixEpochIsZero)
{
RtcDateTime dt;
rtcCivilFromEpoch(0, &dt);
KT_EQ(t, dt.year, 1970);
KT_EQ(t, dt.month, 1);
KT_EQ(t, dt.day, 1);
KT_EQ(t, dt.hour, 0);
KT_EQ(t, dt.minute, 0);
KT_EQ(t, dt.second, 0);
}
KTEST(rtcCivilFromEpochTimeOfDayComponent)
{
RtcDateTime dt;
rtcCivilFromEpoch(1 * 3600 + 2 * 60 + 3, &dt);
KT_EQ(t, dt.year, 1970);
KT_EQ(t, dt.month, 1);
KT_EQ(t, dt.day, 1);
KT_EQ(t, dt.hour, 1);
KT_EQ(t, dt.minute, 2);
KT_EQ(t, dt.second, 3);
}
/* Negative epoch (a date before 1970): -1 second must land on the very
last second of 1969, not silently truncate toward 1970-01-01 the way a
naive C division (which rounds toward zero) would. */
KTEST(rtcCivilFromEpochNegativeBeforeEpoch)
{
RtcDateTime dt;
rtcCivilFromEpoch(-1, &dt);
KT_EQ(t, dt.year, 1969);
KT_EQ(t, dt.month, 12);
KT_EQ(t, dt.day, 31);
KT_EQ(t, dt.hour, 23);
KT_EQ(t, dt.minute, 59);
KT_EQ(t, dt.second, 59);
rtcCivilFromEpoch(-86400, &dt);
KT_EQ(t, dt.year, 1969);
KT_EQ(t, dt.month, 12);
KT_EQ(t, dt.day, 31);
KT_EQ(t, dt.hour, 0);
KT_EQ(t, dt.minute, 0);
KT_EQ(t, dt.second, 0);
}
/* The exact leap-year civil date rtcEpochSecondsLeapYearFebruaryHas29Days
above already proves is one apart from March 1st -- rtcCivilFromEpoch
must recover 2024-02-29 exactly, not silently normalize it into
2024-03-01 (the classic off-by-one a wrong leap rule would produce). */
KTEST(rtcCivilFromEpochLeapDayRoundTrip)
{
RtcDateTime leapDay = { .year = 2024, .month = 2, .day = 29, .hour = 12, .minute = 30, .second = 45 };
int64 epoch = rtcEpochSeconds(&leapDay);
RtcDateTime back;
rtcCivilFromEpoch(epoch, &back);
KT_EQ(t, back.year, 2024);
KT_EQ(t, back.month, 2);
KT_EQ(t, back.day, 29);
KT_EQ(t, back.hour, 12);
KT_EQ(t, back.minute, 30);
KT_EQ(t, back.second, 45);
}
/* Round trip through both directions (rtcEpochSeconds then
rtcCivilFromEpoch) across a spread of dates spanning several years,
month/year boundaries, and a century-leap year -- proves the two
functions are genuine inverses of each other over a real range, not
just individually correct at a handful of hand-picked points. */
KTEST(rtcCivilFromEpochRoundTripsAcrossManyDates)
{
RtcDateTime samples[] = {
{ 1970, 1, 1, 0, 0, 0 },
{ 1999, 12, 31, 23, 59, 59 },
{ 2000, 1, 1, 0, 0, 1 },
{ 2000, 2, 29, 6, 7, 8 },
{ 2023, 2, 28, 18, 0, 0 },
{ 2023, 3, 1, 0, 0, 0 },
{ 2026, 9, 7, 17, 30, 0 },
{ 2099, 12, 31, 23, 59, 59 },
};
for (uint64 i = 0; i < sizeof(samples) / sizeof(samples[0]); i++) {
int64 epoch = rtcEpochSeconds(&samples[i]);
RtcDateTime back;
rtcCivilFromEpoch(epoch, &back);
KT_EQ(t, back.year, samples[i].year);
KT_EQ(t, back.month, samples[i].month);
KT_EQ(t, back.day, samples[i].day);
KT_EQ(t, back.hour, samples[i].hour);
KT_EQ(t, back.minute, samples[i].minute);
KT_EQ(t, back.second, samples[i].second);
}
}
KTEST(rtcReadDateTimeReturnsPlausibleFields)
{
RtcDateTime dt;
KT_TRUE(t, rtcReadDateTime(&dt));
KT_GE(t, dt.year, 2020);
KT_LE(t, dt.year, 2099);
KT_GE(t, dt.month, 1);
KT_LE(t, dt.month, 12);
KT_GE(t, dt.day, 1);
KT_LE(t, dt.day, 31);
KT_GE(t, dt.hour, 0);
KT_LT(t, dt.hour, 24);
KT_GE(t, dt.minute, 0);
KT_LT(t, dt.minute, 60);
KT_GE(t, dt.second, 0);
KT_LT(t, dt.second, 60);
}
KTEST(rtcNowIsNeverNegativeOrObviouslyWrong)
{
/* rtcEpochSecondsUnixEpochIsZero above already proves the math side
hits exactly 0 for 1970-01-01; a real read of a QEMU-emulated CMOS
clock in 2026 should be nowhere near that, so a driver that
accidentally reads all-zero registers (unplugged/uninitialized
hardware, a wrong port) is easy to tell apart from a real one here. */
int64 now = rtcNow();
KT_GT(t, now, 0);
/* 2020-01-01T00:00:00Z, a fixed sanity floor well before this test can
ever plausibly run. */
KT_GT(t, now, 1577836800);
}
KTEST(rtcNowIsMonotonicAcrossConsecutiveReads)
{
int64 first = rtcNow();
/* Real port I/O, not a busy-loop the compiler could fold away: two
fresh hardware reads a few microseconds apart must never see time
run backwards. */
for (volatile int i = 0; i < 20000; i++) { }
int64 second = rtcNow();
KT_GE(t, second, first);
}
+1097 -19
View File
File diff suppressed because it is too large. Load diff
+1655
View File
File diff suppressed because it is too large. Load diff
+1426
View File
File diff suppressed because it is too large. Load diff
+1357
View File
File diff suppressed because it is too large. Load diff
+1529 -72
View File
File diff suppressed because it is too large. Load diff
+812
View File
@@ -0,0 +1,812 @@
#include "ktest.h"
#include "shell.h"
#include "string.h"
/* shellTokenize (shell.c) is the pure-string half of the shell's new
`NAME [ARGS] [&]` job-control dispatch (runProgram, shell.c) -- turning
a typed line into a real argv[] the way a normal shell does, instead of
the single "rest of the line" string every other built-in command is
content with. Everything below runs before schedStart() (ktest.h's own
file comment), so it deliberately never touches the VFS, jobctl.h, or a
real spawn -- only the tokenizer itself, which is ordinary deterministic
string manipulation. */
KTEST(shellTokenizeEmptyLine)
{
char text[] = "";
char* tokens[4];
int count = shellTokenize(text, tokens, 4);
KT_EQ(t, count, 0);
}
KTEST(shellTokenizeSingleWord)
{
char text[] = "hello";
char* tokens[4];
int count = shellTokenize(text, tokens, 4);
KT_EQ(t, count, 1);
KT_STREQ(t, tokens[0], "hello");
}
KTEST(shellTokenizeMultipleWordsCollapsesRepeatedSpaces)
{
char text[] = " one two three ";
char* tokens[8];
int count = shellTokenize(text, tokens, 8);
KT_EQ(t, count, 3);
KT_STREQ(t, tokens[0], "one");
KT_STREQ(t, tokens[1], "two");
KT_STREQ(t, tokens[2], "three");
}
KTEST(shellTokenizeAllSpacesYieldsNothing)
{
char text[] = " ";
char* tokens[4];
int count = shellTokenize(text, tokens, 4);
KT_EQ(t, count, 0);
}
KTEST(shellTokenizeRespectsCapacityInsteadOfOverrunningArray)
{
char text[] = "a b c d e";
char* tokens[3];
int count = shellTokenize(text, tokens, 3);
KT_EQ(t, count, 3);
KT_STREQ(t, tokens[0], "a");
KT_STREQ(t, tokens[1], "b");
KT_STREQ(t, tokens[2], "c");
}
KTEST(shellTokenizeTrailingAmpersandIsJustAnotherToken)
{
/* runProgram (shell.c) is the one that gives "&" its background
meaning by inspecting the LAST token itself -- shellTokenize's own
job is only to split words apart, so it must hand "&" back
unchanged like any other token, not special-case it. */
char text[] = "sleepjob arg1 &";
char* tokens[8];
int count = shellTokenize(text, tokens, 8);
KT_EQ(t, count, 3);
KT_STREQ(t, tokens[0], "sleepjob");
KT_STREQ(t, tokens[1], "arg1");
KT_STREQ(t, tokens[2], "&");
}
KTEST(shellTokenizeZeroCapacityYieldsNothing)
{
char text[] = "one two";
char* tokens[1];
int count = shellTokenize(text, tokens, 0);
KT_EQ(t, count, 0);
}
/* shellSplitPipelineStages (shell.c) is the pure-string half of the shell's
`cmd1 | cmd2 | ... | cmdN` pipeline syntax -- deciding whether execute()
should run runPipelineStages (N spawned processes joined by N-1 real
pipes) or fall back to a single command, exactly the same "pure logic, no
VFS/scheduler" split shellTokenize already has from its own doc comment
above. */
KTEST(shellSplitPipelineStagesNoPipeReturnsZero)
{
char text[] = "ls -l";
char* stages[shellMaxPipelineStages];
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, 0);
}
KTEST(shellSplitPipelineStagesBasicTwoStageSplit)
{
char text[] = "producer|consumer";
char* stages[shellMaxPipelineStages];
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, 2);
KT_STREQ(t, stages[0], "producer");
KT_STREQ(t, stages[1], "consumer");
}
KTEST(shellSplitPipelineStagesTrimsSpacesAroundEachPipe)
{
char text[] = "pipesrc arg1 | pipecat | pipesink arg2";
char* stages[shellMaxPipelineStages];
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, 3);
KT_STREQ(t, stages[0], "pipesrc arg1");
KT_STREQ(t, stages[1], "pipecat");
KT_STREQ(t, stages[2], "pipesink arg2");
}
KTEST(shellSplitPipelineStagesThreeStageSplitIsRealNotJustFirstPipe)
{
/* Unlike the old two-way shellSplitPipeline this generalizes, every
top-level '|' is now recognized -- a real N-stage pipeline, not just
the first split with the rest left embedded in one text blob. */
char text[] = "a | b | c";
char* stages[shellMaxPipelineStages];
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, 3);
KT_STREQ(t, stages[0], "a");
KT_STREQ(t, stages[1], "b");
KT_STREQ(t, stages[2], "c");
}
KTEST(shellSplitPipelineStagesEmptyLastStageYieldsEmptyString)
{
char text[] = "producer |";
char* stages[shellMaxPipelineStages];
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, 2);
KT_STREQ(t, stages[0], "producer");
KT_STREQ(t, stages[1], "");
}
KTEST(shellSplitPipelineStagesTooManyStagesReturnsNegativeOneWithoutMutating)
{
char text[] = "a | b | c | d | e";
char* stages[shellMaxPipelineStages];
/* shellMaxPipelineStages is 4, but this line has 5 stages -- must be
rejected outright, leaving `text` completely untouched, exactly the
same "malformed, nothing changed" contract shellExtractRedirects'
own dangling-operator case already uses. */
int count = shellSplitPipelineStages(text, stages, shellMaxPipelineStages);
KT_EQ(t, count, -1);
KT_STREQ(t, text, "a | b | c | d | e");
}
/* shellSplitAndOr (shell.c) is the pure-string half of the shell's new
`cmd1 && cmd2 || cmd3 ...` conditional-chaining syntax -- deciding
whether execute() should short-circuit through several commands' real
exit statuses (executeSingle, shell.c) or fall back to a single
command-or-pipeline exactly as before, run BEFORE pipeline splitting
(`|` binds tighter than `&&`/`||`), the same "pure logic, no
VFS/scheduler" split every other parser above already has. */
KTEST(shellSplitAndOrNoOperatorReturnsZero)
{
char text[] = "ls -l";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 0);
}
KTEST(shellSplitAndOrBasicAndSplit)
{
char text[] = "mkdir /tmp/a && echo ok";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 2);
KT_STREQ(t, segments[0], "mkdir /tmp/a");
KT_STREQ(t, segments[1], "echo ok");
KT_EQ(t, ops[0], shellAndOrAnd);
}
KTEST(shellSplitAndOrBasicOrSplit)
{
char text[] = "false || echo fallback";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 2);
KT_STREQ(t, segments[0], "false");
KT_STREQ(t, segments[1], "echo fallback");
KT_EQ(t, ops[0], shellAndOrOr);
}
KTEST(shellSplitAndOrMixedChainRecordsEachOperatorIndependently)
{
/* The classic shell if-then-else idiom: "run b only if a succeeded,
run c only if the segment that actually ran last (a or b) did NOT
succeed" -- proves && and || are tracked as two independently
recorded operators, not collapsed into one boolean. */
char text[] = "a && b || c";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 3);
KT_STREQ(t, segments[0], "a");
KT_STREQ(t, segments[1], "b");
KT_STREQ(t, segments[2], "c");
KT_EQ(t, ops[0], shellAndOrAnd);
KT_EQ(t, ops[1], shellAndOrOr);
}
KTEST(shellSplitAndOrTrimsSpacesAroundEachOperator)
{
char text[] = "a&&b";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 2);
KT_STREQ(t, segments[0], "a");
KT_STREQ(t, segments[1], "b");
}
KTEST(shellSplitAndOrLeavesSingleTopLevelPipeInsideItsOwnSegment)
{
/* '|' binds tighter than '&&'/'||': a lone pipe character must never be
mistaken for half of '||' (which only matches two CONSECUTIVE '|'
characters), so "a | b" stays a single, whole segment here -- only
execute()'s own later shellSplitPipelineStages call on that segment
ever splits it further. */
char text[] = "a | b && c";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, 2);
KT_STREQ(t, segments[0], "a | b");
KT_STREQ(t, segments[1], "c");
KT_EQ(t, ops[0], shellAndOrAnd);
}
KTEST(shellSplitAndOrTooManySegmentsReturnsNegativeOneWithoutMutating)
{
char text[] = "a && b && c && d && e";
char* segments[shellMaxAndOrSegments];
ShellAndOrOp ops[shellMaxAndOrSegments - 1];
/* shellMaxAndOrSegments is 4, but this line has 5 segments -- must be
rejected outright, leaving `text` completely untouched, the same
"malformed, nothing changed" contract shellSplitPipelineStages' own
analogous limit already uses. */
int count = shellSplitAndOr(text, segments, ops, shellMaxAndOrSegments);
KT_EQ(t, count, -1);
KT_STREQ(t, text, "a && b && c && d && e");
}
/* shellExtractRedirects (shell.c) is the pure-token half of shell.c's new
`cmd [ARGS] [< IN] [> OUT | >> OUT]` file redirection -- exactly the same
"pure logic, no VFS/scheduler" split shellTokenize/shellSplitPipelineStages
already have from their own doc comments above, operating on
shellTokenize's own output shape (an already-split argv) instead of a
raw line. */
KTEST(shellExtractRedirectsNoOperatorsLeavesArgvUnchanged)
{
char text[] = "cat file.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 2);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, argv[1], "file.txt");
KT_NULL(t, redirect.inPath);
KT_NULL(t, redirect.outPath);
}
KTEST(shellExtractRedirectsOutputTruncate)
{
char text[] = "sort > out.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "sort");
KT_NULL(t, redirect.inPath);
KT_NOTNULL(t, redirect.outPath);
KT_STREQ(t, redirect.outPath, "out.txt");
KT_FALSE(t, redirect.appendOut);
}
KTEST(shellExtractRedirectsOutputAppend)
{
char text[] = "sort >> out.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "sort");
KT_STREQ(t, redirect.outPath, "out.txt");
KT_TRUE(t, redirect.appendOut);
}
KTEST(shellExtractRedirectsInputAndOutputTogetherKeepRealArgs)
{
char text[] = "sort -r < in.txt > out.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 2);
KT_STREQ(t, argv[0], "sort");
KT_STREQ(t, argv[1], "-r");
KT_STREQ(t, redirect.inPath, "in.txt");
KT_STREQ(t, redirect.outPath, "out.txt");
KT_FALSE(t, redirect.appendOut);
}
KTEST(shellExtractRedirectsOperatorsCanComeBeforeRealArgs)
{
/* A real shell accepts redirection operators anywhere on the line, not
only trailing -- "> out.txt cat file.txt" is a weird but legal way
to write "cat file.txt > out.txt". */
char text[] = "> out.txt cat file.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 2);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, argv[1], "file.txt");
KT_STREQ(t, redirect.outPath, "out.txt");
}
KTEST(shellExtractRedirectsLastOutputOperatorWins)
{
char text[] = "cat > a.txt >> b.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, redirect.outPath, "b.txt");
KT_TRUE(t, redirect.appendOut);
}
KTEST(shellExtractRedirectsDanglingOperatorFailsWithoutMutating)
{
char text[] = "cat file.txt >";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
int originalArgc = argc;
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_FALSE(t, ok);
KT_EQ(t, argc, originalArgc);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, argv[1], "file.txt");
KT_STREQ(t, argv[2], ">");
}
KTEST(shellExtractRedirectsNoTokensYieldsNoRedirects)
{
char* argv[1];
int argc = 0;
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 0);
KT_NULL(t, redirect.inPath);
KT_NULL(t, redirect.outPath);
}
/* `2>`/`2>>` (stderr redirection) are a separate operator family from
`<`/`>`/`>>` above, targeting fd 2 instead of fd 0/fd 1 -- covered on
their own (truncate, append, combined with stdout, last-one-wins,
dangling) plus one case proving the two families never interfere with
each other even when both appear on the same line. */
KTEST(shellExtractRedirectsErrTruncate)
{
char text[] = "prog 2> err.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "prog");
KT_NULL(t, redirect.inPath);
KT_NULL(t, redirect.outPath);
KT_NOTNULL(t, redirect.errPath);
KT_STREQ(t, redirect.errPath, "err.txt");
KT_FALSE(t, redirect.appendErr);
}
KTEST(shellExtractRedirectsErrAppend)
{
char text[] = "prog 2>> err.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "prog");
KT_STREQ(t, redirect.errPath, "err.txt");
KT_TRUE(t, redirect.appendErr);
}
KTEST(shellExtractRedirectsStdoutAndStderrTogetherKeepRealArgs)
{
char text[] = "sort -r > out.txt 2> err.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 2);
KT_STREQ(t, argv[0], "sort");
KT_STREQ(t, argv[1], "-r");
KT_STREQ(t, redirect.outPath, "out.txt");
KT_FALSE(t, redirect.appendOut);
KT_STREQ(t, redirect.errPath, "err.txt");
KT_FALSE(t, redirect.appendErr);
}
KTEST(shellExtractRedirectsLastErrOperatorWins)
{
char text[] = "cat 2> a.txt 2>> b.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, redirect.errPath, "b.txt");
KT_TRUE(t, redirect.appendErr);
}
KTEST(shellExtractRedirectsDanglingErrOperatorFailsWithoutMutating)
{
char text[] = "cat file.txt 2>";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
int originalArgc = argc;
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_FALSE(t, ok);
KT_EQ(t, argc, originalArgc);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, argv[1], "file.txt");
KT_STREQ(t, argv[2], "2>");
}
/* `<<<` (here-string) is a third, independent fd-0 operator alongside `<` --
covered on its own (basic recognition, combined with real args and `>`,
dangling), plus the two last-one-wins cases proving `<` and `<<<` share
ownership of the SAME fd 0 (whichever comes later in the line wins,
clearing the other field entirely) rather than being two unrelated
fields a caller could accidentally read both of at once. */
KTEST(shellExtractRedirectsHereStringBasic)
{
char text[] = "cat <<< hello";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, redirect.hereString, "hello");
KT_NULL(t, redirect.inPath);
}
KTEST(shellExtractRedirectsHereStringWithRealArgsAndOutput)
{
char text[] = "sort -r <<< hello > out.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 2);
KT_STREQ(t, argv[0], "sort");
KT_STREQ(t, argv[1], "-r");
KT_STREQ(t, redirect.hereString, "hello");
KT_STREQ(t, redirect.outPath, "out.txt");
KT_NULL(t, redirect.inPath);
}
KTEST(shellExtractRedirectsDanglingHereStringFailsWithoutMutating)
{
char text[] = "cat <<<";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
int originalArgc = argc;
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_FALSE(t, ok);
KT_EQ(t, argc, originalArgc);
KT_STREQ(t, argv[0], "cat");
KT_STREQ(t, argv[1], "<<<");
}
KTEST(shellExtractRedirectsHereStringAfterFileRedirectWins)
{
char text[] = "cat < a.txt <<< b";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, redirect.hereString, "b");
KT_NULL(t, redirect.inPath);
}
KTEST(shellExtractRedirectsFileRedirectAfterHereStringWins)
{
char text[] = "cat <<< b < a.txt";
char* argv[8];
int argc = shellTokenize(text, argv, 8);
ShellRedirect redirect;
bool ok = shellExtractRedirects(argv, &argc, &redirect);
KT_TRUE(t, ok);
KT_EQ(t, argc, 1);
KT_STREQ(t, redirect.inPath, "a.txt");
KT_NULL(t, redirect.hereString);
}
/* shellParseVarRef (shell.c) is the pure-syntax half of the shell's new
`$NAME` unquoted parameter expansion -- recognizing the "$" + name-chars
shape, never touching sched.h's Thread.env (that lookup is
shellExpandArgv's job, not KTEST-able here for the same pre-scheduler
reason shellTokenize's own file comment gives). */
KTEST(shellParseVarRefAcceptsPlainName)
{
char name[16];
bool ok = shellParseVarRef("$HOME", name, sizeof(name));
KT_TRUE(t, ok);
KT_STREQ(t, name, "HOME");
}
KTEST(shellParseVarRefAcceptsDigitsAndUnderscore)
{
char name[16];
bool ok = shellParseVarRef("$_var_2", name, sizeof(name));
KT_TRUE(t, ok);
KT_STREQ(t, name, "_var_2");
}
KTEST(shellParseVarRefRejectsBareDollarSign)
{
char name[16];
KT_FALSE(t, shellParseVarRef("$", name, sizeof(name)));
}
KTEST(shellParseVarRefRejectsTokenWithoutLeadingDollar)
{
char name[16];
KT_FALSE(t, shellParseVarRef("HOME", name, sizeof(name)));
}
KTEST(shellParseVarRefRejectsEmbeddedPunctuation)
{
char name[16];
KT_FALSE(t, shellParseVarRef("$FOO=bar", name, sizeof(name)));
KT_FALSE(t, shellParseVarRef("$FOO/bar", name, sizeof(name)));
}
KTEST(shellParseVarRefRejectsNameTooLongForCapacity)
{
char name[4];
KT_FALSE(t, shellParseVarRef("$TOOLONG", name, sizeof(name)));
}
/* shellParseVarRef must never accept `$?` itself -- '?' is not in its own
[A-Za-z0-9_] name-character set -- so the two token classes
(shellIsStatusVarToken below and shellParseVarRef above) can never
both claim the same token. */
KTEST(shellParseVarRefRejectsQuestionMark)
{
char name[16];
KT_FALSE(t, shellParseVarRef("$?", name, sizeof(name)));
}
/* shellIsStatusVarToken (shell.c) is the pure-syntax half of the shell's
new `$?` special parameter -- recognizing the EXACT two-character token
"$?", never touching this file's own file-scope `lastStatus` (that
substitution is shellExpandToken's job, not KTEST-able here for the
same pre-scheduler reason shellExpandArgv's own env lookups are not). */
KTEST(shellIsStatusVarTokenAcceptsExactMatch)
{
KT_TRUE(t, shellIsStatusVarToken("$?"));
}
KTEST(shellIsStatusVarTokenRejectsBareDollarSign)
{
KT_FALSE(t, shellIsStatusVarToken("$"));
}
KTEST(shellIsStatusVarTokenRejectsBareQuestionMark)
{
KT_FALSE(t, shellIsStatusVarToken("?"));
}
KTEST(shellIsStatusVarTokenRejectsTrailingGarbage)
{
KT_FALSE(t, shellIsStatusVarToken("$?x"));
}
KTEST(shellIsStatusVarTokenRejectsLeadingGarbage)
{
KT_FALSE(t, shellIsStatusVarToken("x$?"));
}
/* shellGlobMatch/shellTokenHasGlob/shellSplitGlobToken (shell.c) are the
pure-logic half of the shell's new wildcard glob expansion
(shellExpandGlobs, which actually walks the VFS and is not KTEST-able
here for the same pre-scheduler reason shellExpandArgv's own VFS/thread
lookups are not) -- the same "pure logic here, thread/VFS-touching glue
stays static in shell.c" split every other pure function above already
follows. */
KTEST(shellGlobMatchExactLiteral)
{
KT_TRUE(t, shellGlobMatch("hello", "hello"));
KT_FALSE(t, shellGlobMatch("hello", "hellox"));
KT_FALSE(t, shellGlobMatch("hello", "hell"));
}
KTEST(shellGlobMatchStarMatchesAnySuffix)
{
KT_TRUE(t, shellGlobMatch("a*", "a"));
KT_TRUE(t, shellGlobMatch("a*", "abc"));
KT_FALSE(t, shellGlobMatch("a*", "xabc"));
}
KTEST(shellGlobMatchStarMatchesAnyPrefix)
{
KT_TRUE(t, shellGlobMatch("*.txt", "a.txt"));
KT_TRUE(t, shellGlobMatch("*.txt", ".txt"));
KT_FALSE(t, shellGlobMatch("*.txt", "a.log"));
}
KTEST(shellGlobMatchStarInTheMiddle)
{
KT_TRUE(t, shellGlobMatch("a*b", "ab"));
KT_TRUE(t, shellGlobMatch("a*b", "axxxb"));
KT_FALSE(t, shellGlobMatch("a*b", "axxxc"));
}
KTEST(shellGlobMatchMultipleStars)
{
KT_TRUE(t, shellGlobMatch("*a*b*", "aXbY"));
KT_TRUE(t, shellGlobMatch("*a*b*", "ab"));
KT_FALSE(t, shellGlobMatch("*a*b*", "ba"));
}
KTEST(shellGlobMatchBareStarMatchesEverythingIncludingEmpty)
{
KT_TRUE(t, shellGlobMatch("*", ""));
KT_TRUE(t, shellGlobMatch("*", "anything"));
}
KTEST(shellGlobMatchQuestionMarkMatchesExactlyOneChar)
{
KT_TRUE(t, shellGlobMatch("a?c", "abc"));
KT_FALSE(t, shellGlobMatch("a?c", "ac"));
KT_FALSE(t, shellGlobMatch("a?c", "abbc"));
}
KTEST(shellGlobMatchEmptyPatternOnlyMatchesEmptyName)
{
KT_TRUE(t, shellGlobMatch("", ""));
KT_FALSE(t, shellGlobMatch("", "x"));
}
KTEST(shellTokenHasGlobDetectsStarAndQuestionMark)
{
KT_TRUE(t, shellTokenHasGlob("*.txt"));
KT_TRUE(t, shellTokenHasGlob("file?.log"));
KT_FALSE(t, shellTokenHasGlob("plain.txt"));
KT_FALSE(t, shellTokenHasGlob(""));
}
KTEST(shellSplitGlobTokenNoSlashYieldsEmptyDir)
{
char dir[64];
char pattern[64];
shellSplitGlobToken("*.txt", dir, sizeof(dir), pattern, sizeof(pattern));
KT_STREQ(t, dir, "");
KT_STREQ(t, pattern, "*.txt");
}
KTEST(shellSplitGlobTokenSplitsAtLastSlashIncludingIt)
{
char dir[64];
char pattern[64];
shellSplitGlobToken("/tmp/gd/*.txt", dir, sizeof(dir), pattern, sizeof(pattern));
KT_STREQ(t, dir, "/tmp/gd/");
KT_STREQ(t, pattern, "*.txt");
}
KTEST(shellSplitGlobTokenNoGlobCharStillSplitsCleanly)
{
/* shellSplitGlobToken itself does not care whether the pattern half
actually contains a glob character -- that gate is
shellTokenHasGlob's own job, called first by shellExpandGlobs. */
char dir[64];
char pattern[64];
shellSplitGlobToken("/tmp/gd/plain.txt", dir, sizeof(dir), pattern, sizeof(pattern));
KT_STREQ(t, dir, "/tmp/gd/");
KT_STREQ(t, pattern, "plain.txt");
}
/* shellFindCommandSubst (shell.c) is the pure-syntax half of the shell's
new `$(cmd [args])` command-substitution expansion -- the impure glue
that actually spawns and captures a command's output (shellExpandCommandSubst,
static in shell.c) needs a real jobctl spawn, off limits for KTEST's
pre-scheduler context, so only the locate-and-extract half is covered
here, the same "pure logic here, thread/VFS-touching glue stays static
in shell.c" split every other shell.h syntax function already gets. */
KTEST(shellFindCommandSubstNoDollarParenReturnsNotFound)
{
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("echo hello", &start, &end, cmd, sizeof(cmd)));
KT_EQ(t, start, -1);
}
KTEST(shellFindCommandSubstWholeToken)
{
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("$(date)", &start, &end, cmd, sizeof(cmd)));
KT_EQ(t, start, 0);
KT_EQ(t, end, 7);
KT_STREQ(t, cmd, "date");
}
KTEST(shellFindCommandSubstEmbeddedInLargerLine)
{
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("echo $(date) done", &start, &end, cmd, sizeof(cmd)));
KT_EQ(t, start, 5);
KT_EQ(t, end, 12);
KT_STREQ(t, cmd, "date");
}
KTEST(shellFindCommandSubstCapturesMultiWordCommand)
{
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("argecho $(argecho a b)", &start, &end, cmd, sizeof(cmd)));
KT_STREQ(t, cmd, "argecho a b");
}
KTEST(shellFindCommandSubstUnterminatedIsMalformed)
{
int start, end;
char cmd[32];
KT_FALSE(t, shellFindCommandSubst("echo $(date", &start, &end, cmd, sizeof(cmd)));
}
KTEST(shellFindCommandSubstEmptyParensYieldsEmptyCommand)
{
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("$()", &start, &end, cmd, sizeof(cmd)));
KT_EQ(t, start, 0);
KT_EQ(t, end, 3);
KT_STREQ(t, cmd, "");
}
KTEST(shellFindCommandSubstTruncatesToCapacity)
{
int start, end;
char cmd[4];
KT_TRUE(t, shellFindCommandSubst("$(longcommand)", &start, &end, cmd, sizeof(cmd)));
KT_STREQ(t, cmd, "lon");
}
KTEST(shellFindCommandSubstOnlyMatchesFirstOccurrence)
{
/* No nested-paren support (documented in shell.h) -- the very NEXT ')'
after the opening '$(' ends the match, so a second, unrelated
`$(...)` later in the line is left completely untouched for a
caller's own repeat scan (shellExpandCommandSubst's own loop) to
find on a later pass, never folded into this one's own cmdOut. */
int start, end;
char cmd[32];
KT_TRUE(t, shellFindCommandSubst("$(a) $(b)", &start, &end, cmd, sizeof(cmd)));
KT_EQ(t, start, 0);
KT_EQ(t, end, 4);
KT_STREQ(t, cmd, "a");
}
+1247
View File
File diff suppressed because it is too large. Load diff
+41
View File
@@ -0,0 +1,41 @@
#include "syscall.h"
/*
* Shared signal-delivery trampoline (see signal.h/signal.c). This is NOT
* entered like a normal function call from C -- signal.c's
* sigConsumePending rewrites a thread's own saved trapframe so that
* iretq lands ring 3 execution directly at sigTrampolineStart, with %rax
* already holding the handler's address and %rdi already holding the
* signal number (the handler's sole SysV argument), exactly as if the
* kernel itself had performed `call *%rax` with that argument set up.
*
* Position independent by construction: every instruction here is either
* PC-relative (the final backward jmp) or references no address at all
* (call through a register, an immediate syscall number, int $0x80) -- so
* sigInstallTrampoline can copy these bytes to any physical frame and map
* that frame at any virtual address (sigTrampolineVirtualAddr) and it
* still runs correctly, unlike this same code linked and executed in
* place inside the kernel's own (very different) virtual address range.
*/
.section .text
.code64
.global sigTrampolineStart
.global sigTrampolineEnd
sigTrampolineStart:
call *%rax
movl $sysSigReturn, %eax
int $0x80
/* Only reached if sysSigReturn somehow returned instead of restoring the
pre-signal context and resuming it (a kernel bug, not a state a correct
sysOnSigReturn can produce) -- park here rather than run off into
whatever garbage happens to follow this page. */
sigTrampolineHang:
hlt
jmp sigTrampolineHang
sigTrampolineEnd:
.section .note.GNU-stack, "", @progbits
+402
View File
@@ -0,0 +1,402 @@
#include "signal.h"
#include "string.h"
#include "pmm.h"
#include "paging.h"
/* Machine code for the shared trampoline page, sigTrampoline.S -- position
independent by construction (no absolute address references at all), so
copying its bytes anywhere and mapping them there is enough. */
extern char sigTrampolineStart[];
extern char sigTrampolineEnd[];
/* Physical frame addresses backing sigInfoVirtualAddr's page(s), filled in
by sigInstallInfoPage -- kept as separate frames (not assumed physically
contiguous, unlike their virtual mapping, which IS contiguous by
construction: pmmAlloc gives no contiguity guarantee across calls, so
sigConsumePending must look up the right frame for a given slot rather
than pointer-arithmetic across a single base). Physical frames below the
1 GiB boot-time identity map (see sigTrampolineVirtualAddr's own comment)
are directly dereferenceable through their own physical value from
kernel code, exactly like sigInstallTrampoline's memcpy into `frame`
above already relies on. */
static uint64 sigInfoFrames[sigInfoMaxPages];
static uint32 sigInfoPageCount = 0;
/* How many SigInfo structs fit in one frameSize page -- shared by
sigInstallInfoPage's sizing loop and sigConsumePending's slot lookup, so
the two can never disagree about the layout. */
#define sigInfoSlotsPerPage (frameSize / sizeof(SigInfo))
void sigResetState(Thread* thread)
{
thread->sigPending = 0;
thread->sigBlocked = 0;
thread->sigHandlerDepth = 0;
thread->sigInRetryableSyscall = false;
memset(thread->sigHandlers, 0, sizeof(thread->sigHandlers));
memset(thread->sigHandlerMasks, 0, sizeof(thread->sigHandlerMasks));
memset(thread->sigHandlerFlags, 0, sizeof(thread->sigHandlerFlags));
memset(thread->sigPendingInfo, 0, sizeof(thread->sigPendingInfo));
memset(thread->sigSavedBlocked, 0, sizeof(thread->sigSavedBlocked));
memset(thread->sigSavedRegs, 0, sizeof(thread->sigSavedRegs));
}
/* Real-world default disposition for signals this kernel actually defines
that is Ignore rather than Terminate -- SIGCHLD (no handler installed:
nothing happens) and SIGCONT (its actual resume effect already happened
unconditionally in sigRaise -- see file comment -- so "no handler" here
just means no handler runs, not that nothing happened at all). SIGSTOP
never reaches this function to begin with (sigRaise bypasses the whole
scan for it, like sigKill), so it is deliberately absent from this list,
the same way sigKill already was. SIGTSTP is NOT here -- its default is
Stop, not Ignore, see sigDefaultIsStop below. Every other defined signal
defaults to Terminate. */
static bool sigDefaultIsIgnore(int signum)
{
switch (signum) {
case sigChld:
case sigCont:
return true;
default:
return false;
}
}
/* Real-world default disposition that is Stop rather than Terminate:
SIGTSTP only. Unlike SIGSTOP (see file comment / sigRaise), SIGTSTP is
fully catchable/blockable/ignorable -- this default only applies when it
is neither, exactly like sigDefaultIsIgnore's entries. */
static bool sigDefaultIsStop(int signum)
{
return signum == sigTstp;
}
/* Whether `signum` would genuinely be delivered (a real handler OR a
default disposition of Terminate/Stop -- anything sigConsumePending
itself would act on, not silently dismiss) if `thread` returned to ring 3
right now. Deliberately mirrors sigConsumePending's own
blocked/ignored-dismissal logic for exactly one signal number rather than
sharing code with it -- sigConsumePending also mutates sigPending/
sigHandlerDepth/the trapframe as it scans, none of which this pure
predicate may do, since it runs from sigRaise against a target that is
not currently resuming at all. Used solely to decide whether a Sleeping/
Blocked target needs early-waking (threadWakeForSignal, sched.c) for this
signal -- if this ever drifted out of sync with sigConsumePending's own
logic, the failure mode is always benign either way: "wakes for a signal
that turns out not to be delivered" is a harmless spurious wake (the
target's own retry-syscall loop just re-blocks, exactly like a real
spurious wakeup), and "does not wake for one that would have delivered"
just falls back to today's behavior of waiting for a natural wake. */
static bool sigWouldDeliver(const Thread* thread, int signum)
{
uint64 bit = 1ull << (uint32)signum;
if (thread->sigBlocked & bit)
return false;
uint64 handler = thread->sigHandlers[signum];
if (handler == sigHandlerIgnore ||
(handler == sigHandlerDefault && sigDefaultIsIgnore(signum)))
return false;
return true;
}
bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus)
{
if (signum <= 0 || signum >= sigMaxSignals)
return false;
/* SIGKILL never touches sigPending/sigBlocked at all -- it is
delegated straight to the pre-existing unconditional termination
path, exactly like real kill(pid, SIGKILL) bypassing every handler/
mask a target could have installed. threadKill does its own id
validation (range, state), so none is duplicated here. Neither it
nor SIGSTOP just below ever reaches a sigHandlers[] entry that could
even be sigFlagSigInfo, so the caller-supplied SigInfo fields are
simply never used on either of these two paths. */
if (signum == sigKill)
return threadKill(targetId);
/* SIGSTOP: uncatchable, unblockable, unignorable, exactly like SIGKILL
above -- bypasses sigPending/sigHandlers/sigBlocked entirely and acts
on ThreadState directly. threadStop does its own id validation. */
if (signum == sigStop)
return threadStop(targetId);
if (targetId == 0 || targetId >= threadCapacity())
return false;
Thread* target = threadAt(targetId);
if (target->state == threadDead || target->state == threadUnused)
return false;
/* SIGCONT's resume is likewise not something a mask or a missing
handler can defer -- called unconditionally before the generic
pending-bit path below, so a Stopped target is moved back to
threadRunnable regardless of whether it also has a handler
installed. Its return value is intentionally ignored: "target
was not actually Stopped" is a harmless no-op at the process-state
level (real SIGCONT to a running process), not a reason to skip
delivering the signal itself for any installed handler. */
if (signum == sigCont)
threadContinue(targetId);
target->sigPending |= (1ull << (uint32)signum);
/* Attached regardless of whether signum's handler actually has
sigFlagSigInfo set (sigConsumePending is what decides whether this
ever gets read at all) -- cheap (4 ints), and simpler than threading
the handler's own flags in here just to skip it, matching how
sigHandlerMasks/sigHandlerFlags are likewise set unconditionally by
sigSetHandler and only ever consulted conditionally. */
target->sigPendingInfo[signum].siSignum = signum;
target->sigPendingInfo[signum].siCode = siCode;
target->sigPendingInfo[signum].siPid = siPid;
target->sigPendingInfo[signum].siStatus = siStatus;
/* Real Unix's interruptible sleep/EINTR (deliberately out of scope
until now, see the file comment's history): a Sleeping/Blocked
target whose sleep timer or wait-channel condition would otherwise
never naturally fire again (e.g. user/eintrtarget.c's read that no
writer will ever satisfy) must still actually receive a signal that
would genuinely be delivered, not sit on it forever. Skipped
entirely for a blocked or ignored signal -- see sigWouldDeliver --
so a masked/ignored signal never disturbs a thread's sleep/wait at
all, matching real Unix. threadWakeForSignal itself refuses anything
other than Sleeping/Blocked (Stopped is excluded on purpose -- only
threadContinue may move it), so this is safe to call unconditionally
whenever sigWouldDeliver agrees. */
if (sigWouldDeliver(target, signum))
threadWakeForSignal(targetId);
return true;
}
bool sigRaise(uint32 targetId, int signum)
{
return sigRaiseInfo(targetId, signum, siCodeKernel, 0, 0);
}
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid)
{
bool anyTarget = false;
for (uint32 id = 1; id < threadCapacity(); id++) {
Thread* candidate = threadAt(id);
if (candidate->state == threadDead || candidate->state == threadUnused)
continue;
if (candidate->pgid != pgid)
continue;
anyTarget = true;
sigRaiseInfo(id, signum, siCode, siPid, 0);
}
return anyTarget;
}
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags)
{
if (signum <= 0 || signum >= sigMaxSignals)
return sigSetFailed;
if (signum == sigKill || signum == sigStop)
return sigSetFailed;
uint64 previous = thread->sigHandlers[signum];
thread->sigHandlers[signum] = handlerAddr;
/* Same stripping sigProcMask already does to a mask it is handed: a
mask cannot make sigKill/sigStop blockable just because it asked. */
mask &= ~(1ull << sigKill);
mask &= ~(1ull << sigStop);
thread->sigHandlerMasks[signum] = mask;
thread->sigHandlerFlags[signum] = flags;
return previous;
}
uint64 sigProcMask(Thread* thread, int how, uint64 mask)
{
uint64 previous = thread->sigBlocked;
mask &= ~(1ull << sigKill);
mask &= ~(1ull << sigStop);
switch (how) {
case sigMaskBlock:
thread->sigBlocked |= mask;
break;
case sigMaskUnblock:
thread->sigBlocked &= ~mask;
break;
case sigMaskSetMask:
thread->sigBlocked = mask;
break;
default:
break;
}
return previous;
}
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum)
{
if (thread->sigHandlerDepth >= sigMaxNestDepth)
return sigOutcomeNone;
for (int signum = 1; signum < sigMaxSignals; signum++) {
uint64 bit = 1ull << (uint32)signum;
if (!(thread->sigPending & bit) || (thread->sigBlocked & bit))
continue;
thread->sigPending &= ~bit;
uint64 handler = thread->sigHandlers[signum];
if (handler == sigHandlerIgnore ||
(handler == sigHandlerDefault && sigDefaultIsIgnore(signum)))
continue;
if (outSignum)
*outSignum = signum;
if (handler == sigHandlerDefault) {
if (sigDefaultIsStop(signum))
return sigOutcomeStop;
return sigOutcomeTerminate;
}
/* SA_RESTART / EINTR (see file comment): consumed here, exactly
once, regardless of which way the decision goes -- a signal
nested INSIDE the handler this delivery is about to enter is
interrupting that handler's own code, not a retryable syscall
retry point, so it must never see this as still true. Only
rewrites `regs` (the context about to be saved below) when this
thread was genuinely mid one of sysRead/sysWrite/sysWait's own
retry-on-block pattern AND this particular signal was not
registered with sigFlagRestart -- anything else (plain running
code, a sleep, an already-nested handler, or sigFlagRestart
itself) leaves `regs` completely untouched, preserving the
unconditional-retry behavior this kernel had before the flag
existed. */
bool interruptedRetry = thread->sigInRetryableSyscall;
thread->sigInRetryableSyscall = false;
if (interruptedRetry && !(thread->sigHandlerFlags[signum] & sigFlagRestart)) {
regs->rip += 2;
regs->rax = (uint64)(int64)errInterrupted;
}
/* Push the whole interrupted trapframe (now possibly rewritten
above) onto this thread's small context stack (the only copy of
it that exists anywhere -- unlike a real kernel this never pushes
one onto the user stack) so sigOnSigReturn can restore it byte
for byte, even if a DIFFERENT, unmasked signal nests another
handler on top before this one ever returns. */
uint32 depth = thread->sigHandlerDepth;
thread->sigSavedRegs[depth] = *regs;
thread->sigSavedBlocked[depth] = thread->sigBlocked;
thread->sigHandlerDepth = depth + 1;
/* sa_mask (sigHandlerMasks[signum], set by sigSetHandler) plus the
signal's own bit (self-blocked, like real Unix's implicit
default) are ADDED to whatever was already blocked -- a union,
not a replacement, so an outer handler's own blocked set stays
blocked for this nested one too. sigFlagNoDefer (signalnum.h)
opts out of exactly the self-block half of that union: a signal
registered with it does not gain its own bit here, so a second
raise of the SAME signum while this handler is still active
finds sigBlocked genuinely clear for it and nests, instead of
merely staying pending until this handler returns. Stripped of
sigKill/sigStop for the same reason every other mask operation in
this file is: neither ever consults sigBlocked at all, so this is
purely cosmetic, but leaving either bit set here would visibly
contradict "can never be blocked" for anything that inspects
sigBlocked. */
uint64 newBlocked = thread->sigBlocked | thread->sigHandlerMasks[signum];
if (!(thread->sigHandlerFlags[signum] & sigFlagNoDefer))
newBlocked |= bit;
newBlocked &= ~(1ull << sigKill);
newBlocked &= ~(1ull << sigStop);
thread->sigBlocked = newBlocked;
/* Rewrite `regs` in place to enter the shared trampoline: rip is
the trampoline's entry, rax is the handler address its
`call *%rax` invokes, rdi is signum (the handler's sole SysV
argument). regs->rsp (the interrupted user stack) is left
untouched -- the trampoline runs on the target's own stack,
right where it already was. */
regs->rax = handler;
regs->rdi = (uint64)signum;
regs->rip = sigTrampolineVirtualAddr;
/* sigFlagSigInfo (signalnum.h): copy the SigInfo sigRaiseInfo
attached to this signum into THIS (thread, depth) pair's own
slot of the shared sigInfoVirtualAddr page -- a different slot
than any still-active outer handler's own siginfo pointer, by
the exact same depth-indexed reasoning sigSavedRegs/
sigSavedBlocked already use, so a nested sigFlagSigInfo delivery
can never clobber an outer one's still-unread copy. rdx is
always 0 (no ucontext_t this kernel can expose). Left completely
untouched otherwise -- see this function's own doc comment. */
if (thread->sigHandlerFlags[signum] & sigFlagSigInfo && sigInfoPageCount > 0) {
uint32 slot = thread->id * sigMaxNestDepth + depth;
uint32 page = slot / sigInfoSlotsPerPage;
uint32 offset = (slot % sigInfoSlotsPerPage) * (uint32)sizeof(SigInfo);
SigInfo* dst = (SigInfo*)(sigInfoFrames[page] + offset);
*dst = thread->sigPendingInfo[signum];
regs->rsi = sigInfoVirtualAddr + (uint64)page * frameSize + offset;
regs->rdx = 0;
}
return sigOutcomeHandler;
}
return sigOutcomeNone;
}
bool sigOnSigReturn(Thread* thread, Registers* regs)
{
if (thread->sigHandlerDepth == 0)
return false;
uint32 depth = thread->sigHandlerDepth - 1;
*regs = thread->sigSavedRegs[depth];
thread->sigBlocked = thread->sigSavedBlocked[depth];
thread->sigHandlerDepth = depth;
return true;
}
void sigInstallTrampoline(void)
{
uint64 frame = pmmAlloc();
if (frame == 0)
return;
uint64 length = (uint64)(sigTrampolineEnd - sigTrampolineStart);
memset((void*)frame, 0, frameSize);
memcpy((void*)frame, sigTrampolineStart, length);
pagingMap(sigTrampolineVirtualAddr, frame, pagePresent | pageUser);
}
void sigInstallInfoPage(void)
{
uint64 totalSlots = (uint64)threadCapacity() * sigMaxNestDepth;
uint64 pagesNeeded = (totalSlots + sigInfoSlotsPerPage - 1) / sigInfoSlotsPerPage;
if (pagesNeeded > sigInfoMaxPages)
pagesNeeded = sigInfoMaxPages;
for (uint32 i = 0; i < pagesNeeded; i++) {
uint64 frame = pmmAlloc();
if (frame == 0)
return;
memset((void*)frame, 0, frameSize);
/* pageUser, no pageWrite: a handler may only ever READ the SigInfo
it was handed a pointer to, exactly like the trampoline page
above is read+execute, never write -- the kernel is the only
writer, via `frame`'s own physical address (see the frame
comment above sigInfoFrames), never through this user-facing
mapping. */
pagingMap(sigInfoVirtualAddr + (uint64)i * frameSize, frame, pagePresent | pageUser);
sigInfoFrames[i] = frame;
}
sigInfoPageCount = (uint32)pagesNeeded;
}
+130
View File
@@ -0,0 +1,130 @@
#include "slab.h"
#include "pmm.h"
typedef struct FreeObj {
struct FreeObj* next;
} FreeObj;
typedef struct Slab {
struct Slab* next;
FreeObj* freeList;
usize freeCount;
} Slab;
static usize alignUp(usize value, usize alignment)
{
return (value + alignment - 1) & ~(alignment - 1);
}
static Slab* growSlab(KmemCache* cache)
{
uint64 frame = pmmAlloc();
if (frame == 0)
return nullptr;
Slab* slab = (Slab*)frame;
slab->next = cache->slabs;
slab->freeCount = cache->objsPerSlab;
uint8* base = (uint8*)frame + alignUp(sizeof(Slab), sizeof(void*));
FreeObj* prev = nullptr;
for (usize i = 0; i < cache->objsPerSlab; i++) {
FreeObj* obj = (FreeObj*)(base + i * cache->objSize);
obj->next = prev;
prev = obj;
}
slab->freeList = prev; /* built back-to-front; order does not matter */
cache->slabs = slab;
cache->slabCount++;
return slab;
}
bool kmemCacheInit(KmemCache* cache, const char* name, usize objSize)
{
cache->name = name;
cache->slabs = nullptr;
cache->slabCount = 0;
cache->usedObjs = 0;
usize size = objSize < sizeof(FreeObj) ? sizeof(FreeObj) : objSize;
size = alignUp(size, sizeof(void*));
cache->objSize = size;
usize headerSize = alignUp(sizeof(Slab), sizeof(void*));
if (headerSize + size > frameSize) {
cache->objsPerSlab = 0;
return false;
}
cache->objsPerSlab = (frameSize - headerSize) / size;
return true;
}
void kmemCacheDestroy(KmemCache* cache)
{
Slab* slab = cache->slabs;
while (slab != nullptr) {
Slab* next = slab->next;
pmmFree((uint64)slab);
slab = next;
}
cache->name = nullptr;
cache->objSize = 0;
cache->objsPerSlab = 0;
cache->slabs = nullptr;
cache->slabCount = 0;
cache->usedObjs = 0;
}
void* kmemCacheAlloc(KmemCache* cache)
{
if (cache->objsPerSlab == 0)
return nullptr;
Slab* slab = cache->slabs;
while (slab != nullptr && slab->freeList == nullptr)
slab = slab->next;
if (slab == nullptr) {
slab = growSlab(cache);
if (slab == nullptr)
return nullptr;
}
FreeObj* obj = slab->freeList;
slab->freeList = obj->next;
slab->freeCount--;
cache->usedObjs++;
return (void*)obj;
}
void kmemCacheFree(KmemCache* cache, void* obj)
{
Slab* slab = (Slab*)((uintptr)obj & ~(uintptr)(frameSize - 1));
FreeObj* fo = (FreeObj*)obj;
fo->next = slab->freeList;
slab->freeList = fo;
slab->freeCount++;
cache->usedObjs--;
}
usize kmemCacheUsed(const KmemCache* cache)
{
return cache->usedObjs;
}
usize kmemCacheCapacity(const KmemCache* cache)
{
return cache->slabCount * cache->objsPerSlab;
}
usize kmemCacheSlabCount(const KmemCache* cache)
{
return cache->slabCount;
}
usize kmemCacheObjsPerSlab(const KmemCache* cache)
{
return cache->objsPerSlab;
}
+154
View File
@@ -0,0 +1,154 @@
#include "ktest.h"
#include "slab.h"
#include "pmm.h"
#include "string.h"
#include "heap.h"
KTEST(slabInitRejectsOversizedObject)
{
KmemCache cache;
KT_FALSE(t, kmemCacheInit(&cache, "toobig", frameSize));
KT_EQ(t, kmemCacheCapacity(&cache), 0);
KT_NULL(t, kmemCacheAlloc(&cache));
}
KTEST(slabAllocDistinctNonOverlapping)
{
KmemCache cache;
KT_TRUE(t, kmemCacheInit(&cache, "small32", 32));
KT_GT(t, kmemCacheObjsPerSlab(&cache), 0);
KT_EQ(t, kmemCacheCapacity(&cache), 0); /* nothing grown yet */
void* a = kmemCacheAlloc(&cache);
void* b = kmemCacheAlloc(&cache);
void* c = kmemCacheAlloc(&cache);
KT_NOTNULL(t, a);
KT_NOTNULL(t, b);
KT_NOTNULL(t, c);
KT_NE(t, a, b);
KT_NE(t, b, c);
KT_NE(t, a, c);
KT_EQ(t, kmemCacheUsed(&cache), 3);
/* write a distinct pattern into each object and check none of them
stomped on a neighbor */
memset(a, 0xAA, 32);
memset(b, 0xBB, 32);
memset(c, 0xCC, 32);
uint8 probe[32];
memset(probe, 0xAA, 32);
KT_EQ(t, memcmp(a, probe, 32), 0);
memset(probe, 0xBB, 32);
KT_EQ(t, memcmp(b, probe, 32), 0);
memset(probe, 0xCC, 32);
KT_EQ(t, memcmp(c, probe, 32), 0);
kmemCacheFree(&cache, a);
kmemCacheFree(&cache, b);
kmemCacheFree(&cache, c);
KT_EQ(t, kmemCacheUsed(&cache), 0);
kmemCacheDestroy(&cache);
}
KTEST(slabFreeReusesLifo)
{
KmemCache cache;
kmemCacheInit(&cache, "reuse", 16);
void* a = kmemCacheAlloc(&cache);
kmemCacheFree(&cache, a);
void* b = kmemCacheAlloc(&cache);
KT_EQP(t, a, b); /* single-slot free list, must come back the same */
kmemCacheFree(&cache, b);
kmemCacheDestroy(&cache);
}
KTEST(slabGrowsAcrossMultipleSlabs)
{
KmemCache cache;
kmemCacheInit(&cache, "grow64", 64);
usize perSlab = kmemCacheObjsPerSlab(&cache);
KT_GT(t, perSlab, 0);
uint64 framesBefore = pmmFreeFrames();
/* fill exactly the first slab */
void* first = nullptr;
for (usize i = 0; i < perSlab; i++) {
void* p = kmemCacheAlloc(&cache);
KT_NOTNULL(t, p);
if (i == 0)
first = p;
}
KT_EQ(t, kmemCacheSlabCount(&cache), 1);
KT_EQ(t, kmemCacheUsed(&cache), perSlab);
/* one more forces a second slab (a second frame from pmm) */
void* overflow = kmemCacheAlloc(&cache);
KT_NOTNULL(t, overflow);
KT_EQ(t, kmemCacheSlabCount(&cache), 2);
KT_EQ(t, kmemCacheCapacity(&cache), perSlab * 2);
KT_EQ(t, pmmFreeFrames(), framesBefore - 2);
KT_NE(t, overflow, first);
kmemCacheDestroy(&cache);
/* destroying the cache must return every frame it ever grew into */
KT_EQ(t, pmmFreeFrames(), framesBefore);
}
#define slabStressCount 800
KTEST(slabStressAllocFreePattern)
{
KmemCache cache;
kmemCacheInit(&cache, "stress", 40);
void** slots = (void**)kmalloc(sizeof(void*) * slabStressCount);
KT_NOTNULL(t, slots);
if (!slots) {
kmemCacheDestroy(&cache);
return;
}
for (int i = 0; i < slabStressCount; i++) {
slots[i] = kmemCacheAlloc(&cache);
KT_NOTNULL(t, slots[i]);
if (slots[i])
*(int32*)slots[i] = i;
}
KT_EQ(t, kmemCacheUsed(&cache), slabStressCount);
/* every slot still holds the value we stamped, none aliased */
for (int i = 0; i < slabStressCount; i++) {
if (slots[i])
KT_EQ(t, *(int32*)slots[i], i);
}
/* free every third object, then re-allocate that many and confirm the
cache handed back exactly that many reclaimed slots (no capacity
growth needed to satisfy them) */
usize freedCount = 0;
for (int i = 0; i < slabStressCount; i += 3) {
kmemCacheFree(&cache, slots[i]);
slots[i] = nullptr;
freedCount++;
}
KT_EQ(t, kmemCacheUsed(&cache), slabStressCount - freedCount);
usize capacityBeforeRefill = kmemCacheCapacity(&cache);
for (int i = 0; i < slabStressCount; i += 3) {
slots[i] = kmemCacheAlloc(&cache);
KT_NOTNULL(t, slots[i]);
}
KT_EQ(t, kmemCacheCapacity(&cache), capacityBeforeRefill);
KT_EQ(t, kmemCacheUsed(&cache), slabStressCount);
for (int i = 0; i < slabStressCount; i++)
kmemCacheFree(&cache, slots[i]);
KT_EQ(t, kmemCacheUsed(&cache), 0);
kfree(slots);
kmemCacheDestroy(&cache);
}
+40
View File
@@ -0,0 +1,40 @@
#include "spinlock.h"
#include "ports.h"
void spinLockInit(SpinLock* lock)
{
lock->locked = 0;
}
uint64 spinLockAcquire(SpinLock* lock)
{
uint64 flags = saveAndDisableInterrupts();
while (__atomic_exchange_n(&lock->locked, 1, __ATOMIC_ACQUIRE) != 0) {
/* single-core: a lock held across this spin can only be released by
an interrupt handler, but interrupts are off -- callers must never
acquire a lock they (or a handler nested under them) already hold */
}
return flags;
}
bool spinLockTryAcquire(SpinLock* lock, uint64* savedFlags)
{
uint64 flags = saveAndDisableInterrupts();
if (__atomic_exchange_n(&lock->locked, 1, __ATOMIC_ACQUIRE) != 0) {
restoreInterrupts(flags);
return false;
}
*savedFlags = flags;
return true;
}
void spinLockRelease(SpinLock* lock, uint64 flags)
{
__atomic_store_n(&lock->locked, 0, __ATOMIC_RELEASE);
restoreInterrupts(flags);
}
bool spinLockIsLocked(const SpinLock* lock)
{
return __atomic_load_n(&lock->locked, __ATOMIC_ACQUIRE) != 0;
}
+73
View File
@@ -0,0 +1,73 @@
#include "ktest.h"
#include "spinlock.h"
KTEST(spinLockInitStartsUnlocked)
{
SpinLock lock;
spinLockInit(&lock);
KT_FALSE(t, spinLockIsLocked(&lock));
}
KTEST(spinLockAcquireReleaseCycle)
{
SpinLock lock;
spinLockInit(&lock);
uint64 flags = spinLockAcquire(&lock);
KT_TRUE(t, spinLockIsLocked(&lock));
spinLockRelease(&lock, flags);
KT_FALSE(t, spinLockIsLocked(&lock));
/* lock is reusable after release */
flags = spinLockAcquire(&lock);
KT_TRUE(t, spinLockIsLocked(&lock));
spinLockRelease(&lock, flags);
KT_FALSE(t, spinLockIsLocked(&lock));
}
KTEST(spinLockTryAcquireFailsWhenHeld)
{
SpinLock lock;
spinLockInit(&lock);
uint64 flags;
KT_TRUE(t, spinLockTryAcquire(&lock, &flags));
KT_TRUE(t, spinLockIsLocked(&lock));
/* second attempt must not block (and must not touch flags/state) since
nothing else on a single core will ever release it for us */
uint64 otherFlags = 0xDEADBEEF;
KT_FALSE(t, spinLockTryAcquire(&lock, &otherFlags));
KT_EQU(t, otherFlags, 0xDEADBEEF);
KT_TRUE(t, spinLockIsLocked(&lock));
spinLockRelease(&lock, flags);
KT_FALSE(t, spinLockIsLocked(&lock));
/* now that it is free, tryAcquire succeeds again */
KT_TRUE(t, spinLockTryAcquire(&lock, &flags));
spinLockRelease(&lock, flags);
}
KTEST(spinLockIndependentLocksDoNotInterfere)
{
SpinLock a, b;
spinLockInit(&a);
spinLockInit(&b);
uint64 flagsA = spinLockAcquire(&a);
KT_TRUE(t, spinLockIsLocked(&a));
KT_FALSE(t, spinLockIsLocked(&b));
uint64 flagsB = spinLockAcquire(&b);
KT_TRUE(t, spinLockIsLocked(&a));
KT_TRUE(t, spinLockIsLocked(&b));
spinLockRelease(&a, flagsA);
KT_FALSE(t, spinLockIsLocked(&a));
KT_TRUE(t, spinLockIsLocked(&b));
spinLockRelease(&b, flagsB);
KT_FALSE(t, spinLockIsLocked(&b));
}
+846 -4
View File
@@ -5,10 +5,24 @@
#include "vfs.h" #include "vfs.h"
#include "fs.h" #include "fs.h"
#include "string.h" #include "string.h"
#include "elf.h"
#include "paging.h"
#include "signal.h"
#include "keyboard.h"
#include "pit.h"
#include "poll.h"
#include "rtc.h"
#include "mmap.h"
#define userSpaceMin 0x0000000040000000ull #define userSpaceMin 0x0000000040000000ull
#define userSpaceMax 0x0000800000000000ull #define userSpaceMax 0x0000800000000000ull
/* Matches kmain.c's userStackSize -- both sides map exactly one slot's
worth of stack for a freshly loaded binary, whether it was spawned at
boot (kmain.c's spawnUser, addresses picked by hand) or at runtime
(doSpawn/doExec below, addresses derived from elfSlotStackTop). */
#define execStackSize 0x4000
static bool userRangeOk(uint64 pointer, uint64 length) static bool userRangeOk(uint64 pointer, uint64 length)
{ {
if (pointer < userSpaceMin) if (pointer < userSpaceMin)
@@ -34,6 +48,45 @@ static bool copyUserPath(uint64 pointer, char* dst, uint64 max)
return true; return true;
} }
#define execArgMaxLen 256
/* Copies a NULL-terminated array of user-space `const char*` (the argv
parameter of sysExec/sysSpawn -- userExec/userSpawn, ulib.c) into kernel
memory: up to elfMaxArgs (elf.h) pointers are read out of userspace one
at a time (each individually range-checked, since the array itself is
just user memory a buggy or hostile caller controls) and each string
copied via copyUserPath into `storage`, which the caller owns for as
long as it needs argvPtrs to stay valid. A null `argvPointer` (the
common "no arguments" case) is not an error -- it yields argc 0, letting
userExec(path, nullptr) mean the same as a bare execve(path) with an
otherwise-empty argv. Fails (leaving *outArgc untouched) on a bad
pointer, a string that doesn't fit `execArgMaxLen`, or an argv longer
than elfMaxArgs entries that never hits its NULL terminator. */
static bool copyUserArgv(uint64 argvPointer, char storage[elfMaxArgs][execArgMaxLen],
const char* argvPtrs[elfMaxArgs], int* outArgc)
{
if (argvPointer == 0) {
*outArgc = 0;
return true;
}
if (!userRangeOk(argvPointer, sizeof(uint64) * (elfMaxArgs + 1)))
return false;
const uint64* userArgv = (const uint64*)argvPointer;
int argc = 0;
while (argc < elfMaxArgs && userArgv[argc] != 0) {
if (!copyUserPath(userArgv[argc], storage[argc], execArgMaxLen))
return false;
argvPtrs[argc] = storage[argc];
argc++;
}
if (argc == elfMaxArgs && userArgv[argc] != 0)
return false;
*outArgc = argc;
return true;
}
static int64 doWrite(uint64 fd, uint64 buffer, uint64 length) static int64 doWrite(uint64 fd, uint64 buffer, uint64 length)
{ {
if (!userRangeOk(buffer, length)) if (!userRangeOk(buffer, length))
@@ -48,15 +101,222 @@ static int64 doRead(uint64 fd, uint64 buffer, uint64 length)
return vfsRead((int)fd, (void*)buffer, length); return vfsRead((int)fd, (void*)buffer, length);
} }
typedef struct LoadedImage {
uint64 entry;
uint64 stackTop;
uint64 brkStart;
} LoadedImage;
/* Shared by doExec and doSpawn: resolves `path` through the VFS, loads its
LOAD segments via elfLoad (which maps them at whatever fixed virtual
address they were linked at -- see elf.c), then derives and maps a fresh
stack for that slot from nothing but the ELF's own entry point (see
elfSlotStackTop/elf.h). Leaves the caller's thread state untouched;
callers decide what to do with a successfully loaded image. */
static bool loadImageByPath(const char* path, LoadedImage* out)
{
VfsNode* node = vfsLookup(path);
if (!node)
return false;
/* Real EACCES-shaped refusal: a file without the right exec bit for the
CALLING thread's identity (vfsPermitted, owner-vs-other-vs-root) can
never be exec'd or spawned, exactly like real Unix's own execve()
check -- jobctl.c's own jobctlSpawn (the shell's separate, parallel
loading path) enforces the identical check the identical way. */
if (!vfsPermitted(vfsMode(node), vfsUid(node), vfsGid(node), currentThreadUid(), currentThreadGid(), fsModeOwnerExec))
return false;
const void* image = vfsFileData(node);
uint64 size = vfsSize(node);
uint64 entry = elfLoad(image, size);
if (entry == 0)
return false;
out->entry = entry;
out->stackTop = elfSlotStackTop(entry);
out->brkStart = elfSlotBrkStart(entry);
return pagingMapUserRangeFlags(out->stackTop - execStackSize, execStackSize, pageNoExecute);
}
/* execve()-lite: replaces the calling thread's own program image with a
different one, in place -- same pid, same open file descriptors (POSIX's
default, no O_CLOEXEC support to opt out of here), new code/stack/brk.
Mutates the live interrupt frame's rip/rsp directly (the same trick
threadCreateUser uses to bootstrap a brand new thread's very first
frame): on success this syscall does not "return" to the caller in any
meaningful sense, since regs->rip now points at the new image's entry
point instead of back into the old one's code -- exactly real exec()
semantics. General-purpose registers are zeroed so the new program never
observes leftover state from a syscall it never made. Failure (bad path,
not a valid ELF, thread table/frame mapping issue) leaves regs untouched
apart from the -1 return value: the caller keeps running its old image,
also matching real exec().
Also reclaims the OLD image's slot: frostbyte's flat single-address-space
design (see doSpawn below) means a program that exec()s into a binary
linked at a different fixed base -- the common case, since two distinct
binaries can never share a slot -- would otherwise leak its entire
elfSlotSize worth of physical frames every time, forever. Skipped only
when the new image happens to land in the SAME slot as the old one (a
binary exec()ing itself): unmapping first would free frames the new
image's own loadImageByPath call had already mapped moments earlier.
argv/argc are already kernel-side copies (copyUserArgv, called from the
sysExec dispatch case below, BEFORE this runs) -- critical ordering,
since they may point into the OLD image's own memory (a process's argv
commonly lives in its own .rodata/stack), which this function is about
to unmap out from under it. */
static bool doExec(const char* path, const char* const* argv, int argc, Registers* regs)
{
uint32 pid = currentThreadId();
uint64 oldSlotBase = threadUserSlotBase(pid);
LoadedImage img;
if (!loadImageByPath(path, &img))
return false;
uint64 initialRsp;
if (!elfBuildInitialStack(img.stackTop, execStackSize, argv, argc, &initialRsp))
return false;
uint64 newSlotBase = elfSlotBase(img.entry);
if (oldSlotBase != 0 && oldSlotBase != newSlotBase) {
uint64 stackProbe = oldSlotBase + elfSlotStackOffset - 1;
bool wasMapped = pagingResolve(stackProbe) != 0;
pagingUnmapUserRange(oldSlotBase, elfSlotSize);
bool nowFreed = pagingResolve(stackProbe) == 0;
kprintf("[exec] pid=%u freed old slot %p (was-mapped=%s now-freed=%s) -> %s\n",
pid, (void*)oldSlotBase, wasMapped ? "yes" : "no", nowFreed ? "yes" : "no",
(wasMapped && nowFreed) ? "exec-old-slot-freed-ok" : "exec-old-slot-freed-FAIL");
} else if (oldSlotBase != 0) {
/* Self-exec (or the pid==0/no-prior-slot case, unreachable for a
real ring-3 caller): the branch above is skipped precisely to
avoid unmapping the NEW image's own LOAD segments/stack, which
already landed on these same pages -- but any mmap mapping from
the OLD image is a disjoint sub-range (mmap.h's own mmapReserve
region, [slot+elfSlotStackOffset, slot+elfSlotSize)) that
loadImageByPath/elfBuildInitialStack never touch, so it still
needs reclaiming here explicitly or its frames would leak
forever every time a binary exec()s itself. */
pagingUnmapUserRange(oldSlotBase + elfSlotStackOffset, elfSlotSize - elfSlotStackOffset);
}
if (oldSlotBase != 0)
mmapReleaseAll(threadAt(pid));
threadSetUserSlotBase(pid, newSlotBase);
threadSetName(pid, path);
threadSetBrk(pid, img.brkStart);
regs->rip = img.entry;
regs->rsp = initialRsp;
regs->rbp = 0;
regs->rdi = 0;
regs->rsi = 0;
regs->rdx = 0;
regs->rcx = 0;
regs->rbx = 0;
regs->rax = 0;
regs->r8 = 0;
regs->r9 = 0;
regs->r10 = 0;
regs->r11 = 0;
regs->r12 = 0;
regs->r13 = 0;
regs->r14 = 0;
regs->r15 = 0;
return true;
}
/* posix_spawn()-lite: loads `path` into a brand new thread/process rather
than replacing the caller, and returns its pid immediately without
waiting -- the caller joins it later with sysWait (threadJoin). This is
deliberately not a literal fork(): frostbyte has one shared address space
system-wide (no per-process page tables/CR3, see paging.c), so there is
no address space to duplicate, only a fresh slot to load a binary into.
As with any binary in this kernel, spawning two live instances of the
same path (or of two paths whose fixed link addresses happen to share a
slot) at once would map both onto the same pages and corrupt each other;
this is an accepted limitation of the flat single-address-space design,
not something doSpawn can detect from a path string alone. */
static int64 doSpawn(const char* path, const char* const* argv, int argc)
{
LoadedImage img;
if (!loadImageByPath(path, &img))
return -1;
uint64 initialRsp;
if (!elfBuildInitialStack(img.stackTop, execStackSize, argv, argc, &initialRsp))
return -1;
uint32 pid = threadCreateUser(path, img.entry, initialRsp);
if (pid == threadInvalidId)
return -1;
threadSetBrk(pid, img.brkStart);
return (int64)pid;
}
uint64 syscallDispatch(Registers* regs) uint64 syscallDispatch(Registers* regs)
{ {
/* This thread is not (yet, or any more) parked mid a retryable
syscall's own block-and-retry pattern the instant ANY dispatch for
it begins -- whether this dispatch turns out to BE that exact retry
re-executing (sysRead/sysWrite/sysWait's own cases set it true again
below if they block once more) or a completely unrelated syscall.
See sched.h's Thread.sigInRetryableSyscall and signal.c's
sigConsumePending for why this precise a window matters: without it,
a signal delivered while this thread is doing perfectly ordinary
work could be mistaken for one interrupting a slow syscall. */
currentThread()->sigInRetryableSyscall = false;
switch (regs->rax) { switch (regs->rax) {
case sysWrite: case sysWrite: {
regs->rax = (uint64)doWrite(regs->rdi, regs->rsi, regs->rdx); int64 result = doWrite(regs->rdi, regs->rsi, regs->rdx);
if (result == -1) {
/* A "would block" result on a fdFlagBlocking pipe end (see
vfs.h) is retried, not returned: park on the pipe's channel
and rewind rip by the 2-byte `int $0x80` this trap came from
so the whole syscall re-executes once schedWakeChannel makes
this thread Runnable again (pipeWrite, called from doWrite
above, already does that whenever it frees or fills space) --
the exact restart-syscall pattern sysWait uses for
threadJoin, for the same reason: halt()ing here directly
would freeze the whole machine, not just this thread, since
this handler runs with interrupts disabled. A non-blocking
pipe end (vfsPipe, e.g. pipetest.c) or a permanently broken
one gets nullptr back and the -1 passes through untouched. */
const void* channel = vfsBlockingChannel((int)regs->rdi, true);
if (channel) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(channel);
regs->rip -= 2;
return schedule((uint64)regs);
}
}
regs->rax = (uint64)result;
return (uint64)regs; return (uint64)regs;
case sysRead: }
regs->rax = (uint64)doRead(regs->rdi, regs->rsi, regs->rdx); case sysRead: {
int64 result = doRead(regs->rdi, regs->rsi, regs->rdx);
if (result == -1) {
/* Covers a fdFlagBlocking pipe/fifo end (see sysWrite above) and
fd 0's console reader queue (keyboard.h) alike -- both report
"would block" as -1 from doRead, and vfsBlockingChannel picks
the right wait channel for whichever kind fd actually is. */
const void* channel = vfsBlockingChannel((int)regs->rdi, false);
if (channel) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(channel);
regs->rip -= 2;
return schedule((uint64)regs);
}
}
regs->rax = (uint64)result;
return (uint64)regs; return (uint64)regs;
}
case sysGetpid: case sysGetpid:
regs->rax = currentThreadId(); regs->rax = currentThreadId();
return (uint64)regs; return (uint64)regs;
@@ -114,6 +374,68 @@ uint64 syscallDispatch(Registers* regs)
regs->rax = (uint64)(int64)vfsMkdir(path); regs->rax = (uint64)(int64)vfsMkdir(path);
return (uint64)regs; return (uint64)regs;
} }
case sysChdir: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChdir(path);
return (uint64)regs;
}
case sysGetcwd:
if (!userRangeOk(regs->rdi, regs->rsi))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)vfsGetcwd((char*)regs->rdi, regs->rsi);
return (uint64)regs;
case sysGetEnv: {
char name[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name)) || !userRangeOk(regs->rsi, regs->rdx)) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
const char* value = currentThreadGetEnv(name);
if (!value) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
uint64 len = strlen(value);
if (len >= regs->rdx) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
memcpy((void*)regs->rsi, value, len + 1);
regs->rax = (uint64)len;
return (uint64)regs;
}
case sysSetEnv: {
char name[envEntryMax];
char value[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name)) ||
!copyUserPath(regs->rsi, value, sizeof(value))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
regs->rax = currentThreadSetEnv(name, value) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysUnsetEnv: {
/* Real unsetenv(3): a no-op success whether or not `name` actually
had an entry -- ONLY a bad user pointer/path is a real failure
here, unlike sysSetEnv above where "table full"/"too long" are
genuine failures. currentThreadUnsetEnv's own bool return (which
DOES distinguish "removed" from "was not there", for a kernel-
side caller like shell.c that might want to) is deliberately
discarded at this syscall boundary. */
char name[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
currentThreadUnsetEnv(name);
regs->rax = 0;
return (uint64)regs;
}
case sysUnlink: { case sysUnlink: {
char path[256]; char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path))) if (!copyUserPath(regs->rdi, path, sizeof(path)))
@@ -122,6 +444,526 @@ uint64 syscallDispatch(Registers* regs)
regs->rax = (uint64)(int64)vfsUnlink(path); regs->rax = (uint64)(int64)vfsUnlink(path);
return (uint64)regs; return (uint64)regs;
} }
case sysRmdir: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsRmdir(path);
return (uint64)regs;
}
case sysChmod: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChmod(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysChown: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChown(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysGetuid:
/* Real getuid(2): no arguments, always succeeds, straight through
to the calling thread's own credential. */
regs->rax = (uint64)currentThreadUid();
return (uint64)regs;
case sysSetuid:
/* Real setuid(2): currentThreadSetUid (sched.h) itself enforces the
actual privilege rule (uid 0 may become anyone; anyone else may
only pass their own current uid back in) -- this dispatch case
does no additional validation of its own, matching sysChdir's own
"the underlying function already checks everything" split. */
regs->rax = currentThreadSetUid((uint32)regs->rdi) ? 0 : (uint64)-1;
return (uint64)regs;
case sysGetgid:
/* Real getgid(2): same shape as sysGetuid above. */
regs->rax = (uint64)currentThreadGid();
return (uint64)regs;
case sysSetgid:
/* Real setgid(2): currentThreadSetGid (sched.h) itself enforces the
actual privilege rule (gated by this thread's own UID, not GID --
see that function's own doc comment). */
regs->rax = currentThreadSetGid((uint32)regs->rdi) ? 0 : (uint64)-1;
return (uint64)regs;
case sysChgrp: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChgrp(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysPidfdOpen:
/* vfsPidfdOpen (vfs.c) already does every validity check pidfd.h's
own pidfdOpen needs (id 0/out-of-range/already-unused target) and
the fd-table installation vfsPipe/vfsPipeAttach's own dispatch
cases above delegate the same way -- nothing left for this
handler to add. */
regs->rax = (uint64)vfsPidfdOpen((uint32)regs->rdi);
return (uint64)regs;
case sysPidfdSend: {
/* vfsPidfdTarget folds "is this fd even a live pidfd" and "has the
original target since been reaped/recycled" (pidfd.h's own
generation check) into one call -- a stale handle is rejected
right here, before sigRaiseInfo ever sees a pid, which is
exactly the ABA-safety pidfd.h exists to provide over a raw
userSigSend(pid, signum) on a cached pid (see pidfd.h's own file
comment). Otherwise identical to sysSigSend's own single-target
path just above (same siCodeUser/senderPid convention, same
sigKill/sigStop bypasses already living inside sigRaiseInfo). */
uint32 target = vfsPidfdTarget((int)regs->rdi);
if (target == threadInvalidId) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
int signum = (int)regs->rsi;
bool ok = sigRaiseInfo(target, signum, siCodeUser, (int)currentThreadId(), 0);
regs->rax = ok ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysPidfdWait: {
/* Same retry-the-syscall park pattern as sysWait's own specific-pid
path below, just resolving the target through a pidfd instead of
a raw pid -- re-resolved on EVERY retry (not just the first
check), so a target that gets reaped by someone else while this
thread is blocked is caught the instant this thread wakes back
up, rather than only at the moment sysPidfdWait was first called.
threadJoin(target) below only ever reaps a thread this exact
check just proved is threadDead AND still the same incarnation
this handle was opened on, so there is no risk of it silently
reaping (or reporting the exit code of) an unrelated thread that
happens to have landed on the same numeric slot since. */
uint32 target = vfsPidfdTarget((int)regs->rdi);
if (target == threadInvalidId) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
Thread* targetThread = threadAt(target);
if (targetThread->state != threadDead) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(targetThread);
regs->rip -= 2;
return schedule((uint64)regs);
}
regs->rax = (uint64)threadJoin(target);
return (uint64)regs;
}
case sysSymlink: {
char target[256];
char path[256];
if (!copyUserPath(regs->rdi, target, sizeof(target)) ||
!copyUserPath(regs->rsi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsSymlink(path, target);
return (uint64)regs;
}
case sysReadlink: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)) || !userRangeOk(regs->rsi, regs->rdx))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)vfsReadlink(path, (char*)regs->rsi, regs->rdx);
return (uint64)regs;
}
case sysLink: {
char oldPath[256];
char newPath[256];
if (!copyUserPath(regs->rdi, oldPath, sizeof(oldPath)) ||
!copyUserPath(regs->rsi, newPath, sizeof(newPath)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsLink(oldPath, newPath);
return (uint64)regs;
}
case sysPipe: {
if (!userRangeOk(regs->rdi, 2 * sizeof(int))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
int* userFds = (int*)regs->rdi;
int readFd, writeFd;
if (vfsPipe(&readFd, &writeFd) < 0) {
regs->rax = (uint64)-1;
} else {
userFds[0] = readFd;
userFds[1] = writeFd;
regs->rax = 0;
}
return (uint64)regs;
}
case sysPipeCreate:
regs->rax = (uint64)vfsPipeCreateHandle();
return (uint64)regs;
case sysPipeAttach:
regs->rax = (uint64)vfsPipeAttach((int)regs->rdi, regs->rsi != 0);
return (uint64)regs;
case sysKill: {
/* Real kill(2)'s own pid<0/pid==0 convention (userKill's `pid` is
already a signed `long`, see ulib.h -- only this kernel-side
interpretation of the raw bit pattern was missing): pid>0 is the
single-target path this always had; pid==0 targets the caller's
OWN process group (threadKillGroup, sched.c -- naturally kills
no one else if this is the group's only OTHER member, since
threadKill itself already excludes the caller's own Running
thread); pid<0 targets group -pid, exactly like real
kill(-pgid, SIGKILL). */
int64 rawPid = (int64)regs->rdi;
bool ok;
if (rawPid > 0)
ok = threadKill((uint32)rawPid);
else
ok = threadKillGroup(rawPid == 0 ? currentThread()->pgid : (uint32)(-rawPid));
regs->rax = ok ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysSigSend: {
/* Same pid>0/pid==0/pid<0 convention as sysKill just above, this
time through sigRaiseGroup (signal.h) for the two group cases --
see that function's own doc comment for why every per-target
special case (SIGKILL/SIGSTOP/SIGCONT bypasses, early-wake,
SigInfo) still applies exactly as it already does for a plain,
single-target send. */
int64 rawPid = (int64)regs->rdi;
int signum = (int)regs->rsi;
uint32 senderPid = currentThreadId();
bool isGroup = (rawPid <= 0);
uint32 targetPgid = isGroup ? (rawPid == 0 ? currentThread()->pgid : (uint32)(-rawPid)) : 0;
/* siCodeUser/senderPid: a real userSigSend always has a genuine
sending process, unlike sched.c's own kernel-internal raises
(SIGALRM/SIGCHLD) or keyboard.c's terminal-generated ones --
exactly real Unix's own SI_USER, si_pid the caller's own pid,
exactly like real kill(2). */
bool ok = isGroup
? sigRaiseGroup(targetPgid, signum, siCodeUser, (int)senderPid)
: sigRaiseInfo((uint32)rawPid, signum, siCodeUser, (int)senderPid, 0);
regs->rax = ok ? 0 : (uint64)-1;
/* Every OTHER outcome of sigRaise/sigRaiseGroup either changes
nothing about the CALLING thread's own state (a signal aimed at
someone else) or, for an ordinary catchable signal aimed at
self, is already caught by isrDispatch's unconditional
deliverPendingSignals call right after this handler returns
(see isr.c) -- self-signaling sigUsr1 and having its handler
fire before ever reaching userspace again already works today
without any special case here. SIGSTOP against the caller's own
pid (or a group that includes it) is the one exception:
threadStop (called from sigRaise above, once per group member
for the group case) already flipped this very thread's state
away from Running, but sigRaise/sysSigSend never touch
sigPending for it (see signal.h), so deliverPendingSignals'
pending-bit scan would find nothing and simply hand `regs`
straight back -- returning to ring 3 and continuing to run a
thread the scheduler no longer considers Runnable, until the
next timer tick eventually notices. Explicit reschedule here
closes that gap, the same "flip state, then let schedule() do
the actual waiting" pattern sysSleep/sysExit already use for
their own self-directed state changes. The group case checks
targetPgid against the caller's OWN pgid rather than an exact id
match, since the caller itself -- not just some other member --
is who this reschedule is actually about. */
bool selfTargeted = isGroup ? (targetPgid == currentThread()->pgid) : ((uint32)rawPid == senderPid);
if (ok && signum == sigStop && selfTargeted)
return schedule((uint64)regs);
return (uint64)regs;
}
case sysSigAction:
regs->rax = sigSetHandler(currentThread(), (int)regs->rdi, regs->rsi, regs->rdx, regs->r10);
return (uint64)regs;
case sysSigProcMask:
regs->rax = sigProcMask(currentThread(), (int)regs->rdi, regs->rsi);
return (uint64)regs;
case sysSigReturn:
sigOnSigReturn(currentThread(), regs);
return (uint64)regs;
case sysAlarm:
regs->rax = threadSetAlarm(regs->rdi);
return (uint64)regs;
case sysSetpgid: {
/* Real setpgid(2)'s own "0 means self"/"0 means use pid itself as
the new group id" resolution -- threadSetPgid (sched.h) takes an
already-fully-resolved value, exactly like threadSetBrk/
threadSetAlarm above do for THEIR raw syscall arguments. */
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 pgid = (regs->rsi == 0) ? pid : (uint32)regs->rsi;
regs->rax = threadSetPgid(pid, pgid) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysGetpgid: {
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 pgid = threadPgid(pid);
regs->rax = (pgid == threadInvalidId) ? (uint64)-1 : (uint64)pgid;
return (uint64)regs;
}
case sysNice: {
/* Real setpriority(PRIO_PROCESS, ...)'s own "0 means the caller
itself" convention, same resolution shape as sysSetpgid's `pid`
argument just above -- rdi/pid, rsi/weight, straight through to
threadSetPriority (sched.h), which now does the SAME id==0/
out-of-range/Dead-or-Unused rejection threadSetPgid already did
(see that function's own updated doc comment for why it had to
catch up once this syscall made it reachable from ring 3). */
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 weight = (uint32)regs->rsi;
regs->rax = threadSetPriority(pid, weight) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysGetPriority: {
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 weight = threadPriority(pid);
regs->rax = (weight == 0) ? (uint64)-1 : (uint64)weight;
return (uint64)regs;
}
case sysTime:
/* Real time(2)'s job: seconds since the Unix epoch, off the real
CMOS RTC (rtc.c) -- the first syscall in this kernel answering
"what day is it" instead of "how long since boot" (sysSleep,
userAlarm). No arguments, nothing to validate. */
regs->rax = (uint64)rtcNow();
return (uint64)regs;
case sysSetFgPgrp:
/* Real tcsetpgrp(3)'s job -- names which process group keyboard.c's
real Ctrl-C/Ctrl-Z (SIGINT/SIGTSTP) is scoped to next. No
permission check at all (this kernel has no notion of "the
controlling terminal of MY session" to check ownership against,
the same honest, documented omission threadSetPgid's own doc
comment already gives for setpgid's session check) -- any
process can claim the foreground for any pgid, including one it
is not even a member of. */
keyboardSetForegroundPgid((uint32)regs->rdi);
regs->rax = 0;
return (uint64)regs;
case sysGetFgPgrp:
regs->rax = (uint64)keyboardForegroundPgid();
return (uint64)regs;
case sysPoll: {
/* Real poll(2): rdi/PollFd array, rsi/nfds, rdx/timeoutMs (signed,
-1 = wait forever, 0 = report current readiness without ever
blocking, like every real poll(2) implementation). Flat address
space (this kernel's honest fork()-less design, see doSpawn's own
comment) means the array can be read and written in place, the
same way sysStat writes straight into its caller's FbStat*
without a separate copy-out step. */
uint64 nfds = regs->rsi;
int64 timeoutMs = (int64)regs->rdx;
if (nfds > maxPollFds || !userRangeOk(regs->rdi, nfds * sizeof(PollFd))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
PollFd* userFds = (PollFd*)regs->rdi;
Thread* self = currentThread();
/* First entry into this call (not a retry re-executing after a
park below) picks the deadline once and remembers it in
pollDeadline -- a later retry must NOT recompute it from the
still-unchanged rdx, or a poll that keeps almost-but-not-quite
finding a ready fd could have its effective timeout pushed out
indefinitely instead of counting down real elapsed time. */
if (!self->pollActive && timeoutMs != 0) {
self->pollActive = true;
if (timeoutMs < 0) {
self->pollDeadline = 0;
} else {
uint64 ticks = ((uint64)timeoutMs * (uint64)pitFrequency()) / 1000;
if (ticks == 0)
ticks = 1;
self->pollDeadline = pitTicks() + ticks;
}
}
int64 ready = 0;
for (uint64 i = 0; i < nfds; i++) {
int16 revents = 0;
if (vfsPollReady(userFds[i].fd, userFds[i].events, &revents))
ready++;
userFds[i].revents = revents;
}
bool expired = self->pollActive && self->pollDeadline != 0 &&
pitTicks() >= self->pollDeadline;
if (ready > 0 || timeoutMs == 0 || expired) {
self->pollActive = false;
self->pollDeadline = 0;
regs->rax = (uint64)ready;
return (uint64)regs;
}
/* None ready yet, timeout not yet reached: park on the one shared
channel every poller blocks on (schedPollChannel, sched.h) and
rewind rip so the whole syscall re-executes -- the same
restart-the-syscall trick sysRead/sysWrite/sysWait already use,
except schedule()'s own expiry scan (sched.c) is what guarantees
this retry eventually happens even if no fd ever becomes ready. */
self->sigInRetryableSyscall = true;
schedBlockCurrent(schedPollChannel());
regs->rip -= 2;
return schedule((uint64)regs);
}
case sysMkfifo: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsMkfifo(path);
return (uint64)regs;
}
case sysExec: {
char path[256];
char argvStorage[elfMaxArgs][execArgMaxLen];
const char* argvPtrs[elfMaxArgs];
int argc;
if (!copyUserPath(regs->rdi, path, sizeof(path)) ||
!copyUserArgv(regs->rsi, argvStorage, argvPtrs, &argc) ||
!doExec(path, argvPtrs, argc, regs))
regs->rax = (uint64)-1;
return (uint64)regs;
}
case sysSpawn: {
char path[256];
char argvStorage[elfMaxArgs][execArgMaxLen];
const char* argvPtrs[elfMaxArgs];
int argc;
if (!copyUserPath(regs->rdi, path, sizeof(path)) ||
!copyUserArgv(regs->rsi, argvStorage, argvPtrs, &argc))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)doSpawn(path, argvPtrs, argc);
return (uint64)regs;
}
case sysWait: {
uint32 id = (uint32)regs->rdi;
if (id == waitAnyPid) {
/* waitpid(-1)-style "wait for ANY child" -- see
threadJoinAnyChild's own doc comment (sched.h) for the full
contract. regs->rsi is an optional user `long*` pointer
(userWaitAny's outPid, ulib.c) that receives which pid
actually got reaped; nullptr means the caller does not care.
Written as a full 8-byte store below to match `long`'s real
width, not truncated to sizeof(uint32) -- a narrower store
would leave the user variable's upper 4 bytes as whatever
uninitialized garbage the caller's stack already had. */
if (regs->rsi != 0 && !userRangeOk(regs->rsi, sizeof(uint64))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
uint32 reapedId;
int64 code;
WaitScanResult scan = threadJoinAnyChild(currentThreadId(), &reapedId, &code);
if (scan == waitScanNoChildren) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
if (scan == waitScanNoneDead) {
/* Same retry-syscall dance as the specific-pid path below,
just parked on our OWN slot instead of a named child's --
threadExit/threadKill wake this channel for EVERY dying
child of ours, not just one in particular (see
threadJoinAnyChild's doc comment), so re-scanning from
scratch on each retry is what actually picks up whichever
one died. */
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(currentThread());
regs->rip -= 2;
return schedule((uint64)regs);
}
if (regs->rsi != 0)
*(uint64*)regs->rsi = (uint64)reapedId;
regs->rax = (uint64)code;
return (uint64)regs;
}
if (id == 0 || id >= threadCapacity()) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
Thread* target = threadAt(id);
if (target->state == threadUnused) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
if (target->state != threadDead) {
/* Not dead yet: parking here and calling halt() the way
threadJoin (sched.c) does for a plain kernel thread body
would hang the whole machine, not just this thread -- this
handler runs with interrupts disabled (int 0x80 is an
interrupt gate, idt.c) on the single shared bootstrap kernel
stack (isrStubs.S/tss.c), not the calling thread's own
persistent one, so unlike kmain.c's joinerThread it cannot
literally halt in place and pick its own C call frame back
up later: hlt with IF=0 never wakes, since nothing can ever
deliver the timer interrupt that would run schedule() again.
Instead: block on the same channel threadExit wakes
(schedWakeChannel(&threads[id])), rewind rip by the 2 bytes
of the `int $0x80` opcode this trap just came from so it
re-executes once this thread is scheduled again, and hand
the CPU to schedule() -- the same "flip state, then let the
interrupt-return path do the actual waiting" technique
sysSleep already uses. The retry re-enters this exact case
with the same rax/rdi; since threadExit will have already
flipped the target to Dead before waking this thread back
up, it takes the branch below next time and completes
without blocking again. */
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(target);
regs->rip -= 2;
return schedule((uint64)regs);
}
regs->rax = (uint64)threadJoin(id);
return (uint64)regs;
}
case sysMmap: {
/* addrHint==0 means "kernel picks" (mmapReserve's own first-fit,
real mmap(NULL, ...) shape); a nonzero addrHint is taken as an
exact placement request, refused (not silently relocated) if it
does not fit -- there is no MAP_FIXED bit to distinguish the two,
every addrHint here behaves like real mmap's own weaker "hint"
contract already narrowed down to "exact or refused". fd/offset
are read (and validated below) even when MAP_ANONYMOUS is set,
harmlessly ignored by mmapReserve itself in that case, exactly
like real mmap(2)'s own documented behavior. */
uint64 addrHint = regs->rdi;
uint64 length = regs->rsi;
uint32 prot = (uint32)regs->rdx;
uint32 flags = (uint32)regs->r10;
int fd = (int)regs->r8;
uint64 offset = regs->r9;
bool anonymous = (flags & mmapAnonymous) != 0;
if (!anonymous) {
/* Same three seekable, real-data fd kinds vfsLseek itself
already restricts to (vfs.c) -- a pipe/fifo/console/pidfd fd
has no fixed byte range a page fault could ever re-read
deterministically, so mmap refuses them here rather than
letting mmapFaultHandler discover that failure lazily, one
page at a time, later. */
if (fd < 0 || fd >= maxOpenFiles) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
FileDescriptor* fdEntry = &currentThread()->fds[fd];
if (fdEntry->kind != fdFile && fdEntry->kind != fdSfs && fdEntry->kind != fdBlockDev) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
}
uint64 mappedAddr;
if (!mmapReserve(currentThread(), addrHint, length, prot, anonymous, fd, offset, &mappedAddr)) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
regs->rax = mappedAddr;
return (uint64)regs;
}
case sysMunmap:
regs->rax = mmapRelease(currentThread(), regs->rdi, regs->rsi) ? 0 : (uint64)-1;
return (uint64)regs;
default: default:
regs->rax = (uint64)-1; regs->rax = (uint64)-1;
return (uint64)regs; return (uint64)regs;
+116
View File
@@ -0,0 +1,116 @@
#include "vec.h"
#include "heap.h"
#include "string.h"
void vecInit(Vec* v, usize elemSize)
{
v->data = nullptr;
v->elemSize = elemSize;
v->length = 0;
v->capacity = 0;
}
void vecFree(Vec* v)
{
kfree(v->data);
v->data = nullptr;
v->length = 0;
v->capacity = 0;
}
bool vecReserve(Vec* v, usize newCapacity)
{
if (newCapacity <= v->capacity)
return true;
usize grown = v->capacity == 0 ? 4 : v->capacity * 2;
if (grown < newCapacity)
grown = newCapacity;
uint8* bigger = (uint8*)krealloc(v->data, grown * v->elemSize);
if (!bigger)
return false;
v->data = bigger;
v->capacity = grown;
return true;
}
bool vecPush(Vec* v, const void* elem)
{
if (!vecReserve(v, v->length + 1))
return false;
memcpy(v->data + v->length * v->elemSize, elem, v->elemSize);
v->length++;
return true;
}
bool vecPop(Vec* v, void* outElem)
{
if (v->length == 0)
return false;
v->length--;
if (outElem)
memcpy(outElem, v->data + v->length * v->elemSize, v->elemSize);
return true;
}
void* vecAt(Vec* v, usize index)
{
if (index >= v->length)
return nullptr;
return v->data + index * v->elemSize;
}
bool vecSet(Vec* v, usize index, const void* elem)
{
if (index >= v->length)
return false;
memcpy(v->data + index * v->elemSize, elem, v->elemSize);
return true;
}
bool vecInsert(Vec* v, usize index, const void* elem)
{
if (index > v->length)
return false;
if (!vecReserve(v, v->length + 1))
return false;
usize tailCount = v->length - index;
if (tailCount > 0) {
memmove(v->data + (index + 1) * v->elemSize,
v->data + index * v->elemSize,
tailCount * v->elemSize);
}
memcpy(v->data + index * v->elemSize, elem, v->elemSize);
v->length++;
return true;
}
bool vecRemove(Vec* v, usize index, void* outElem)
{
if (index >= v->length)
return false;
if (outElem)
memcpy(outElem, v->data + index * v->elemSize, v->elemSize);
usize tailCount = v->length - index - 1;
if (tailCount > 0) {
memmove(v->data + index * v->elemSize,
v->data + (index + 1) * v->elemSize,
tailCount * v->elemSize);
}
v->length--;
return true;
}
void vecClear(Vec* v)
{
v->length = 0;
}
usize vecLen(const Vec* v) { return v->length; }
usize vecCap(const Vec* v) { return v->capacity; }
bool vecIsEmpty(const Vec* v) { return v->length == 0; }
+116
View File
@@ -0,0 +1,116 @@
#include "ktest.h"
#include "vec.h"
KTEST(vecPushPopGrows)
{
Vec v;
vecInit(&v, sizeof(int));
KT_TRUE(t, vecIsEmpty(&v));
KT_EQ(t, vecCap(&v), 0);
for (int i = 0; i < 20; i++)
KT_TRUE(t, vecPush(&v, &i));
KT_EQ(t, vecLen(&v), 20);
KT_GE(t, vecCap(&v), 20);
for (int i = 0; i < 20; i++)
KT_EQ(t, *(int*)vecAt(&v, (usize)i), i);
int popped = -1;
KT_TRUE(t, vecPop(&v, &popped));
KT_EQ(t, popped, 19);
KT_EQ(t, vecLen(&v), 19);
vecFree(&v);
}
KTEST(vecPopEmptyFails)
{
Vec v;
vecInit(&v, sizeof(int));
int out;
KT_FALSE(t, vecPop(&v, &out));
KT_NULL(t, vecAt(&v, 0));
vecFree(&v);
}
KTEST(vecSetAt)
{
Vec v;
vecInit(&v, sizeof(int));
for (int i = 0; i < 5; i++)
vecPush(&v, &i);
int replacement = 99;
KT_TRUE(t, vecSet(&v, 2, &replacement));
KT_EQ(t, *(int*)vecAt(&v, 2), 99);
KT_FALSE(t, vecSet(&v, 10, &replacement)); /* out of range */
vecFree(&v);
}
KTEST(vecInsertRemoveShifts)
{
Vec v;
vecInit(&v, sizeof(int));
for (int i = 0; i < 5; i++)
vecPush(&v, &i); /* 0 1 2 3 4 */
int inserted = 100;
KT_TRUE(t, vecInsert(&v, 2, &inserted)); /* 0 1 100 2 3 4 */
KT_EQ(t, vecLen(&v), 6);
KT_EQ(t, *(int*)vecAt(&v, 2), 100);
KT_EQ(t, *(int*)vecAt(&v, 3), 2);
KT_EQ(t, *(int*)vecAt(&v, 5), 4);
int removed = -1;
KT_TRUE(t, vecRemove(&v, 2, &removed)); /* back to 0 1 2 3 4 */
KT_EQ(t, removed, 100);
KT_EQ(t, vecLen(&v), 5);
for (int i = 0; i < 5; i++)
KT_EQ(t, *(int*)vecAt(&v, (usize)i), i);
vecFree(&v);
}
KTEST(vecClearKeepsCapacity)
{
Vec v;
vecInit(&v, sizeof(int));
for (int i = 0; i < 8; i++)
vecPush(&v, &i);
usize capBefore = vecCap(&v);
vecClear(&v);
KT_EQ(t, vecLen(&v), 0);
KT_TRUE(t, vecIsEmpty(&v));
KT_EQ(t, vecCap(&v), capBefore); /* clear does not free backing storage */
int again = 7;
KT_TRUE(t, vecPush(&v, &again));
KT_EQ(t, *(int*)vecAt(&v, 0), 7);
vecFree(&v);
}
KTEST(vecStructElements)
{
typedef struct { int32 x; int32 y; } Point;
Vec v;
vecInit(&v, sizeof(Point));
Point a = { 1, 2 };
Point b = { 3, 4 };
vecPush(&v, &a);
vecPush(&v, &b);
Point* pa = (Point*)vecAt(&v, 0);
Point* pb = (Point*)vecAt(&v, 1);
KT_EQ(t, pa->x, 1);
KT_EQ(t, pa->y, 2);
KT_EQ(t, pb->x, 3);
KT_EQ(t, pb->y, 4);
vecFree(&v);
}
+1201 -39
View File
File diff suppressed because it is too large. Load diff
+405
View File
@@ -0,0 +1,405 @@
#include "ktest.h"
#include "vfs.h"
#include "pipe.h"
#include "fs.h"
#include "string.h"
#include "klib.h"
#include "sfs.h"
/*
* Coverage for named pipes (vfsMkfifo, vfs.h/vfs.c): a real filesystem entry
* of type fsTypeFifo that lets two processes rendezvous over a blocking
* pipe just by agreeing on a path, with no common ancestor and no handle
* number to pass through argv the way vfsPipeCreateHandle/vfsPipeAttach
* (pipeShareTest.c) require. The genuine cross-process park/wake handshake
* over such a fifo only runs for real at boot (namedreader.c/namedwriter.c,
* two independently spawned kmain.c programs, needles "namedreader:
* opened, blocking on read"/"namedreader: payload-ok" in tools/ci-test.sh)
* -- what is deterministic and scheduler-free (open/read/write never
* actually parking a thread) lives here, matching the division of labor
* pipeShareTest.c already established for the handle-based flavor.
*/
KTEST(vfsMkfifoCreatesFifoNodeVisibleViaStat)
{
KT_EQ(t, vfsMkfifo("/tmp/fifoBasic"), 0);
FbStat st;
KT_EQ(t, vfsStat("/tmp/fifoBasic", &st), 0);
KT_EQ(t, st.type, fsTypeFifo);
/* Same path again must fail, like mkdir/open(O_CREAT|O_EXCL) would --
mkfifo never silently succeeds on an existing name. */
KT_EQ(t, vfsMkfifo("/tmp/fifoBasic"), -1);
KT_EQ(t, vfsUnlink("/tmp/fifoBasic"), 0);
KT_EQ(t, vfsStat("/tmp/fifoBasic", &st), -1);
}
KTEST(vfsMkfifoRejectsReservedAndInvalidPaths)
{
/* The one reserved, non-tree path mkfifo never supports -- a raw byte
device has no notion of a named directory entry at all, unlike "/mnt"
(see the sfsCreateFifo-backed tests below), which does. */
KT_EQ(t, vfsMkfifo("/dev/disk0"), -1);
/* Parent directory does not exist at all. */
KT_EQ(t, vfsMkfifo("/doesNotExist/fifo"), -1);
/* Root itself is never writable -- only vfsMountTmpfs-created
directories (e.g. "/tmp") are, same rule makeChildFile enforces for
oCreate. */
KT_EQ(t, vfsMkfifo("/rootFifo"), -1);
}
KTEST(vfsFifoOpenRejectsDualDirectionModes)
{
KT_EQ(t, vfsMkfifo("/tmp/fifoModes"), 0);
/* This kernel's pipe end is inherently one-directional (fdPipeRead XOR
fdPipeWrite) -- a fifo fd can never be both, unlike Linux's permissive
(POSIX-undefined) O_RDWR on a FIFO. */
KT_EQ(t, vfsOpen("/tmp/fifoModes", oReadWrite), -1);
KT_EQ(t, vfsOpen("/tmp/fifoModes", oReadWrite | oWriteOnly), -1);
KT_EQ(t, vfsUnlink("/tmp/fifoModes"), 0);
}
KTEST(vfsFifoRendezvousByPathAloneNoSharedHandle)
{
KT_EQ(t, vfsMkfifo("/tmp/fifoRendezvous"), 0);
/* Two independent vfsOpen calls on the SAME PATH -- no handle number, no
argv, no common caller -- is exactly what namedreader.c/namedwriter.c
(two kmain.c boot programs with no parent-child relationship) rely
on. */
int rfd = vfsOpen("/tmp/fifoRendezvous", oReadOnly);
KT_GE(t, rfd, 0);
/* Nobody has opened the write end yet: a read must report "try again"
(-1), never spurious end-of-file, exactly like a freshly
pipeCreateDetached handle (pipeShareTest.c). */
char probe[4];
KT_EQ(t, vfsRead(rfd, probe, sizeof(probe)), -1);
KT_NOTNULL(t, vfsBlockingChannel(rfd, false));
int wfd = vfsOpen("/tmp/fifoRendezvous", oWriteOnly);
KT_GE(t, wfd, 0);
KT_NE(t, rfd, wfd);
KT_EQ(t, vfsWrite(wfd, "hi", 2), 2);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2);
KT_TRUE(t, memcmp(buffer, "hi", 2) == 0);
vfsClose(rfd);
vfsClose(wfd);
KT_EQ(t, vfsUnlink("/tmp/fifoRendezvous"), 0);
}
KTEST(vfsFifoUnlinkWhileOpenLeavesLiveFdsWorking)
{
KT_EQ(t, vfsMkfifo("/tmp/fifoUnlinkLive"), 0);
int rfd = vfsOpen("/tmp/fifoUnlinkLive", oReadOnly);
int wfd = vfsOpen("/tmp/fifoUnlinkLive", oWriteOnly);
KT_GE(t, rfd, 0);
KT_GE(t, wfd, 0);
/* Real unlink-while-open semantics: removing the name must not disturb
fds opened before the unlink -- they keep working off their own copy
of the raw pipe handle, never the freed vfs node. */
KT_EQ(t, vfsUnlink("/tmp/fifoUnlinkLive"), 0);
FbStat st;
KT_EQ(t, vfsStat("/tmp/fifoUnlinkLive", &st), -1);
KT_EQ(t, vfsWrite(wfd, "ok", 2), 2);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2);
KT_TRUE(t, memcmp(buffer, "ok", 2) == 0);
vfsClose(rfd);
vfsClose(wfd);
}
KTEST(vfsUnlinkOnNeverOpenedFifoReclaimsPipeSlot)
{
/* Drives the pipe table (pipe.h's fixed-size `pipes[maxPipes]`, shared
with every other pipe user in the kernel) to exhaustion using fifos
that are never opened by anyone -- the one case pipeCloseRead/
pipeCloseWrite's own cleanup can never reach, since neither is ever
called for an end nobody attached. Does not assume the table starts
fully empty (kmain.c's own vfsMkfifo("/tmp/rendezvous") already holds
one slot for the rest of boot by the time ktests run) -- just proves
whatever headroom exists gets used up, and reclaimed, correctly. */
char paths[maxPipes][24];
int madeCount = 0;
for (int i = 0; i < (int)maxPipes; i++) {
snprintf(paths[i], sizeof(paths[i]), "/tmp/fifoLeak%d", i);
if (vfsMkfifo(paths[i]) != 0)
break;
madeCount++;
}
KT_GT(t, madeCount, 0);
/* Table is now genuinely full: one more must fail. */
KT_EQ(t, vfsMkfifo("/tmp/fifoOverflow"), -1);
/* Unlinking one never-opened fifo must reclaim its slot immediately
(pipeDestroyIfUnattached), not leave it dangling until some later,
unrelated event. */
KT_EQ(t, vfsUnlink(paths[0]), 0);
KT_EQ(t, vfsMkfifo("/tmp/fifoOverflow"), 0);
/* Leave the shared pipe table exactly as free as it was found, so later
boot-time pipe users (the pipeProducer/pipeConsumer demo,
fifomaster's handle, killtest's shared pipe, namedreader/
namedwriter's own fifo) are never starved by this test having run. */
vfsUnlink("/tmp/fifoOverflow");
for (int i = 1; i < madeCount; i++)
vfsUnlink(paths[i]);
}
/*
* Coverage for PERSISTENT named pipes: vfsMkfifo under "/mnt" now creates a
* real, on-disk sfs.h entry (sfsTypeFifo) instead of being rejected, so the
* special file itself -- unlike everything above, which lives only in this
* process's kmalloc heap -- survives sfsUnmount()+sfsMount(). The live
* pipe.h byte stream two openers exchange data through is still purely
* in-memory (vfs.c's sfsFifoHandleFor), created lazily on first open, since
* buffered bytes never persist across a real mkfifo(2)'s reboot either --
* only the rendezvous point's name and type do. Every test below starts
* with its own sfsFormat() (same discipline sfsTest.c uses) so results
* never depend on whatever an earlier test (here or in sfsTest.c, same
* boot, same disk) left behind.
*/
KTEST(sfsBackedMkfifoCreatesPersistentFifoVisibleViaStat)
{
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoBasic"), 0);
FbStat st;
KT_EQ(t, vfsStat("/mnt/fifoBasic", &st), 0);
KT_EQ(t, st.type, fsTypeFifo);
KT_EQU(t, st.size, 0u);
/* Same path again must fail, exactly like the tmpfs case above. */
KT_EQ(t, vfsMkfifo("/mnt/fifoBasic"), -1);
KT_EQ(t, vfsUnlink("/mnt/fifoBasic"), 0);
KT_EQ(t, vfsStat("/mnt/fifoBasic", &st), -1);
}
KTEST(sfsBackedFifoRendezvousByPathAloneNoSharedHandle)
{
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoRendezvous"), 0);
int rfd = vfsOpen("/mnt/fifoRendezvous", oReadOnly);
KT_GE(t, rfd, 0);
/* Nobody has opened the write end yet: "try again" (-1), never spurious
end-of-file, exactly like the tmpfs fifo and a freshly
pipeCreateDetached handle both already guarantee. */
char probe[4];
KT_EQ(t, vfsRead(rfd, probe, sizeof(probe)), -1);
KT_NOTNULL(t, vfsBlockingChannel(rfd, false));
int wfd = vfsOpen("/mnt/fifoRendezvous", oWriteOnly);
KT_GE(t, wfd, 0);
KT_NE(t, rfd, wfd);
KT_EQ(t, vfsWrite(wfd, "hi", 2), 2);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2);
KT_TRUE(t, memcmp(buffer, "hi", 2) == 0);
vfsClose(rfd);
vfsClose(wfd);
KT_EQ(t, vfsUnlink("/mnt/fifoRendezvous"), 0);
}
KTEST(sfsBackedFifoUnlinkWhileOpenLeavesLiveFdsWorking)
{
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoUnlinkLive"), 0);
int rfd = vfsOpen("/mnt/fifoUnlinkLive", oReadOnly);
int wfd = vfsOpen("/mnt/fifoUnlinkLive", oWriteOnly);
KT_GE(t, rfd, 0);
KT_GE(t, wfd, 0);
/* Real unlink-while-open semantics, same guarantee as the tmpfs fifo:
removing the directory entry must not disturb fds opened before the
unlink -- they keep working off their own copy of the raw pipe
handle number. */
KT_EQ(t, vfsUnlink("/mnt/fifoUnlinkLive"), 0);
FbStat st;
KT_EQ(t, vfsStat("/mnt/fifoUnlinkLive", &st), -1);
KT_EQ(t, vfsWrite(wfd, "ok", 2), 2);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2);
KT_TRUE(t, memcmp(buffer, "ok", 2) == 0);
vfsClose(rfd);
vfsClose(wfd);
}
KTEST(sfsBackedFifoEntryPersistsAcrossUnmountRemount)
{
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoPersist"), 0);
/* The real point of this test (mirrors sfsTest.c's own
sfsMountReloadsExistingFilesystem-style coverage): drop EVERY
in-memory sfs.c structure (bitmaps, superblock) and reconstruct them
purely from what sfsFormat/sfsCreateFifo actually wrote to disk --
proving the fifo is a genuine on-disk entry, not a kmalloc node that
would evaporate the instant this process's heap did (like the tmpfs
fifo above, which vfsMkfifo("/mnt/...") deliberately never was). */
sfsUnmount();
KT_TRUE(t, sfsMount());
FbStat st;
KT_EQ(t, vfsStat("/mnt/fifoPersist", &st), 0);
KT_EQ(t, st.type, fsTypeFifo);
/* Still fully functional after the remount, not just present in a
directory listing: a fresh open/write/read round trip works exactly
as it would have before the remount. */
int rfd = vfsOpen("/mnt/fifoPersist", oReadOnly);
int wfd = vfsOpen("/mnt/fifoPersist", oWriteOnly);
KT_GE(t, rfd, 0);
KT_GE(t, wfd, 0);
KT_EQ(t, vfsWrite(wfd, "yo", 2), 2);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2);
KT_TRUE(t, memcmp(buffer, "yo", 2) == 0);
vfsClose(rfd);
vfsClose(wfd);
KT_EQ(t, vfsUnlink("/mnt/fifoPersist"), 0);
}
KTEST(sfsBackedFifoReopenAfterCompletedRoundTripGetsFreshWorkingSession)
{
/* Ordinary fifo usage, not a corner case: open both ends, exchange
data, close both, then reopen the SAME persistent path later for a
second, unrelated conversation -- exactly what a real named pipe is
for. The first round trip fully releases pipe.h's underlying slot
back to its general pool (pipeReleaseIfUnused, once both readers and
writers hit 0), so the second round's vfs.c-level binding
(sfsFifoHandleFor) MUST detect that its remembered handle number no
longer refers to the same pipe (pipeIncarnation, pipe.h) and mint a
genuinely fresh one -- otherwise this second write would silently
target a slot nobody has attached to (or, worse, one some unrelated
pipe elsewhere in the kernel grabbed in between), and would report
-1 forever instead of actually delivering the byte. */
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoRoundTrip"), 0);
int rfd1 = vfsOpen("/mnt/fifoRoundTrip", oReadOnly);
int wfd1 = vfsOpen("/mnt/fifoRoundTrip", oWriteOnly);
KT_GE(t, rfd1, 0);
KT_GE(t, wfd1, 0);
KT_EQ(t, vfsWrite(wfd1, "one", 3), 3);
char first[4];
memset(first, 0, sizeof(first));
KT_EQ(t, vfsRead(rfd1, first, sizeof(first)), 3);
KT_TRUE(t, memcmp(first, "one", 3) == 0);
vfsClose(rfd1);
vfsClose(wfd1);
/* Second, independent session on the same path. */
int rfd2 = vfsOpen("/mnt/fifoRoundTrip", oReadOnly);
KT_GE(t, rfd2, 0);
/* A genuinely fresh pipe reports "try again" here, exactly like a
first-ever open -- a stale, no-longer-attached handle would instead
report -1 forever for an entirely different reason (pipeAt finding
nothing "used" at that slot), which the write assertion below is
what actually tells apart from this one. */
char probe[4];
KT_EQ(t, vfsRead(rfd2, probe, sizeof(probe)), -1);
int wfd2 = vfsOpen("/mnt/fifoRoundTrip", oWriteOnly);
KT_GE(t, wfd2, 0);
KT_EQ(t, vfsWrite(wfd2, "two", 3), 3);
char second[4];
memset(second, 0, sizeof(second));
KT_EQ(t, vfsRead(rfd2, second, sizeof(second)), 3);
KT_TRUE(t, memcmp(second, "two", 3) == 0);
vfsClose(rfd2);
vfsClose(wfd2);
KT_EQ(t, vfsUnlink("/mnt/fifoRoundTrip"), 0);
}
KTEST(sfsBackedFifoBindingSurvivesReformatOfAnUnrelatedInode)
{
/* sfsTest.c's tests each start with their own sfsFormat(), reusing
inode numbers dozens of times a boot. A fifo binding left open
ACROSS such a reformat (contrived here, but exactly the shape
reclaimOpenFds/sched.c would leave behind for a thread that died
without closing its fd -- see sched.c) must never let the fresh
filesystem's completely unrelated reuse of the same inode number
inherit the old, orphaned session. */
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoGenA"), 0);
int32 firstInode = sfsLookup("/fifoGenA");
KT_GE(t, firstInode, 0);
/* Left deliberately open across the reformat below -- an orphaned
session, on purpose, is exactly what must NOT leak into the new one. */
int staleRfd = vfsOpen("/mnt/fifoGenA", oReadOnly);
KT_GE(t, staleRfd, 0);
/* A full reformat: every old inode number, including firstInode, now
means whatever the fresh, empty filesystem below reassigns it to. */
KT_TRUE(t, sfsFormat());
KT_EQ(t, vfsMkfifo("/mnt/fifoGenA"), 0);
int32 secondInode = sfsLookup("/fifoGenA");
KT_GE(t, secondInode, 0);
/* Deterministic on a freshly formatted, otherwise-empty filesystem:
both are the very first entry created after root, so the numbers
collide -- exactly the scenario that makes the generation check
necessary rather than merely defensive. */
KT_EQ(t, secondInode, firstInode);
int rfd = vfsOpen("/mnt/fifoGenA", oReadOnly);
int wfd = vfsOpen("/mnt/fifoGenA", oWriteOnly);
KT_GE(t, rfd, 0);
KT_GE(t, wfd, 0);
KT_EQ(t, vfsWrite(wfd, "new", 3), 3);
/* The discriminating check: if the binding table wrongly inherited the
pre-reformat session (generation check not actually doing its job),
staleRfd would be a SECOND reader attached to the very same
underlying pipe wfd just wrote into, and -- read before rfd below --
would steal the payload meant for the new session's own reader.
Correct isolation means staleRfd's pipe never had any writer at all
(it only ever attached as a reader), so it must still report "try
again", never the new session's bytes. */
char stalePeek[4];
KT_EQ(t, vfsRead(staleRfd, stalePeek, sizeof(stalePeek)), -1);
char buffer[4];
memset(buffer, 0, sizeof(buffer));
KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 3);
KT_TRUE(t, memcmp(buffer, "new", 3) == 0);
vfsClose(rfd);
vfsClose(wfd);
/* The orphaned pre-reformat fd is cleaned up here rather than left to
leak a pipe.c slot for the rest of the boot -- vfsClose only ever
touches pipe.c state (pipeCloseRead), never re-derefs the long-gone
inode, so this is safe regardless of what the fresh filesystem did
with the number. */
vfsClose(staleRfd);
vfsUnlink("/mnt/fifoGenA");
}
+330 -6
View File
@@ -1,19 +1,27 @@
#!/bin/sh #!/bin/sh
# Headless boot gate: run the ISO in QEMU, capture COM1, and require that a # Headless boot gate: run the ISO in QEMU, capture COM1, and require that a
# ring-3 program read a known ramdisk (USTAR) file and round-tripped a tmpfs # ring-3 program read a known ramdisk (USTAR) file, round-tripped a tmpfs
# file. Exits non-zero when any expected string is missing. # file, and round-tripped real sector I/O against a QEMU-emulated ATA disk
# via /dev/disk0. Exits non-zero when any expected string is missing.
here=$(dirname "$0") here=$(dirname "$0")
root=$(cd "$here/.." && pwd) root=$(cd "$here/.." && pwd)
iso="$root/os.iso" iso="$root/os.iso"
disk="$root/build/disk.img"
if [ ! -f "$iso" ]; then if [ ! -f "$iso" ]; then
echo "ci-test: $iso not found, build it first (make)" echo "ci-test: $iso not found, build it first (make)"
exit 2 exit 2
fi fi
if [ ! -f "$disk" ]; then
mkdir -p "$root/build"
dd if=/dev/zero of="$disk" bs=1M count=8 status=none
fi
log=$(mktemp) log=$(mktemp)
timeout 25 qemu-system-x86_64 -cdrom "$iso" -serial "file:$log" \ timeout 25 qemu-system-x86_64 -cdrom "$iso" \
-drive file="$disk",format=raw,if=ide -serial "file:$log" \
-display none -m 128M -no-reboot >/dev/null 2>&1 || true -display none -m 128M -no-reboot >/dev/null 2>&1 || true
echo "----- serial log (tail) -----" echo "----- serial log (tail) -----"
@@ -24,7 +32,301 @@ status=0
for needle in \ for needle in \
"frostbyte-ustar-read-ok" \ "frostbyte-ustar-read-ok" \
"frostbyte-tmpfs-rw-ok" \ "frostbyte-tmpfs-rw-ok" \
"fileread: done" "fileread: done" \
"pipetest: done" \
"pipe demo: eof, count=10" \
"disktest: done" \
"fstest: done" \
"linktest: done" \
"sched-priority-ok" \
"join-reap-ok" \
"proctest: spawn+wait ok" \
"proctest: spawn bad path rejected" \
"proctest: wait bad pid rejected" \
"exectarget: running" \
"exec-pid-preserved-ok" \
"user-mem-reclaimed-ok" \
"exec-old-slot-freed-ok" \
"proctest: spawn with argv ok" \
"argtest: argv-ok" \
"fiforeader: attached, blocking on read" \
"fifowriter: wrote payload" \
"fiforeader: payload-ok" \
"fifomaster: pipeshare ok" \
"killtest: self-kill rejected" \
"killtest: kill bad pid rejected" \
"killvictim: blocking on read forever" \
"killtest: killed victim" \
"killtest: kill-ok" \
"mkfifo /tmp/rendezvous ready" \
"namedreader: opened, blocking on read" \
"namedwriter: wrote payload" \
"namedreader: payload-ok" \
"consolereader1: start" \
"consolereader2: start" \
"injecting synthetic keypress" \
"console-fanout-1-ok" \
"console-fanout-2-ok" \
"sigtest: signal to bad pid rejected" \
"sigtest: sigkill-handler-rejected-ok" \
"sigtarget: waiting for signal" \
"sigtest: sent sigUsr1 to sigtarget" \
"sigtarget: handler invoked signum=10" \
"sigtarget: handler-fired-ok" \
"sigtarget: resumed-after-handler-ok" \
"sigtest: handler-roundtrip-ok" \
"sigdefault: waiting to be terminated by default disposition" \
"sigtest: sent sigTerm to sigdefault" \
"sigtest: default-terminate-ok" \
"jobtarget: ready, blocking on go pipe" \
"jobtest: jobtarget ready" \
"jobtest: child-inherits-parent-pgid-ok" \
"jobtest: sent sigStop to jobtarget" \
"jobtest: sent sigCont to jobtarget" \
"jobtarget: received go" \
"jobtest: job-stop-continue-ok" \
"nesttarget: ready, waiting for sigUsr1" \
"nesttest: nesttarget ready" \
"nesttest: sent sigUsr1 to nesttarget" \
"nesttarget: usr1-handler-enter" \
"nesttest: nesttarget usr1-handler-entered" \
"nesttest: sent sigUsr2 to nesttarget" \
"nesttarget: usr2-handler-fired-nested" \
"nesttarget: usr1-handler-resumed-after-nested-usr2" \
"nesttarget: nested-delivery-ok" \
"nesttarget: resumed-after-both-handlers-ok" \
"nesttest: nested-signal-delivery-ok" \
"eintrtarget: blocking on read forever" \
"eintrtest: eintrtarget ready" \
"eintrtest: sent sigTerm to eintrtarget" \
"eintrtest: early-wake-signal-ok" \
"restarttarget: ready, handlers installed" \
"restarttest: restarttarget ready" \
"restarttest: sent sigUsr1 (no restart) to restarttarget" \
"restarttarget: usr1-handler-fired" \
"restarttarget: phase1-eintr-no-restart-ok" \
"restarttest: sent sigUsr2 (restart) to restarttarget" \
"restarttarget: usr2-handler-fired" \
"restarttarget: phase2-restart-transparent-ok" \
"restarttarget: all-phases-ok" \
"restarttest: sa-restart-eintr-distinction-ok" \
"masktarget: ready, waiting for sigUsr1" \
"masktest: masktarget ready" \
"masktest: sent sigUsr1 to masktarget" \
"masktarget: usr1-handler-enter" \
"masktest: masktarget usr1-handler-entered" \
"masktest: sent sigUsr2 to masktarget (should stay masked)" \
"masktarget: still-masked-after-read-ok" \
"masktarget: usr2-handler-fired-after-unmask" \
"masktarget: mask-defers-then-delivers-ok" \
"masktarget: resumed-after-deferred-usr2-ok" \
"masktest: sa-mask-defer-then-deliver-ok" \
"sigchldchild: exiting now" \
"sigchldtest: sigchld-handler-fired" \
"sigchldtest: sigchld-delivered-to-parent-ok" \
"sigchldtest: chld-reap-after-signal-ok" \
"waitanyfast: exiting now" \
"waitanyslow: exiting now" \
"waitanytest: any-child-reap-set-ok" \
"waitanytest: echild-when-no-children-ok" \
"stopchldtarget: ready, blocking on go pipe" \
"stopchldtest: stopchldtarget ready" \
"stopchldtest: sent sigStop to stopchldtarget" \
"stopchldtest: sigchld-on-stop-ok" \
"stopchldtest: sent sigCont to stopchldtarget" \
"stopchldtest: sigchld-on-continue-ok" \
"stopchldtarget: received go" \
"stopchldtest: job-control-sigchld-ok" \
"nodefertest: onUsr1 first entry, self-raising sigUsr1 (NODEFER)" \
"nodefertest: onUsr1 nested entry (depth 2)" \
"nodefertest: onUsr1 observed its own nested re-entry (nodefer-self-nest-ok)" \
"nodefertest: nodefer-self-nest-verified-ok" \
"nodefertest: onUsr2 first entry, self-raising sigUsr2 (default defer)" \
"nodefertest: onUsr2 confirmed no self-nest while deferred (still-deferred-ok)" \
"nodefertest: onUsr2 second, TOP-LEVEL entry after first returned" \
"nodefertest: default-defer-no-self-nest-verified-ok" \
"nodefertest: sa-nodefer-contrast-ok" \
"alarmtest: alarm-replace-and-cancel-bookkeeping-ok" \
"alarmtest: blocking on read, waiting for real alarm to fire" \
"alarmtest: onAlrm fired" \
"alarmtest: alarm-fired-interrupted-blocking-read-ok" \
"alarmdefault: blocking on read forever" \
"alarmtest: alarm-default-disposition-terminated-ok" \
"alarmtest: sigalrm-ok" \
"siginfotarget: siginfo-sender-pid-ok" \
"siginfotarget: resumed-after-siginfo-handler-ok" \
"siginfotest: sent sigUsr1 to siginfotarget" \
"siginfotest: sigchld-siginfo-accurate-ok" \
"siginfotest: siginfo-roundtrip-ok" \
"selfstoptarget: self-stopping now" \
"selfstoptest: self-stop-confirmed-via-sigchld-ok" \
"selfstoptest: still-stopped-before-cont-ok" \
"selfstoptest: sent sigCont to selfstoptarget" \
"selfstoptarget: resumed-after-self-stop-ok" \
"selfstoptest: post-cont-proof-received-ok" \
"selfstoptest: self-stop-continue-ok" \
"pgroupfg: ready, blocking on go pipe" \
"pgroupbg: own-process-group-ok" \
"pgroupbg: ready, blocking on go pipe" \
"injecting synthetic ctrl-c for the process-group demo" \
"pgrouptest-fg-terminated-by-sigint-ok" \
"pgroupbg: still-alive-after-ctrlc-ok" \
"pgroupbg: resumed-after-signal-ok" \
"jobctlbg started in background" \
"jobctlbg: own-process-group-ok" \
"jobctlbg: ready, blocking on go pipe" \
"jobctlfg: ready, blocking on go pipe" \
"injecting synthetic ctrl-c for the on-demand job-control demo" \
"jobctlfg exited with code 130" \
"shell-foreground-reclaimed-ok" \
"jobctlbg: still-alive-after-ctrlc-ok" \
"jobctlbg: resumed-after-signal-ok" \
"polltest: non-blocking-poll-empty-ok" \
"polltest: poll-timeout-ok" \
"pollwriter: attached, sleeping before write" \
"polltest: blocking on multi-fd poll, waiting for pollwriter" \
"pollwriter: wrote payload" \
"polltest: poll-woken-by-write-ok" \
"polltest: payload-ok" \
"polltest: poll-multiplex-ok" \
"typing shell command for the pipeline demo" \
"pipesrc: wrote payload" \
"pipesink: blocking on read, waiting for pipesrc" \
"pipesink: payload-ok" \
"pipesrc | pipesink exited with code 0" \
"typing shell commands for the file redirection demo" \
"redirwriter: wrote payload" \
"redirwriter exited with code 0" \
"redirreader: reading redirected stdin" \
"redirreader: payload-ok" \
"redirreader exited with code 0" \
"typing shell commands for the stderr redirection demo" \
"stderrwriter: wrote payload" \
"stderrwriter exited with code 0" \
"stderrreader: reading redirected stdin" \
"stderrreader: payload-ok" \
"stderrreader exited with code 0" \
"typing shell command for the here-string demo" \
"herestest: reading redirected stdin" \
"herestest: payload-ok" \
"herestest exited with code 0" \
"typing shell command for the here-string pipeline demo" \
"hcat: reading here-string stdin" \
"hcat: forwarded payload" \
"hsink: blocking on read, waiting for hcat" \
"hsink: payload-ok" \
"hcat | hsink exited with code 0" \
"typing shell command for the three-stage pipeline demo" \
"pipecat: reading piped stdin" \
"pipecat: forwarded payload" \
"pipesrc | pipecat | pipesink exited with code 0" \
"cwdtest: chdir-cwda-ok" \
"cwdtest: relative-write-cwda-ok" \
"cwdtarget: inherited-cwd-ok" \
"cwdtarget: relative-write-cwdb-ok" \
"cwdtest: parent-cwd-unaffected-ok" \
"cwdtest: isolation-verified-ok" \
"typing shell commands for the environment variable demo" \
"envprint: inherited-ok GREETING=frostbyte" \
"envprint: self-set-ok GREETING=child-mutated" \
"envprint: unset-ok" \
"envprint: unset-missing-ok" \
"envprint exited with code 0" \
"env: GREETING=frostbyte" \
"nicetest: bogus-pid-rejected-ok" \
"nicetest: zero-weight-rejected-ok" \
"nicetest: self-renice-ok" \
"nicetarget: reniced-while-sleeping-ok" \
"nicetest: other-renice-ok" \
"nicetest: reaped-pid-rejected-ok" \
"nicetest: priority-syscalls-ok" \
"date: printed-ok" \
"date: roundtrip-ok" \
"typing shell commands for the wildcard glob demo" \
"globcat: got 2 files" \
"globcat: /tmp/gd/a.txt=a" \
"globcat: /tmp/gd/b.txt=b" \
"timetest: create-stamped-ok" \
"timetest: write-stamped-ok" \
"timetest: hardlink-shares-mtime-ok" \
"timetest: PASS" \
"rmtest: tmpfs-reject-ok" \
"rmtest: tmpfs-remove-ok" \
"rmtest: sfs-reject-ok" \
"rmtest: sfs-remove-ok" \
"rmtest: PASS" \
"permtest: default-mode-ok" \
"permtest: write-denied-ok" \
"permtest: write-restored-ok" \
"permtest: copy-not-executable-ok" \
"permtest: exec-denied-ok" \
"permtest: exec-after-chmod-ok" \
"permtest: PASS" \
"identtest: root-by-default-ok" \
"identtest: owner-root-ok" \
"identtest: owner-write-ok-before-drop" \
"identtest: owner-exec-ok-before-drop" \
"identtest: chown-root-ok" \
"identtest: drop-to-1000-ok" \
"identtest: cannot-regain-root-ok" \
"identtest: setuid-self-noop-ok" \
"identtest: write-denied-non-owner-ok" \
"identtest: exec-denied-non-owner-ok" \
"identtest: chmod-nonowner-denied-ok" \
"identtest: chown-nonroot-denied-ok" \
"identtest: chmod-owner-after-drop-ok" \
"identtest: chown-owner-nonroot-denied-ok" \
"identtest: PASS" \
"dirpermtest: setup-ok" \
"dirpermtest: root-self-write-denied-ok" \
"dirpermtest: drop-to-1000-ok" \
"dirpermtest: parent-write-denied-ok" \
"dirpermtest: other-write-allowed-ok" \
"dirpermtest: other-unlink-allowed-ok" \
"dirpermtest: traverse-denied-ok" \
"dirpermtest: traverse-allowed-ok" \
"dirpermtest: list-denied-ok" \
"dirpermtest: search-without-list-ok" \
"dirpermtest: owner-write-allowed-ok" \
"dirpermtest: PASS" \
"grouptest: root-by-default-ok" \
"grouptest: fresh-file-gid-stamped-ok" \
"grouptest: chgrp-root-arbitrary-ok" \
"grouptest: setgid-root-arbitrary-ok" \
"grouptest: drop-to-6000-7000-ok" \
"grouptest: setgid-nonroot-denied-ok" \
"grouptest: setgid-self-noop-ok" \
"grouptest: group-read-allowed-ok" \
"grouptest: group-write-denied-ok" \
"grouptest: other-fallback-denied-ok" \
"grouptest: group-exec-allowed-ok" \
"grouptest: group-exec-denied-ok" \
"grouptest: chgrp-owner-self-gid-ok" \
"grouptest: chgrp-owner-arbitrary-denied-ok" \
"grouptest: chgrp-nonowner-nonroot-denied-ok" \
"grouptest: PASS" \
"pidfdtest: bad-target rejected" \
"pidfdtest: pidfd-open-ok" \
"pidfdtest: pidfd-send-ok" \
"pidfdtest: pidfd-wait-ok" \
"pidfdtest: reaped-target-detected-ok" \
"pidfdtest: PASS" \
"mmaptest: anon-zero-fill-ok" \
"mmaptest: anon-write-read-ok" \
"mmaptest: anon-munmap-ok" \
"mmaptest: file-map-content-ok" \
"mmapprotvictim: read-ok" \
"mmaptest: protection-sigsegv-ok" \
"mmaptest: exec-allowed-ok" \
"mmapexecvictim: write-ok" \
"mmaptest: exec-denied-sigsegv-ok" \
"mmaptest: PASS" \
"typing shell commands for the ls -l demo" \
"f 17 data.txt " \
"f 17 alias.txt " \
"f 17 data.txt 20" \
"typing shell commands for the &&/|| chaining demo" \
"ao2"
do do
if grep -qF "$needle" "$log"; then if grep -qF "$needle" "$log"; then
echo "PASS: found '$needle'" echo "PASS: found '$needle'"
@@ -34,10 +336,32 @@ do
fi fi
done done
# In-kernel self-test battery (ktest). The summary line is machine-readable:
# "KTEST SUMMARY: <tests> tests, <checks> checks, <failures> failed"
# The gate requires that line to exist, report 0 failures, and run at least
# one test. A hung or crashing test suppresses the line and fails the gate.
summary=$(grep -F "KTEST SUMMARY:" "$log" | tail -n 1)
if [ -z "$summary" ]; then
echo "FAIL: no 'KTEST SUMMARY:' line (in-kernel tests did not complete)"
status=1
else
echo "PASS: $summary"
tests=$(echo "$summary" | sed -n 's/.*: \([0-9]*\) tests.*/\1/p')
fails=$(echo "$summary" | sed -n 's/.* \([0-9]*\) failed.*/\1/p')
if [ "${tests:-0}" -lt 1 ]; then
echo "FAIL: ktest ran 0 tests"
status=1
fi
if [ "${fails:-1}" -ne 0 ]; then
echo "FAIL: ktest reported $fails failed check(s)"
status=1
fi
fi
rm -f "$log" rm -f "$log"
if [ "$status" -eq 0 ]; then if [ "$status" -eq 0 ]; then
echo "ci-test: all filesystem checks passed" echo "ci-test: all boot and self-test checks passed"
else else
echo "ci-test: filesystem checks FAILED" echo "ci-test: checks FAILED"
fi fi
exit $status exit $status
+53
View File
@@ -0,0 +1,53 @@
#include "ulib.h"
/*
* Only ever spawned by alarmtest.c. Installs NO sigAlrm handler at all --
* the real-Unix default disposition for SIGALRM (signal.c's
* sigDefaultIsIgnore explicitly does NOT list sigAlrm, so it falls through
* to the same default-Terminate path every other "real" signal in this
* kernel already has) is Terminate, not Ignore. userAlarm(1) schedules a
* real SIGALRM one second from now against THIS process; the read below
* blocks on a pipe no writer will ever attach to (the same "blocked
* forever" setup eintrtarget.c/killvictim.c already use), so the only way
* this process ever leaves that read is the alarm actually firing and
* killing it -- exactly like eintrtarget.c, except the interrupting signal
* here comes from schedule()'s own timer-tick expiry scan (sched.c), not
* from another process's userSigSend. If threadSetAlarm/schedule's expiry
* scan were ever broken, this process would simply never return from the
* read below, hanging the whole boot gate to its 25s QEMU timeout instead
* of merely failing a check -- the same fault-injection-visible shape
* eintrtest.c's own file comment already establishes for early-wake.
*/
int main(void)
{
print("alarmdefault: start, pid=");
printDec(userGetpid());
print("\n");
long victimHandle = userPipeCreate();
if (victimHandle < 0) {
print("alarmdefault: FAIL create victim pipe\n");
return 1;
}
long victimFd = userPipeAttach(victimHandle, 0);
if (victimFd < 0) {
print("alarmdefault: FAIL attach victim pipe\n");
return 1;
}
if (userAlarm(1) != 0) {
print("alarmdefault: FAIL unexpected pre-existing alarm\n");
return 1;
}
print("alarmdefault: blocking on read forever\n");
char buffer[4];
userRead((int)victimFd, buffer, sizeof(buffer));
/* Only reached if userRead somehow returned on its own -- nothing ever
writes to the victim pipe, so the alarm not firing (or firing but
not actually terminating this process) is the only way to get here. */
print("alarmdefault: FAIL read returned without being signaled\n");
return 1;
}
+168
View File
@@ -0,0 +1,168 @@
#include "ulib.h"
/*
* Boot-time driver for real alarm(2)/SIGALRM (sched.c's threadSetAlarm,
* schedule()'s own timer-tick expiry scan, signalnum.h's sigAlrm):
* schedule() already fires against a per-thread pitTicks() deadline
* regardless of what that thread is doing, the exact same shape sleepUntil
* already has for threadSleep -- but expiry here raises a REAL signal
* (sigRaise) instead of just flipping ThreadState, which means everything
* this kernel already built for signals (catch, default-terminate,
* SA_RESTART/EINTR on a retryable syscall, early-wake for a Sleeping/
* Blocked target) applies to it for free. Three phases, each proving a
* DIFFERENT piece of that:
*
* phase 1: userAlarm's own "at most one pending alarm" bookkeeping --
* scheduling one, then replacing it, then canceling it, checking the
* PREVIOUS remaining-seconds return value each time. Purely arithmetic
* (see threadSetAlarm's own KTEST coverage in schedTest.c for the same
* logic driven directly, deterministically, with no real waiting) --
* this phase proves the userspace-visible contract of that same logic,
* with no real elapsed time required at all.
*
* phase 2: a REAL 1-second alarm actually firing against a thread
* genuinely blocked forever in userRead (the same "attach a victim pipe
* no writer will ever attach to" setup eintrtarget.c/restarttarget.c
* already use) -- WITH a caught handler, no sigFlagRestart, so the read
* must return errInterrupted the instant the handler returns, exactly
* like restarttarget.c's own phase 1, just self-triggered by a hardware
* timer instead of another process's userSigSend.
*
* phase 3: the SAME real firing, but against a completely separate,
* freshly spawned child (alarmdefault.c) that installs NO handler at
* all -- SIGALRM's real default disposition is Terminate (signal.c's
* sigDefaultIsIgnore deliberately does not list it), so that child must
* actually die, with exit code 128+sigAlrm, exactly like sigtest.c's own
* sigdefault.c proves for sigTerm.
*
* alarmdefault is deliberately SPAWNED before phase 2's own 1-second alarm
* is even set, not after phase 2 finishes: its own 1-second countdown then
* runs CONCURRENTLY with this process's phase-2 wait (this kernel's real
* preemptive scheduler already runs every Runnable/Sleeping thread's own
* timer independently -- see schedule()'s alarm-expiry scan), so the two
* real-time waits overlap into roughly one real second of added boot time
* total, not two sequential ones -- every boot-time demo shares one fixed
* 25s QEMU budget (tools/ci-test.sh), and stacking real-time waits serially
* eats into every OTHER demo's own margin against that same clock for no
* added proof value (phase 2 and phase 3 exercise two independent code
* paths -- caught+EINTR vs default-Terminate -- neither depends on the
* other's outcome, so nothing is lost by racing them).
*
* If schedule()'s alarm-expiry scan were ever broken (deadline set but
* never actually checked, or checked but sigRaise never called), phase 2's
* read call below would simply never return and phase 3's userWait would
* simply never see its child die -- both hang the whole boot gate to its
* 25s QEMU timeout rather than merely failing a check, the same
* fault-injection-visible shape every other "blocked forever until a
* signal rescues it" demo in this kernel already has.
*/
static volatile int handlerFired = 0;
static void onAlrm(int signum)
{
(void)signum;
handlerFired++;
print("alarmtest: onAlrm fired\n");
}
int main(void)
{
print("alarmtest: start, pid=");
printDec(userGetpid());
print("\n");
/* Phase 1: pure bookkeeping, no real waiting. */
unsigned long previous1 = userAlarm(10);
if (previous1 != 0) {
print("alarmtest: FAIL expected no prior alarm\n");
return 1;
}
unsigned long previous2 = userAlarm(3);
if (previous2 != 10) {
print("alarmtest: FAIL expected 10s remaining on replaced alarm, got ");
printDec((long)previous2);
print("\n");
return 1;
}
unsigned long previous3 = userAlarm(0);
if (previous3 != 3) {
print("alarmtest: FAIL expected 3s remaining on canceled alarm, got ");
printDec((long)previous3);
print("\n");
return 1;
}
unsigned long previous4 = userAlarm(0);
if (previous4 != 0) {
print("alarmtest: FAIL expected no alarm pending after cancel, got ");
printDec((long)previous4);
print("\n");
return 1;
}
print("alarmtest: alarm-replace-and-cancel-bookkeeping-ok\n");
/* Spawned here, deliberately BEFORE phase 2 sets its own alarm below --
see the file comment: this lets alarmdefault's own 1-second alarm
count down concurrently with phase 2's, instead of only starting
once phase 2 has already finished. */
long child = userSpawn("/bin/alarmdefault", 0);
if (child < 0) {
print("alarmtest: FAIL spawn alarmdefault\n");
return 1;
}
/* Phase 2: real firing, caught, no sigFlagRestart -- must EINTR. */
if (userSigAction(sigAlrm, onAlrm, 0, 0) == (long)sigSetFailed) {
print("alarmtest: FAIL install sigAlrm handler\n");
return 1;
}
long victimHandle = userPipeCreate();
if (victimHandle < 0) {
print("alarmtest: FAIL create victim pipe\n");
return 1;
}
long victimFd = userPipeAttach(victimHandle, 0);
if (victimFd < 0) {
print("alarmtest: FAIL attach victim pipe\n");
return 1;
}
if (userAlarm(1) != 0) {
print("alarmtest: FAIL expected no prior alarm before phase 2\n");
return 1;
}
print("alarmtest: blocking on read, waiting for real alarm to fire\n");
char victimBuffer[4];
long readResult = userRead((int)victimFd, victimBuffer, sizeof(victimBuffer));
userClose((int)victimFd);
if (handlerFired != 1) {
print("alarmtest: FAIL onAlrm handler did not fire exactly once\n");
return 1;
}
if (readResult != errInterrupted) {
print("alarmtest: FAIL blocking read did not return errInterrupted\n");
return 1;
}
print("alarmtest: alarm-fired-interrupted-blocking-read-ok\n");
/* Phase 3: real firing, default disposition, must terminate a
completely separate process -- alarmdefault (spawned above, before
phase 2) has had its own 1-second alarm counting down this whole
time, so this wait should return almost immediately, not add a
second real second of its own. */
long childStatus = userWait(child);
print("alarmtest: alarmdefault exit status=");
printDec(childStatus);
print("\n");
if (childStatus != 128 + sigAlrm) {
print("alarmtest: FAIL expected alarmdefault exit status 128+sigAlrm\n");
return 1;
}
print("alarmtest: alarm-default-disposition-terminated-ok\n");
print("alarmtest: sigalrm-ok\n");
return 0;
}
+24
View File
@@ -0,0 +1,24 @@
#include "ulib.h"
/*
* Real ring-3 proof that shell.c's `$NAME`/`$?` expansion (shellExpandArgv,
* shellExpandToken) genuinely reaches a SPAWNED program's own argv, not
* just a shell builtin's -- every earlier consumer of expansion (export,
* env, echo) is a builtin, and shell.c's doEcho never calls shellExpandArgv
* at all (only runProgram/runPipelineStages do, for a real spawned
* process), so nothing in this kernel's live-demo chain had ever actually
* typed a `$` at the real shell before this program existed. Prints each
* argv[1..argc) on its own line, prefixed like globcat.c's own
* "globcat: " diagnostic style -- a generic argv printer, not tied to any
* one variable, so a future demo can reuse it for any other `$NAME`/`$?`
* case without needing yet another single-purpose binary.
*/
int main(int argc, char** argv)
{
for (int i = 1; i < argc; i++) {
print("argecho: ");
print(argv[i]);
print("\n");
}
return 0;
}
+47
View File
@@ -0,0 +1,47 @@
#include "ulib.h"
/*
* Exercises argv/argc plumbing added this wave (elfBuildInitialStack in
* elf.c, copyUserArgv in syscall.c, crt.S's _start now reading argc/argv
* off the initial stack instead of calling main() with nothing). Only ever
* spawned by proctest.c, with a specific, known argv -- if the kernel-side
* stack layout is wrong in any way (misaligned rsp, wrong argc, a string
* copied to the wrong address, an argv[i] pointer landing on garbage),
* these comparisons catch it directly instead of a lucky match papering
* over a bug. userspace has no libc string.h, so the comparison is spelled
* out by hand exactly the way spawntarget.c/proctest.c already avoid
* pulling in anything beyond ulib.h.
*/
static int streq(const char* a, const char* b)
{
while (*a && *a == *b) {
a++;
b++;
}
return *a == *b;
}
int main(int argc, char** argv)
{
print("argtest: argc=");
printDec(argc);
print("\n");
for (int i = 0; i < argc; i++) {
print("argtest: argv[");
printDec(i);
print("]=");
print(argv[i]);
print("\n");
}
int ok = argc == 4 &&
streq(argv[0], "/bin/argtest") &&
streq(argv[1], "alpha") &&
streq(argv[2], "beta") &&
streq(argv[3], "gamma");
print(ok ? "argtest: argv-ok\n" : "argtest: argv-FAIL\n");
return ok ? 0 : 1;
}
+37
View File
@@ -0,0 +1,37 @@
#include "ulib.h"
/*
* Boot-spawned independently of consolereader2.c (see kmain.c's spawnUser
* calls) -- like namedreader.c/namedwriter.c, neither process's pid, argv,
* or fd table ever mentions the other. Both simply read fd 0, which every
* process gets for free (vfs.c's vfsFdInit) as its own console reader queue
* (keyboard.h) rather than a single kernel-wide buffer: proving that two
* completely unrelated processes reading "the keyboard" each get their own
* independent copy of every keystroke instead of racing each other for it.
*
* kmain.c's consoleInjectorThread fires exactly one synthetic keystroke a
* few hundred milliseconds after boot (there is no automated way to drive a
* real PS/2 device under headless QEMU, see its own comment) through the
* same keyboardHandleScancode the real IRQ handler calls -- so this process
* has to genuinely park in userRead across many timer ticks and be woken
* later, not just win a race against a byte that was already queued.
*/
int main(void)
{
print("consolereader1: start, pid=");
printDec(userGetpid());
print(", blocking on fd 0\n");
char c = 0;
long got = userRead(0, &c, 1);
if (got != 1) {
print("consolereader1: FAIL read\n");
return 1;
}
print("consolereader1: got byte '");
userWrite(1, &c, 1);
print("' -> console-fanout-1-ok\n");
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
#include "ulib.h"
/* See consolereader1.c's comment -- an entirely independent process reading
the same fd 0, spawned separately by kmain.c, sharing nothing with
consolereader1 but the fact that both happen to be alive when
consoleInjectorThread's single synthetic keystroke fires. If fan-out
ever regressed back to one shared buffer, whichever of the two processes
the scheduler happened to run first would steal the only queued byte and
the other would hang forever -- the boot gate's timeout would catch that
as a missing needle rather than a hang, since only this process is
missing, not the whole machine. */
int main(void)
{
print("consolereader2: start, pid=");
printDec(userGetpid());
print(", blocking on fd 0\n");
char c = 0;
long got = userRead(0, &c, 1);
if (got != 1) {
print("consolereader2: FAIL read\n");
return 1;
}
print("consolereader2: got byte '");
userWrite(1, &c, 1);
print("' -> console-fanout-2-ok\n");
return 0;
}
+5
View File
@@ -2,6 +2,11 @@
.global _start .global _start
_start: _start:
xorq %rbp, %rbp xorq %rbp, %rbp
/* Real x86_64 SysV process entry: rsp points at argc, with argv[0..]
and a NULL terminator right above it (elfBuildInitialStack,
elf.c/syscall.c doExec/doSpawn, kmain.c spawnUser). */
movq (%rsp), %rdi
leaq 8(%rsp), %rsi
call main call main
movq %rax, %rdi movq %rax, %rdi
movq $0, %rax movq $0, %rax
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more