Merge pull request #1 from dualfroz/feat/pending-work-2026-09
feat: land pending September work
This commit is contained in:
176 files changed
+33949
-219
No files matched your search
@@ -3,3 +3,4 @@ os.iso
|
||||
iso/boot/frostbyte.elf
|
||||
*.o
|
||||
*.elf
|
||||
iso/boot/initrd.img
|
||||
@@ -6,7 +6,7 @@ CFLAGS := -std=gnu11 -ffreestanding -nostdlib \
|
||||
-fno-asynchronous-unwind-tables \
|
||||
-mno-red-zone -mno-mmx -mno-sse -mno-sse2 \
|
||||
-mcmodel=kernel \
|
||||
-Wall -Wextra -Iinclude -g -c
|
||||
-Wall -Wextra -Iinclude -MMD -MP -g -c
|
||||
|
||||
ASFLAGS := -ffreestanding -Iinclude -g -c
|
||||
|
||||
@@ -16,7 +16,8 @@ UCFLAGS := -std=gnu11 -ffreestanding -nostdlib \
|
||||
-fno-stack-protector -fno-pic -fno-pie \
|
||||
-fno-asynchronous-unwind-tables \
|
||||
-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
|
||||
|
||||
@@ -33,7 +34,55 @@ OBJS := $(SOBJS) $(COBJS)
|
||||
|
||||
USERCOMMON := $(UBUILD)/crt.o $(UBUILD)/ulib.o
|
||||
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
|
||||
|
||||
@@ -41,6 +90,8 @@ KERNEL := $(BUILD)/frostbyte.elf
|
||||
INITRD := $(BUILD)/initrd.img
|
||||
MKTAR := $(BUILD)/mktar
|
||||
ISO := os.iso
|
||||
DISK := $(BUILD)/disk.img
|
||||
DISKFLAGS := -drive file=$(DISK),format=raw,if=ide
|
||||
|
||||
.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
|
||||
$(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)
|
||||
gcc -O2 -Wall -Wextra -o $@ $<
|
||||
|
||||
@@ -89,6 +564,85 @@ $(INITRD): $(MKTAR) $(USERPROGRAMS) $(FSFILES)
|
||||
--file bin/counter $(UBUILD)/counter.elf \
|
||||
--file bin/sbrktest $(UBUILD)/sbrktest.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/readme.txt fsroot/etc/readme.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
|
||||
grub-mkrescue -o $(ISO) iso 2>/dev/null
|
||||
|
||||
run: $(ISO)
|
||||
qemu-system-x86_64 -cdrom $(ISO) -serial stdio -m 128M
|
||||
# Scratch disk backing /dev/disk0 (ata.c / vfs.c blockDev*). Zeroed, not
|
||||
# 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)
|
||||
qemu-system-x86_64 -cdrom $(ISO) -serial stdio -display none -m 128M -no-reboot
|
||||
run: $(ISO) $(DISK)
|
||||
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
|
||||
|
||||
clean:
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -33,4 +33,60 @@ typedef struct Elf64ProgramHeader {
|
||||
|
||||
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
|
||||
+131
@@ -14,13 +14,144 @@
|
||||
|
||||
#define fsTypeFile 1
|
||||
#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
|
||||
|
||||
/* 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 {
|
||||
unsigned long size;
|
||||
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;
|
||||
/* 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;
|
||||
|
||||
typedef struct FbDirent {
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -4,8 +4,160 @@
|
||||
#include "types.h"
|
||||
|
||||
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);
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -7,8 +7,82 @@
|
||||
#define pageWrite 0x2
|
||||
#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);
|
||||
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);
|
||||
|
||||
/* 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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -3,13 +3,78 @@
|
||||
|
||||
#include "types.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 {
|
||||
threadUnused = 0,
|
||||
threadRunnable = 1,
|
||||
threadRunning = 2,
|
||||
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;
|
||||
|
||||
typedef void (*ThreadEntry)(void);
|
||||
@@ -19,24 +84,663 @@ typedef struct Thread {
|
||||
uint64 stackBase;
|
||||
uint64 kernelStackTop;
|
||||
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 vruntime;
|
||||
uint64 exitCode;
|
||||
uint32 weight;
|
||||
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;
|
||||
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];
|
||||
/* 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;
|
||||
|
||||
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 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
|
||||
uint64 schedule(uint64 currentRsp);
|
||||
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);
|
||||
|
||||
/* 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 threadSetBrk(uint32 id, uint64 brk);
|
||||
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);
|
||||
Thread* threadAt(uint32 index);
|
||||
Thread* currentThread(void);
|
||||
|
||||
+226
@@ -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
@@ -1,6 +1,276 @@
|
||||
#ifndef FROSTBYTE_SHELL_H
|
||||
#define FROSTBYTE_SHELL_H
|
||||
|
||||
#include "types.h"
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -15,5 +15,59 @@
|
||||
#define sysGetdents 11
|
||||
#define sysMkdir 12
|
||||
#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
|
||||
@@ -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
@@ -9,14 +9,36 @@ struct VfsNode;
|
||||
typedef enum {
|
||||
fdFree = 0,
|
||||
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;
|
||||
|
||||
/* 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 {
|
||||
struct VfsNode* node;
|
||||
uint64 offset;
|
||||
uint32 kind;
|
||||
uint32 flags;
|
||||
int32 pipeHandle;
|
||||
} FileDescriptor;
|
||||
|
||||
#define maxOpenFiles 16
|
||||
@@ -35,6 +57,239 @@ int vfsStat(const char* path, FbStat* out);
|
||||
int64 vfsGetdents(int fd, void* buffer, uint64 length);
|
||||
int vfsMkdir(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;
|
||||
VfsNode* vfsLookup(const char* path);
|
||||
@@ -44,6 +299,43 @@ const char* vfsName(VfsNode* node);
|
||||
bool vfsIsDir(VfsNode* node);
|
||||
uint64 vfsSize(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);
|
||||
|
||||
#endif
|
||||
Binary file not shown.
@@ -19,6 +19,7 @@ SECTIONS
|
||||
{
|
||||
*(.multiboot)
|
||||
*(.boot.text)
|
||||
*(.boot.data)
|
||||
. = ALIGN(4096);
|
||||
*(.boot.pagetables)
|
||||
} :boot
|
||||
@@ -33,6 +34,10 @@ SECTIONS
|
||||
.rodata ALIGN(4096) : AT(ADDR(.rodata) - KERNEL_VMA)
|
||||
{
|
||||
*(.rodata .rodata.*)
|
||||
. = ALIGN(8);
|
||||
ktestArrayStart = .;
|
||||
KEEP(*(.ktests .ktests.*))
|
||||
ktestArrayEnd = .;
|
||||
} :rodata
|
||||
|
||||
.data ALIGN(4096) : AT(ADDR(.data) - KERNEL_VMA)
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -60,6 +60,10 @@ fillPd:
|
||||
movl $0xC0000080, %ecx
|
||||
rdmsr
|
||||
orl $(1 << 8), %eax
|
||||
cmpl $0, cpuNxSupported
|
||||
je 2f
|
||||
orl $(1 << 11), %eax
|
||||
2:
|
||||
wrmsr
|
||||
|
||||
movl %cr0, %eax
|
||||
@@ -76,6 +80,16 @@ checkLongMode:
|
||||
jb noLongMode
|
||||
movl $0x80000001, %eax
|
||||
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
|
||||
jz noLongMode
|
||||
ret
|
||||
@@ -111,6 +125,12 @@ bootGdtPointer:
|
||||
.word bootGdtEnd - bootGdtStart - 1
|
||||
.quad bootGdtStart
|
||||
|
||||
.section .boot.data, "aw", @progbits
|
||||
.align 4
|
||||
.global cpuNxSupported
|
||||
cpuNxSupported:
|
||||
.long 0
|
||||
|
||||
.section .boot.pagetables, "aw", @progbits
|
||||
.align 4096
|
||||
.global bootPml4
|
||||
|
||||
+12
-4
@@ -43,13 +43,21 @@ static void writeUnsigned(uint64 value, uint32 base, bool upper, int minWidth, c
|
||||
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) {
|
||||
kputchar('-');
|
||||
writeUnsigned((uint64)(-value), 10, false, 0, ' ');
|
||||
writeUnsigned((uint64)(-value), 10, false, minWidth > 0 ? minWidth - 1 : 0, pad);
|
||||
} else {
|
||||
writeUnsigned((uint64)value, 10, false, 0, ' ');
|
||||
writeUnsigned((uint64)value, 10, false, minWidth, pad);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +101,7 @@ void kprintf(const char* format, ...)
|
||||
break;
|
||||
case 'd':
|
||||
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;
|
||||
case 'u':
|
||||
writeUnsigned(longCount ? va_arg(args, uint64) : (uint64)va_arg(args, uint32), 10, false, width, pad);
|
||||
|
||||
+820
@@ -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);
|
||||
}
|
||||
@@ -7,6 +7,15 @@
|
||||
#define elfTypeExec 2
|
||||
#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)
|
||||
{
|
||||
if (header->ident[0] != 0x7F || header->ident[1] != 'E' ||
|
||||
@@ -21,6 +30,26 @@ static bool validHeader(const Elf64Header* header)
|
||||
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)
|
||||
{
|
||||
const Elf64Header* header = (const Elf64Header*)image;
|
||||
@@ -39,7 +68,8 @@ uint64 elfLoad(const void* image, uint64 imageSize)
|
||||
if (segment->type != programTypeLoad)
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
@@ -49,3 +79,61 @@ uint64 elfLoad(const void* image, uint64 imageSize)
|
||||
|
||||
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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "keyboard.h"
|
||||
#include "sched.h"
|
||||
#include "ports.h"
|
||||
#include "signal.h"
|
||||
#include "mmap.h"
|
||||
|
||||
static const char* exceptionNames[32] = {
|
||||
"divide by zero",
|
||||
@@ -52,24 +54,136 @@ static void panicOnException(Registers* regs)
|
||||
__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)
|
||||
{
|
||||
/* 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)
|
||||
panicOnException(regs);
|
||||
|
||||
if (regs->intNo == 128)
|
||||
return syscallDispatch(regs);
|
||||
uint64 nextRsp;
|
||||
|
||||
uint8 irq = (uint8)(regs->intNo - 32);
|
||||
uint64 nextRsp = (uint64)regs;
|
||||
if (regs->intNo == 128) {
|
||||
nextRsp = syscallDispatch(regs);
|
||||
} else {
|
||||
uint8 irq = (uint8)(regs->intNo - 32);
|
||||
nextRsp = (uint64)regs;
|
||||
|
||||
if (irq == 0) {
|
||||
pitTick();
|
||||
nextRsp = schedule((uint64)regs);
|
||||
} else if (irq == 1) {
|
||||
keyboardHandler();
|
||||
if (irq == 0) {
|
||||
pitTick();
|
||||
nextRsp = schedule((uint64)regs);
|
||||
} else if (irq == 1) {
|
||||
keyboardHandler();
|
||||
}
|
||||
|
||||
picSendEoi(irq);
|
||||
}
|
||||
|
||||
picSendEoi(irq);
|
||||
return nextRsp;
|
||||
return deliverPendingSignals(nextRsp);
|
||||
}
|
||||
+429
@@ -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
@@ -1,8 +1,26 @@
|
||||
#include "keyboard.h"
|
||||
#include "ports.h"
|
||||
#include "ring.h"
|
||||
#include "sched.h"
|
||||
#include "signal.h"
|
||||
|
||||
#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] = {
|
||||
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',
|
||||
@@ -19,32 +37,138 @@ static const char scancodeUpper[128] = {
|
||||
'*', 0, ' ', 0
|
||||
};
|
||||
|
||||
#define keyBufferSize 128
|
||||
static volatile char keyBuffer[keyBufferSize];
|
||||
static volatile uint32 bufferHead;
|
||||
static volatile uint32 bufferTail;
|
||||
static bool shiftHeld;
|
||||
|
||||
#define consoleReaderBufferSize 32
|
||||
|
||||
typedef struct ConsoleReader {
|
||||
RingBuffer ring;
|
||||
uint8 backing[consoleReaderBufferSize];
|
||||
bool used;
|
||||
} ConsoleReader;
|
||||
|
||||
static ConsoleReader consoleReaders[maxConsoleReaders];
|
||||
|
||||
void keyboardInit(void)
|
||||
{
|
||||
bufferHead = 0;
|
||||
bufferTail = 0;
|
||||
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;
|
||||
if (next != bufferTail) {
|
||||
keyBuffer[bufferHead] = c;
|
||||
bufferHead = next;
|
||||
foregroundPgid = pgid;
|
||||
}
|
||||
|
||||
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) {
|
||||
shiftHeld = true;
|
||||
return;
|
||||
@@ -57,21 +181,39 @@ void keyboardHandler(void)
|
||||
return;
|
||||
|
||||
char c = shiftHeld ? scancodeUpper[scancode] : scancodeLower[scancode];
|
||||
if (c)
|
||||
pushChar(c);
|
||||
if (!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;
|
||||
}
|
||||
|
||||
char keyboardReadChar(void)
|
||||
{
|
||||
while (bufferHead == bufferTail)
|
||||
__asm__ volatile("sti; hlt");
|
||||
|
||||
char c = keyBuffer[bufferTail];
|
||||
bufferTail = (bufferTail + 1) % keyBufferSize;
|
||||
return c;
|
||||
uint8 scancode = inb(keyboardData);
|
||||
keyboardHandleScancode(scancode);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+59
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -6,6 +6,20 @@
|
||||
#define entryPageSize 0x80
|
||||
#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)
|
||||
{
|
||||
uint64 value;
|
||||
@@ -65,11 +79,72 @@ bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags)
|
||||
if (!pt)
|
||||
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);
|
||||
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 pml4Index = (virtualAddr >> 39) & 0x1FF;
|
||||
@@ -95,7 +170,7 @@ uint64 pagingResolve(uint64 virtualAddr)
|
||||
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 last = (virtualAddr + size + frameSize - 1) & ~(uint64)(frameSize - 1);
|
||||
@@ -106,9 +181,92 @@ bool pagingMapUserRange(uint64 virtualAddr, uint64 size)
|
||||
uint64 frame = pmmAlloc();
|
||||
if (frame == 0)
|
||||
return false;
|
||||
if (!pagingMap(page, frame, pageUser | pageWrite))
|
||||
if (!pagingMap(page, frame, pageUser | flags))
|
||||
return false;
|
||||
memset((void*)page, 0, frameSize);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -96,9 +96,30 @@ void pmmInit(uint64 multibootInfo, uint64 protectedEnd)
|
||||
|
||||
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;
|
||||
if (protectedEnd > bitmapAnchor)
|
||||
bitmapAnchor = protectedEnd;
|
||||
if (endAddr > bitmapAnchor)
|
||||
bitmapAnchor = endAddr;
|
||||
bitmap = (uint8*)alignUp(bitmapAnchor, frameSize);
|
||||
uint64 bitmapBytes = (frameCount + 7) / 8;
|
||||
memset(bitmap, 0xFF, bitmapBytes);
|
||||
|
||||
+146
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+1655
File diff suppressed because it is too large.
Load diff
+1357
File diff suppressed because it is too large.
Load diff
+1529
-72
File diff suppressed because it is too large.
Load diff
+812
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -5,10 +5,24 @@
|
||||
#include "vfs.h"
|
||||
#include "fs.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 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)
|
||||
{
|
||||
if (pointer < userSpaceMin)
|
||||
@@ -34,6 +48,45 @@ static bool copyUserPath(uint64 pointer, char* dst, uint64 max)
|
||||
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)
|
||||
{
|
||||
if (!userRangeOk(buffer, length))
|
||||
@@ -48,15 +101,222 @@ static int64 doRead(uint64 fd, uint64 buffer, uint64 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)
|
||||
{
|
||||
/* 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) {
|
||||
case sysWrite:
|
||||
regs->rax = (uint64)doWrite(regs->rdi, regs->rsi, regs->rdx);
|
||||
case sysWrite: {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
case sysGetpid:
|
||||
regs->rax = currentThreadId();
|
||||
return (uint64)regs;
|
||||
@@ -114,6 +374,68 @@ uint64 syscallDispatch(Registers* regs)
|
||||
regs->rax = (uint64)(int64)vfsMkdir(path);
|
||||
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: {
|
||||
char path[256];
|
||||
if (!copyUserPath(regs->rdi, path, sizeof(path)))
|
||||
@@ -122,6 +444,526 @@ uint64 syscallDispatch(Registers* regs)
|
||||
regs->rax = (uint64)(int64)vfsUnlink(path);
|
||||
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 = ¤tThread()->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:
|
||||
regs->rax = (uint64)-1;
|
||||
return (uint64)regs;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -1,19 +1,27 @@
|
||||
#!/bin/sh
|
||||
# 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
|
||||
# file. Exits non-zero when any expected string is missing.
|
||||
# ring-3 program read a known ramdisk (USTAR) file, round-tripped a tmpfs
|
||||
# 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")
|
||||
root=$(cd "$here/.." && pwd)
|
||||
iso="$root/os.iso"
|
||||
disk="$root/build/disk.img"
|
||||
|
||||
if [ ! -f "$iso" ]; then
|
||||
echo "ci-test: $iso not found, build it first (make)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f "$disk" ]; then
|
||||
mkdir -p "$root/build"
|
||||
dd if=/dev/zero of="$disk" bs=1M count=8 status=none
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
echo "----- serial log (tail) -----"
|
||||
@@ -24,7 +32,301 @@ status=0
|
||||
for needle in \
|
||||
"frostbyte-ustar-read-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
|
||||
if grep -qF "$needle" "$log"; then
|
||||
echo "PASS: found '$needle'"
|
||||
@@ -34,10 +336,32 @@ do
|
||||
fi
|
||||
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"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "ci-test: all filesystem checks passed"
|
||||
echo "ci-test: all boot and self-test checks passed"
|
||||
else
|
||||
echo "ci-test: filesystem checks FAILED"
|
||||
echo "ci-test: checks FAILED"
|
||||
fi
|
||||
exit $status
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
.global _start
|
||||
_start:
|
||||
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
|
||||
movq %rax, %rdi
|
||||
movq $0, %rax
|
||||
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user