diff --git a/examples/typephp-os/.gitignore b/examples/typephp-os/.gitignore index 884faf23..f6f12142 100644 --- a/examples/typephp-os/.gitignore +++ b/examples/typephp-os/.gitignore @@ -1,3 +1,3 @@ /build/ *.o -/thirdparty \ No newline at end of file +/thirdparty/ diff --git a/examples/typephp-os/Makefile b/examples/typephp-os/Makefile index 47ef2009..4240f006 100644 --- a/examples/typephp-os/Makefile +++ b/examples/typephp-os/Makefile @@ -6,34 +6,42 @@ PAYLOAD_BIN := $(BUILD)/kernel64.bin PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o KERNEL := $(BUILD)/typephp-os.elf DISK := $(BUILD)/typephp-os.img -USER_COMMANDS := ls cd date pwd fault +USER_COMMANDS := ls cd date pwd cat echo write touch mkdir rm rmdir mv fault vmfault wrfault memtest USER_PROGRAMS := sh $(USER_COMMANDS) USER_SH_ELF := $(BUILD)/sh.elf USER_COMMAND_ELFS := $(addprefix $(BUILD)/,$(addsuffix .elf,$(USER_COMMANDS))) -USER_IMAGES := $(addprefix $(OBJECT_DIR)/,$(addsuffix -image.o,$(USER_PROGRAMS))) +USER_CRT0_OBJECT := $(OBJECT_DIR)/user-crt0.o +USER_LIBC_OBJECTS := $(OBJECT_DIR)/user-libc.o $(OBJECT_DIR)/user-syscall.o BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \ -fno-stack-protector -Wall -Wextra USER_CFLAGS := -m64 -O2 -ffreestanding -fno-builtin -fno-pic -fno-pie \ - -fno-stack-protector -mno-red-zone -Wall -Wextra -Iuser -Ifreestanding/abi + -fno-stack-protector -mno-red-zone -Wall -Wextra -Iuser/include -Ifreestanding/abi USER_LDFLAGS := -m elf_x86_64 -nostdlib -static -T user/linker.ld -.PHONY: all payload objects disk run test clean +.PHONY: all thirdparty payload objects disk run test clean all: $(KERNEL) $(DISK) -payload: objects +thirdparty: + ./tools/fetch-thirdparty.sh + +payload: thirdparty objects cd ../.. && ./bin/tpc.php --nano examples/typephp-os/project.yml # Only the 32-bit bootstrap requires flags incompatible with the 64-bit Nano # payload. Ordinary .c/.cc/.S files stay in project.yml sources. -objects: $(BOOTSTRAP_OBJECT) $(USER_IMAGES) +objects: $(BOOTSTRAP_OBJECT) disk: $(DISK) -$(DISK): | $(BUILD) +$(DISK): $(USER_SH_ELF) $(USER_COMMAND_ELFS) | $(BUILD) truncate -s 32M $@ mkfs.fat -F 16 -n TYPEPHPOS $@ + mmd -i $@ ::/BIN + for program in $(USER_PROGRAMS); do \ + mcopy -o -i $@ $(BUILD)/$$program.elf ::/BIN/$$program.elf; \ + done $(BUILD): mkdir -p $@ @@ -44,20 +52,23 @@ $(OBJECT_DIR): $(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR) gcc $(BOOTFLAGS) -c $< -o $@ -$(OBJECT_DIR)/%.o: user/%.c user/syscall.h freestanding/abi/typephp_os_syscall.h | $(OBJECT_DIR) +$(OBJECT_DIR)/%.o: user/%.c freestanding/abi/typephp_os_syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ + +$(USER_CRT0_OBJECT): user/crt0.S freestanding/abi/typephp_os_syscall.h | $(OBJECT_DIR) gcc $(USER_CFLAGS) -c $< -o $@ -$(USER_SH_ELF): $(OBJECT_DIR)/sh.o user/linker.ld | $(BUILD) - ld $(USER_LDFLAGS) --defsym USER_BASE=0x2000000 -o $@ $< +$(OBJECT_DIR)/user-libc.o: user/libc.c freestanding/abi/typephp_os_syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ + +$(OBJECT_DIR)/user-syscall.o: user/syscall.S freestanding/abi/typephp_os_syscall.h | $(OBJECT_DIR) + gcc $(USER_CFLAGS) -c $< -o $@ -$(USER_COMMAND_ELFS): $(BUILD)/%.elf: $(OBJECT_DIR)/%.o user/linker.ld | $(BUILD) - ld $(USER_LDFLAGS) --defsym USER_BASE=0x2100000 -o $@ $< +$(USER_SH_ELF): $(OBJECT_DIR)/sh.o $(USER_CRT0_OBJECT) $(USER_LIBC_OBJECTS) user/linker.ld | $(BUILD) + ld $(USER_LDFLAGS) --defsym USER_BASE=0x2000000 -o $@ $(filter %.o,$^) -$(OBJECT_DIR)/%-image.o: $(BUILD)/%.elf | $(OBJECT_DIR) - objcopy -I binary -O elf64-x86-64 -B i386:x86-64 \ - --rename-section .data=.rodata.user_elf,alloc,load,readonly,data,contents \ - --redefine-sym _binary_build_$*_elf_start=typephp_user_$*_elf_start \ - --redefine-sym _binary_build_$*_elf_end=typephp_user_$*_elf_end $< $@ +$(USER_COMMAND_ELFS): $(BUILD)/%.elf: $(OBJECT_DIR)/%.o $(USER_CRT0_OBJECT) $(USER_LIBC_OBJECTS) user/linker.ld | $(BUILD) + ld $(USER_LDFLAGS) --defsym USER_BASE=0x2100000 -o $@ $(filter %.o,$^) $(KERNEL): payload objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN) @@ -72,7 +83,22 @@ run: all -drive file=$(DISK),format=raw,if=ide,index=0 \ -display none -serial stdio -monitor none -no-reboot -no-shutdown -test: all +test: $(KERNEL) $(USER_SH_ELF) $(USER_COMMAND_ELFS) + truncate -s 32M $(DISK) + mkfs.fat -F 16 -n TYPEPHPOS $(DISK) + mmd -i $(DISK) ::/BIN + for program in $(USER_PROGRAMS); do \ + mcopy -o -i $(DISK) $(BUILD)/$$program.elf ::/BIN/$$program.elf; \ + done + # An alias not known while linking the kernel verifies disk-based discovery. + mcopy -o -i $(DISK) $(BUILD)/echo.elf ::/BIN/hello.elf + mcopy -o -i $(DISK) tools/invalid-elf.txt ::/BIN/bad.elf + # Fill one directory cluster (dot entries plus 62 files). The QEMU test + # creates F62.TXT and therefore exercises directory-chain growth once. + mmd -i $(DISK) ::/EXPAND + for index in $$(seq -w 0 61); do \ + mcopy -o -i $(DISK) tools/invalid-elf.txt ::/EXPAND/F$$index.TXT; \ + done bash tools/test-qemu.sh $(KERNEL) $(DISK) $(BUILD)/qemu.log clean: diff --git a/examples/typephp-os/README.md b/examples/typephp-os/README.md index dcd2ff59..3fb5f8ac 100644 --- a/examples/typephp-os/README.md +++ b/examples/typephp-os/README.md @@ -6,9 +6,9 @@ PHP, libc, or libstdc++. TypePHP implements the startup self-check, a custom Zend class, the prime-number demo, and filesystem services. Small C and assembly layers provide the machine bootstrap, current kernel services, and the temporary freestanding userspace programs. -The first userspace programs (`sh`, `ls`, `cd`, `pwd`, and `date`) are -deliberately written in freestanding C until tpc can target this small -userspace ABI. +The userspace shell and commands are deliberately written in freestanding C +until tpc can target this small userspace ABI. They already use standard +`main()`, libc-shaped APIs, and a Linux-style process-entry stack. The architectural rules and staged plan are maintained in [ROADMAP.md](ROADMAP.md). @@ -28,6 +28,8 @@ C/POSIX and C++ ABI functions live there, while APIs required for linking but not implemented by the kernel are exported as panic stubs. Consequently an unsupported operation fails immediately with its ABI symbol instead of silently returning fabricated data. Sockets are outside the current scope. +On a fresh checkout, run `examples/typephp-os/tools/fetch-thirdparty.sh` before +invoking tpc directly; `make payload` performs both steps automatically. Cross-project portability uses general feature switches only: @@ -48,7 +50,11 @@ only excluded capability is dynamic PHP execution through ZendVM. ## Build and run Required host tools are TypePHP's PHP/Composer dependencies, GCC/G++, GNU -binutils, GNU make, `dosfstools`, and `qemu-system-x86_64`. +binutils, GNU make, `dosfstools`, `mtools`, and `qemu-system-x86_64`. +Third-party source is not committed. `make` invokes +[`tools/fetch-thirdparty.sh`](tools/fetch-thirdparty.sh), which downloads fixed +archives and verifies their SHA-256 checksums according to +[`THIRDPARTY.md`](THIRDPARTY.md). From this directory, build and boot with: @@ -62,9 +68,10 @@ The build produces these useful files: - `build/kernel64.elf`: the 64-bit TypePHP + ordinary Nano payload produced by tpc; - `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU; -- `build/typephp-os.img`: a persistent 32 MiB FAT16 disk image. -- `build/sh.elf`, `build/ls.elf`, `build/cd.elf`, `build/pwd.elf`, and - `build/date.elf`: independent ELF64 Ring-3 programs built without libc. +- `build/typephp-os.img`: a persistent 32 MiB FAT16 disk image containing the + independently built shell and command executables; +- `build/sh.elf` and the command ELF files: independent ELF64 Ring-3 programs + linked with the small userspace bootstrap libc rather than a hosted libc. Run the automated serial-output smoke test with: @@ -74,12 +81,12 @@ make test The Makefile compiles the 32-bit Multiboot bootstrap externally because its `-m32` ABI cannot participate in the 64-bit payload link. It also builds the -five deliberately small freestanding C userspace ELF files. All ordinary +deliberately small freestanding C userspace ELF files and installs them into +the FAT16 image with `mcopy`. All ordinary 64-bit `.c`, `.cc`, and `.S` files remain in `project.yml` and use tpc's generic -`c-flags`, `cxx-flags`, and `asm-flags`. Generic same-ABI prebuilt objects can -be supplied with `objects`; this is how read-only copies of the user ELF files -are embedded for the kernel ELF loader. The architecture-changing bootstrap is -instead combined during the final packaging link. +`c-flags`, `cxx-flags`, and `asm-flags`. The user executables are not linked +into the kernel payload. The architecture-changing bootstrap is combined +during the final packaging link. After tpc emits `kernel64.elf`, the Makefile uses `objcopy` to turn the payload into a raw binary and then an ELF32 data object. GNU ld combines that object @@ -101,10 +108,12 @@ Press `Ctrl+C` to leave headless QEMU. ## Current capabilities The bootstrap passes the Multiboot memory map to the 64-bit kernel. The -physical layer reserves the complete kernel image, selects mapped usable RAM, -and installs the remaining arena behind the project-owned `posix_memalign()`. -Upstream `zend_alloc` then obtains and subdivides aligned 2 MiB chunks. C++ -global `new` and `delete`, including `std::vector` allocations, use Zend MM. +physical layer reserves the complete kernel image and divides the selected +usable region into two pools. Upstream `zend_alloc` obtains aligned 2 MiB +chunks from one pool through the project-owned `posix_memalign()`; the other +pool is a recyclable 4 KiB physical-page allocator used by page tables, ELF +images, stacks, `brk()`, and `mmap()`. C++ global `new` and `delete`, including +`std::vector` allocations, continue to use Zend MM. The QEMU smoke test currently verifies: @@ -113,42 +122,78 @@ The QEMU smoke test currently verifies: - PHPX `Variant`, `Str`, `Array`, custom classes, and a typed `std::vector`; - OpenLibm implementations of the ordinary double-precision math ABI; -- built-in date handling and `sleep()` through Zend Bridge; -- ATA PIO sector I/O and a TypePHP FAT16 implementation with 8.3 root files - and directories; +- RTC-derived UTC time exposed to userspace through `time()`; +- ATA PIO sector I/O, a fixed 128-sector LRU read/write-through cache, and a + TypePHP FAT16 implementation with DOS 8.3 files, nested traversal and + mutation, including automatic directory-chain growth; - PHP's unchanged plain file stream and `php_stat()` paths, including `file_put_contents()`, `file_get_contents()`, `is_dir()`, `mkdir()`, and `scandir()`, forwarded through the POSIX ABI to TypePHP; - normal PHP output through multi-argument `echo` and `PHPWRITE`; - a TypePHP prime calculation for 0–100; -- a resident loop that prints the RTC-derived UTC time and - `Hello TypePHP-OS!` every two seconds. - -The 64-bit payload currently occupies about 7 MiB, so the first 16 MiB is -reserved before physical memory is handed to Zend MM. Returning entire Zend -chunks to a future physical page allocator remains later work. - -The first filesystem milestone deliberately supports only the FAT16 root -directory and DOS 8.3 names. Nested path traversal, long filenames, -timestamps, permissions, and a general block-device layer remain future work. +- disk-backed ELF64 loading, Linux-style `argc/argv` startup, and libc-shaped + userspace calls; +- a separate x86-64 address space for the resident shell and every command, + 4 KiB user mappings, a guarded 64 KiB stack, NX/WP enforcement, and ELF + `RX`/`RW` segment permissions; +- Linux-numbered `brk`, anonymous private `mmap`, `mprotect`, and `munmap` + calls backed by recyclable physical pages; +- dynamic command discovery and safe rejection of malformed ELF files; +- recovery from invalid opcodes, cross-address-space reads, and writes to + read-only mappings without losing the resident shell; +- user-mode file creation, reading, writing, seeking, closing, removal, and + nested directory creation/removal through the TypePHP FAT16 implementation. + +The 64-bit payload currently occupies about 7 MiB. The first 40 MiB is kept +away from both allocators, covering the kernel and the current 32–36 MiB user +virtual-address window. Entire Zend chunks are not yet returned to the +physical-page pool. + +The current filesystem deliberately supports only DOS 8.3 names. Reading, +directory enumeration, `stat`, executable loading, and file/directory mutation +traverse nested FAT16 directory chains. Full directories grow by linking a new +cluster. The current ATA cache is deliberately small, synchronous, and +write-through; it is not yet a general virtual-filesystem page cache. Rename +currently stays within one parent directory; cross-directory rename, +replacement semantics, long filenames, timestamps, permissions, and a general +block-device layer remain future work. Network sockets, dynamic module loading, `include`/`require`/`eval`, and PHP APIs that execute host commands remain unavailable. ## Single-task userspace -After the TypePHP self-check, the kernel validates and loads `sh.elf`, installs -a 64-bit TSS and an IDT gate, and enters Ring 3 with `iretq`. The user pages at -32-36 MiB are marked user-accessible while kernel pages remain supervisor-only. -An `int 0x80` boundary currently provides synchronous `read`, `write`, `exec`, -`exit`, `getcwd`, `chdir`, `time`, and directory-list operations. Standard +After the TypePHP self-check, the kernel opens `/BIN/SH.ELF` from FAT16, +validates and loads its `PT_LOAD` segments, installs a 64-bit TSS and an IDT +gate, and enters Ring 3 with `iretq`. The resident shell and each transient +command receive an independent CR3. Their 32–36 MiB virtual window is composed +from recyclable 4 KiB pages; the shared identity-mapped kernel remains +supervisor-only. The CPU has write protection and no-execute enabled, and the +loader applies the final ELF `PF_W` and `PF_X` permissions after copying each +segment. +An `int 0x80` boundary currently provides synchronous `read`, `write`, `close`, +`lseek`, `openat`, `exit`, `getcwd`, `chdir`, `mkdir`, `rmdir`, `unlink`, +`time`, `brk`, anonymous private `mmap`, `mprotect`, `munmap`, and private +spawn, directory-list, and same-directory rename operations. Standard input and output are backed by QEMU's COM1 serial console. Syscall numbers are shared by the kernel and userspace through `typephp_os_syscall.h`. -The shell synchronously executes separate `ls.elf`, `cd.elf`, `pwd.elf`, and -`date.elf` images. Short-lived commands reuse one ELF load address; only the -resident shell has a separate image and stack. Only one user context runs at a -time: while a command is active, the kernel keeps the shell register frame and -restores it when the command calls `exit`. +Userspace programs now expose standard C `main(argc, argv)` functions. A shared +`crt0.S` consumes a Linux-style initial stack containing `argc`, `argv`, an +empty environment, and a terminating auxiliary-vector entry. The normative +userspace compatibility rules are recorded in [`user/README.md`](user/README.md): +the long-term target is GCC/glibc-compatible userspace, and Linux syscall +numbers are reserved for genuinely compatible semantics. + +The shell synchronously executes separate command ELF files from FAT16. +Entering `name` requests `/BIN/.ELF`; the kernel validates the ELF header, +program table, file bounds, target address range, and entry point before +loading it. There is no compiled-in command whitelist, so a compatible command +can be installed in the disk image without relinking the kernel. Commands may +reuse the same virtual addresses, but never the shell's mappings: every launch +creates and later destroys an independent address space and stack. A page-count +invariant detects leaked command pages on both normal exit and faults. Only one +user context runs at a time: while a command is active, the kernel keeps the +shell register frame and restores it when the command calls `exit`. Because this is a deliberately single-task model, the working directory is a session-global property; a successful `cd` therefore remains visible after control returns to the shell. @@ -159,12 +204,32 @@ Available commands are: ls date pwd +echo Hello TypePHP +cat HELLO.TXT +write NOTE.TXT Hello from Ring 3 +cat NOTE.TXT +touch EMPTY.TXT +mkdir TMP +rm NOTE.TXT +rmdir TMP +mv OLD.TXT NEW.TXT cd DOCS pwd ls cd .. ``` -The current FAT16 limitation still applies: a root child such as `/DOCS` is a -valid working directory, but nested directory storage is not implemented, so -its listing currently contains only `.` and `..`. +The development-only `fault`, `vmfault`, and `wrfault` commands exercise an +invalid opcode, an attempted read from the shell's virtual address, and an +attempted write to a read-only `mmap()` page. The kernel reports each Ring-3 +exception, destroys the faulty address space, and restores the shell. The +`memtest` command exercises `sbrk()`, anonymous mapping, protection changes, +and unmapping. User exceptions for divide errors, breakpoints, bounds, invalid +opcodes, invalid TSS/segments, stack faults, general-protection faults, and +page faults have IDT entries. A fault raised in Ring 0 still causes an +immediate kernel panic. + +Executables now live in the conventional `/BIN` directory and both `ls /BIN` +and `cd /BIN` use TypePHP's FAT16 cluster-chain traversal. The same path +resolution is used by `cat`, `write`, `touch`, `mkdir`, `rm`, and `rmdir`, so +nested directory trees are mutable from userspace. diff --git a/examples/typephp-os/ROADMAP.md b/examples/typephp-os/ROADMAP.md index 2effd324..ffdb61a6 100644 --- a/examples/typephp-os/ROADMAP.md +++ b/examples/typephp-os/ROADMAP.md @@ -36,20 +36,44 @@ behavior. classes, generated user classes, construction, properties, and destruction. 4. **Time service — complete.** CMOS wall clock, monotonic host hook, built-in date formatting, and the standard extension's `sleep()` path. -5. **Filesystem — first slice complete.** ATA PIO, a persistent FAT16 image, +5. **Filesystem — fourth slice complete.** ATA PIO, a persistent FAT16 image, TypePHP-owned cluster/directory/file logic, file descriptors, POSIX - directory/stat operations, and PHP's local file stream API. The current - implementation is intentionally limited to root-level DOS 8.3 names; - nested directories and long filenames are next. -6. **Single-task userspace — first slice complete.** ELF64 validation/loading, - supervisor/user page separation, GDT/TSS, Ring-3 entry, synchronous + directory/stat operations, and PHP's local file stream API. DOS 8.3 path + lookup, file reads, directory enumeration, file/directory mutation, and + same-parent rename traverse nested cluster chains. Directories grow by + allocating and linking additional clusters. A fixed 128-sector LRU + read/write-through cache now avoids repeated ATA PIO reads while preserving + synchronous persistence. A general VFS page cache, cross-directory rename, + replacement semantics, and long filenames are next. +6. **Single-task userspace — third slice complete.** Disk-backed ELF64 + validation/loading from FAT16, + GDT/TSS, Ring-3 entry, synchronous `int 0x80` system calls, COM1 standard I/O, saved parent context, shared - syscall definitions, reusable command address space, and independent - freestanding C `sh`, `ls`, `cd`, `pwd`, and `date` programs. + syscall definitions, complete `argv[]` + delivery, and independent freestanding C shell/command programs. The first + Linux-compatible file syscalls cover `openat`, `read`, `write`, `lseek`, + `close`, `mkdir`, `rmdir`, and `unlink`; `cat`, `write`, `touch`, and the + directory commands exercise persistent FAT16 changes. The `mv` command + exposes same-parent rename through a private syscall until full Linux + rename semantics are implemented. The resident shell + and every command are independent files under `/BIN` rather than byte arrays embedded in + the kernel, and command discovery no longer uses a compiled-in whitelist. + Each process now has its own CR3 and recyclable 4 KiB physical pages. ELF + segments retain `RX`/`RW` permissions, stacks have an unmapped guard, and + NX plus supervisor write protection are enabled. Linux-compatible `brk`, + anonymous private `mmap`, `mprotect`, and `munmap` provide the first real + userspace memory-management ABI. Ring-3 CPU exceptions terminate a + short-lived command and restore the saved shell; `fault.elf`, `vmfault.elf`, + and `wrfault.elf` cover invalid-opcode, isolation, and write-protection + recovery, while page accounting checks reclamation. The + programs use standard C `main(argc, argv)` behind a shared crt0 and receive + a Linux-style initial stack; syscall evolution follows the glibc migration + rules in `user/README.md`. 7. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing of PHPX fields under sustained allocation. -8. **Kernel services.** Interrupt-driven timer, keyboard, physical-page - reclamation, and a capability-oriented native API. +8. **Kernel services.** Interrupt-driven timer and keyboard, higher-level VM + region management, Zend-chunk reclamation, and a capability-oriented native + API. 9. **Packaging and CI.** Automate the two-stage ELF32/ELF64 build and QEMU boot smoke test in GitHub Actions. diff --git a/examples/typephp-os/THIRDPARTY.md b/examples/typephp-os/THIRDPARTY.md new file mode 100644 index 00000000..60f526fd --- /dev/null +++ b/examples/typephp-os/THIRDPARTY.md @@ -0,0 +1,34 @@ +# Third-party source policy + +The generated `thirdparty/` directory is intentionally ignored by Git. Every +third-party source dependency must instead be declared by a script under +`tools/` with all of the following information: + +- an immutable upstream version or commit; +- the canonical download URL; +- a SHA-256 checksum for the downloaded archive; +- an explicit list of files copied into the build tree; +- the upstream license file. + +Run the common fetch entry point before compiling: + +```shell +./tools/fetch-thirdparty.sh +``` + +`make` invokes this command automatically. A matching local version marker +makes repeated invocations offline and effectively free. Adding another +library must extend this same entry point or call a dedicated, equivalently +strict fetch script from it; third-party source must not be committed directly. + +## OpenLibm + +- Upstream: +- Version: `v0.8.7` +- Commit: `9fbeafcd4f1b6ef6aa3946c1c8faead50f38a94d` +- Archive SHA-256: `e328a1d59b94748b111e022bca6a9d2fc0481fb57d23c87d90f394b559d4f062` +- Selected files: [`tools/openlibm-files.txt`](tools/openlibm-files.txt) + +Only the architecture/compatibility headers and C sources required by +TypePHP-OS's current x86_64 double-precision math ABI are installed. The +selection keeps unused implementations out of the freestanding payload. diff --git a/examples/typephp-os/freestanding/abi/typephp_os_syscall.h b/examples/typephp-os/freestanding/abi/typephp_os_syscall.h index 1264b7d9..776dd199 100644 --- a/examples/typephp-os/freestanding/abi/typephp_os_syscall.h +++ b/examples/typephp-os/freestanding/abi/typephp_os_syscall.h @@ -1,18 +1,29 @@ #ifndef TYPEPHP_OS_SYSCALL_H #define TYPEPHP_OS_SYSCALL_H -/* Keep familiar Linux x86_64 numbers where the operation has a close match. - * READDIR is currently a compact TypePHP-OS directory-list operation rather - * than Linux getdents64's binary record ABI. */ +/* Keep Linux x86_64 numbers only where both the operation and its ABI match. + * Experimental TypePHP-OS operations live in a private range so they do not + * occupy numbers that a future glibc port expects to have Linux semantics. */ enum typephp_os_syscall_number { TYPEPHP_SYS_READ = 0, TYPEPHP_SYS_WRITE = 1, - TYPEPHP_SYS_EXEC = 59, + TYPEPHP_SYS_CLOSE = 3, + TYPEPHP_SYS_LSEEK = 8, + TYPEPHP_SYS_MMAP = 9, + TYPEPHP_SYS_MPROTECT = 10, + TYPEPHP_SYS_MUNMAP = 11, + TYPEPHP_SYS_BRK = 12, TYPEPHP_SYS_EXIT = 60, TYPEPHP_SYS_GETCWD = 79, TYPEPHP_SYS_CHDIR = 80, + TYPEPHP_SYS_MKDIR = 83, + TYPEPHP_SYS_RMDIR = 84, + TYPEPHP_SYS_UNLINK = 87, TYPEPHP_SYS_TIME = 201, - TYPEPHP_SYS_READDIR = 217, + TYPEPHP_SYS_OPENAT = 257, + TYPEPHP_SYS_SPAWN = 0x54500001, + TYPEPHP_SYS_LISTDIR = 0x54500002, + TYPEPHP_SYS_RENAME = 0x54500003, }; #endif diff --git a/examples/typephp-os/freestanding/ata.c b/examples/typephp-os/freestanding/ata.c index ae3888ca..597458c1 100644 --- a/examples/typephp-os/freestanding/ata.c +++ b/examples/typephp-os/freestanding/ata.c @@ -9,6 +9,7 @@ #include #include +#include enum { ATA_DATA = 0x1f0, @@ -32,11 +33,24 @@ enum { ATA_COMMAND_CACHE_FLUSH = 0xe7, ATA_SECTOR_SIZE = 512, ATA_TIMEOUT = 10000000, + ATA_CACHE_CAPACITY = 128, PCI_CONFIG_ADDRESS = 0xcf8, PCI_CONFIG_DATA = 0xcfc, }; static int ata_initialized; +static uint64_t ata_cache_clock; +static uint64_t ata_cache_hit_count; +static uint64_t ata_cache_miss_count; + +typedef struct { + uint32_t lba; + uint64_t last_used; + int valid; + unsigned char data[ATA_SECTOR_SIZE]; +} ata_cache_entry; + +static ata_cache_entry ata_cache[ATA_CACHE_CAPACITY]; static void ata_delay_400ns(void); @@ -156,7 +170,7 @@ int typephp_os_disk_available(void) return status != 0xff; } -int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data) +static int ata_read_sector_raw(uint32_t lba, unsigned char *data) { if (data == 0 || !ata_select_lba(lba)) { return 0; @@ -174,7 +188,7 @@ int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data) return ata_wait_not_busy(); } -int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data) +static int ata_write_sector_raw(uint32_t lba, const unsigned char *data) { if (data == 0 || !ata_select_lba(lba)) { return 0; @@ -190,6 +204,78 @@ int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data) return ata_wait_not_busy(); } +static ata_cache_entry *ata_cache_slot(uint32_t lba, int *found) +{ + ata_cache_entry *oldest = &ata_cache[0]; + for (size_t index = 0; index < ATA_CACHE_CAPACITY; ++index) { + ata_cache_entry *entry = &ata_cache[index]; + if (entry->valid && entry->lba == lba) { + *found = 1; + return entry; + } + if (!entry->valid) { + *found = 0; + return entry; + } + if (entry->last_used < oldest->last_used) { + oldest = entry; + } + } + *found = 0; + return oldest; +} + +int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data) +{ + int found; + ata_cache_entry *entry; + if (data == 0) { + return 0; + } + entry = ata_cache_slot(lba, &found); + if (found) { + ++ata_cache_hit_count; + entry->last_used = ++ata_cache_clock; + memcpy(data, entry->data, ATA_SECTOR_SIZE); + return 1; + } + ++ata_cache_miss_count; + if (!ata_read_sector_raw(lba, data)) { + return 0; + } + entry->valid = 1; + entry->lba = lba; + entry->last_used = ++ata_cache_clock; + memcpy(entry->data, data, ATA_SECTOR_SIZE); + return 1; +} + +int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data) +{ + int found; + ata_cache_entry *entry; + if (data == 0 || !ata_write_sector_raw(lba, data)) { + return 0; + } + entry = ata_cache_slot(lba, &found); + (void) found; + entry->valid = 1; + entry->lba = lba; + entry->last_used = ++ata_cache_clock; + memcpy(entry->data, data, ATA_SECTOR_SIZE); + return 1; +} + +unsigned long typephp_os_disk_cache_hits(void) +{ + return (unsigned long) ata_cache_hit_count; +} + +unsigned long typephp_os_disk_cache_misses(void) +{ + return (unsigned long) ata_cache_miss_count; +} + int typephp_os_disk_flush(void) { if (!ata_wait_not_busy()) { diff --git a/examples/typephp-os/freestanding/boot.S b/examples/typephp-os/freestanding/boot.S index f266379f..2ff095f6 100644 --- a/examples/typephp-os/freestanding/boot.S +++ b/examples/typephp-os/freestanding/boot.S @@ -72,14 +72,14 @@ map_page: mov $pml4_table, %eax mov %eax, %cr3 - # Set EFER.LME, then enable paging. Protected mode is already active. + # Enable long mode and execute-disable page permissions. mov $0xc0000080, %ecx rdmsr - or $0x100, %eax + or $0x900, %eax wrmsr mov %cr0, %eax - or $0x80000000, %eax + or $0x80010000, %eax mov %eax, %cr0 lgdt gdt64_pointer diff --git a/examples/typephp-os/freestanding/bridge.cc b/examples/typephp-os/freestanding/bridge.cc index 79cebfc7..2d8fba17 100644 --- a/examples/typephp-os/freestanding/bridge.cc +++ b/examples/typephp-os/freestanding/bridge.cc @@ -19,6 +19,8 @@ extern "C" int typephp_os_disk_available(); extern "C" int typephp_os_disk_read_sector(uint32_t lba, unsigned char *data); extern "C" int typephp_os_disk_write_sector(uint32_t lba, const unsigned char *data); extern "C" int typephp_os_disk_flush(); +extern "C" unsigned long typephp_os_disk_cache_hits(); +extern "C" unsigned long typephp_os_disk_cache_misses(); extern "C" void typephp_os_process_start(); extern "C" ZEND_NORETURN void phpx_no_exception_abort(const char *fallback) @@ -140,6 +142,16 @@ php::Bool php_kernel_disk_flush() return typephp_os_disk_flush() != 0; } +php::Int php_kernel_disk_cache_hits() +{ + return static_cast(typephp_os_disk_cache_hits()); +} + +php::Int php_kernel_disk_cache_misses() +{ + return static_cast(typephp_os_disk_cache_misses()); +} + void php_kernel_process_start() { typephp_os_process_start(); diff --git a/examples/typephp-os/freestanding/memory.c b/examples/typephp-os/freestanding/memory.c index 764583b2..798f57ef 100644 --- a/examples/typephp-os/freestanding/memory.c +++ b/examples/typephp-os/freestanding/memory.c @@ -2,16 +2,18 @@ typedef unsigned int uint32_t; typedef unsigned long uint64_t; extern void typephp_os_memory_init(void *address, unsigned long size); +extern void *memset(void *destination, int value, unsigned long size); enum { MULTIBOOT_INFO_MEMORY_MAP = 1u << 6, }; static const uint64_t CHUNK_SIZE = 2ul * 1024ul * 1024ul; -/* Keep the complete TypePHP + PHP Nano payload away from the page allocator. */ -/* Keep both the kernel payload and the fixed 32-36 MiB Ring-3 area away from - * the Zend arena. This is deliberately simple until a page-frame allocator - * owns user address-space placement. */ +static const uint64_t PAGE_SIZE = 4096ul; +/* Keep the kernel payload and the 32-36 MiB user virtual window away from + * kernel physical allocations. Each user CR3 replaces that part of the + * shared identity map, so physical kernel data there would become hidden. + * A future high-half kernel map can remove this temporary reservation. */ static const uint64_t KERNEL_RESERVED_END = 40ul * 1024ul * 1024ul; static const uint64_t IDENTITY_MAP_END = 1024ul * 1024ul * 1024ul; @@ -38,6 +40,11 @@ typedef struct __attribute__((packed)) { static uint64_t usable_memory_bytes; static uint64_t chunk_cursor; static uint64_t chunk_end; +static uint64_t page_cursor; +static uint64_t page_begin; +static uint64_t page_end; +static uint64_t free_page_head; +static uint64_t available_pages; static uint64_t align_up(uint64_t value, uint64_t alignment) { @@ -51,6 +58,11 @@ void physical_memory_init(uint64_t multiboot_info_address) usable_memory_bytes = 0; chunk_cursor = 0; chunk_end = 0; + page_cursor = 0; + page_begin = 0; + page_end = 0; + free_page_head = 0; + available_pages = 0; if ((info->flags & MULTIBOOT_INFO_MEMORY_MAP) == 0) { return; @@ -86,10 +98,58 @@ void physical_memory_init(uint64_t multiboot_info_address) } if (chunk_cursor != 0 && chunk_end > chunk_cursor) { + const uint64_t region_size = chunk_end - chunk_cursor; + uint64_t zend_size = (region_size / 2) & ~(CHUNK_SIZE - 1); + if (zend_size < 16ul * 1024ul * 1024ul + && region_size >= 32ul * 1024ul * 1024ul) { + zend_size = 16ul * 1024ul * 1024ul; + } typephp_os_memory_init( (void *) (unsigned long) chunk_cursor, - (unsigned long) (chunk_end - chunk_cursor)); + (unsigned long) zend_size); + page_cursor = align_up(chunk_cursor + zend_size, PAGE_SIZE); + page_begin = page_cursor; + page_end = chunk_end & ~(PAGE_SIZE - 1); + if (page_end > page_cursor) { + available_pages = (page_end - page_cursor) / PAGE_SIZE; + } + } +} + +uint64_t physical_page_allocate(void) +{ + uint64_t page; + if (free_page_head != 0) { + page = free_page_head; + free_page_head = *(uint64_t *) (unsigned long) page; + } else { + if (page_cursor == 0 || page_cursor + PAGE_SIZE > page_end) { + return 0; + } + page = page_cursor; + page_cursor += PAGE_SIZE; + } + if (available_pages != 0) { + --available_pages; } + memset((void *) (unsigned long) page, 0, PAGE_SIZE); + return page; +} + +void physical_page_free(uint64_t page) +{ + if ((page & (PAGE_SIZE - 1)) != 0 || page < page_begin + || page >= page_cursor || page >= page_end) { + return; + } + *(uint64_t *) (unsigned long) page = free_page_head; + free_page_head = page; + ++available_pages; +} + +uint64_t physical_page_available(void) +{ + return available_pages; } uint64_t physical_memory_megabytes(void) diff --git a/examples/typephp-os/freestanding/process-entry.S b/examples/typephp-os/freestanding/process-entry.S index d344550e..cfa566bf 100644 --- a/examples/typephp-os/freestanding/process-entry.S +++ b/examples/typephp-os/freestanding/process-entry.S @@ -45,6 +45,77 @@ typephp_os_syscall_entry: iretq .size typephp_os_syscall_entry, . - typephp_os_syscall_entry +.macro TYPEPHP_EXCEPTION_NO_ERROR number +.global typephp_os_exception_\number +.type typephp_os_exception_\number, @function +typephp_os_exception_\number: + pushq $0 + pushq $\number + jmp typephp_os_exception_common +.size typephp_os_exception_\number, . - typephp_os_exception_\number +.endm + +.macro TYPEPHP_EXCEPTION_WITH_ERROR number +.global typephp_os_exception_\number +.type typephp_os_exception_\number, @function +typephp_os_exception_\number: + # The CPU has already pushed the exception error code. + pushq $\number + jmp typephp_os_exception_common +.size typephp_os_exception_\number, . - typephp_os_exception_\number +.endm + +TYPEPHP_EXCEPTION_NO_ERROR 0 +TYPEPHP_EXCEPTION_NO_ERROR 3 +TYPEPHP_EXCEPTION_NO_ERROR 5 +TYPEPHP_EXCEPTION_NO_ERROR 6 +TYPEPHP_EXCEPTION_WITH_ERROR 10 +TYPEPHP_EXCEPTION_WITH_ERROR 11 +TYPEPHP_EXCEPTION_WITH_ERROR 12 +TYPEPHP_EXCEPTION_WITH_ERROR 13 +TYPEPHP_EXCEPTION_WITH_ERROR 14 + +.type typephp_os_exception_common, @function +.extern typephp_os_exception_dispatch +typephp_os_exception_common: + push %rbx + push %rcx + push %rdx + push %rsi + push %rdi + push %rbp + push %r8 + push %r9 + push %r10 + push %r11 + push %r12 + push %r13 + push %r14 + push %r15 + push %rax + + mov %rsp, %rdi + call typephp_os_exception_dispatch + + pop %rax + pop %r15 + pop %r14 + pop %r13 + pop %r12 + pop %r11 + pop %r10 + pop %r9 + pop %r8 + pop %rbp + pop %rdi + pop %rsi + pop %rdx + pop %rcx + pop %rbx + add $16, %rsp + iretq +.size typephp_os_exception_common, . - typephp_os_exception_common + .global typephp_os_enter_user .type typephp_os_enter_user, @function typephp_os_enter_user: diff --git a/examples/typephp-os/freestanding/process.c b/examples/typephp-os/freestanding/process.c index 4022843e..2230137b 100644 --- a/examples/typephp-os/freestanding/process.c +++ b/examples/typephp-os/freestanding/process.c @@ -9,28 +9,52 @@ #include "typephp_os_abi.h" #include "typephp_os_syscall.h" +#include "vm.h" +#include +#include #include #include #include +#include +#include enum { USER_BEGIN = 32u * 1024u * 1024u, + USER_COMMAND_BEGIN = 33u * 1024u * 1024u, + USER_SHELL_END = USER_COMMAND_BEGIN, + USER_COMMAND_END = 34u * 1024u * 1024u, USER_END = 36u * 1024u * 1024u, USER_STACK = USER_END - 16u, + USER_COMMAND_STACK = 35u * 1024u * 1024u - 16u, + USER_STACK_SIZE = 64u * 1024u, + USER_HEAP_LIMIT = 34u * 1024u * 1024u, + USER_MMAP_BEGIN = USER_HEAP_LIMIT, USER_CODE_SELECTOR = 0x23, USER_DATA_SELECTOR = 0x1b, KERNEL_CODE_SELECTOR = 0x08, TSS_SELECTOR = 0x28, IDT_SYSCALL = 0x80, + MAX_USER_ARGUMENTS = 8, + USER_PATH_MAX = 128, }; enum { ELF_PT_LOAD = 1, + ELF_PF_X = 1, + ELF_PF_W = 2, ELF_ET_EXEC = 2, ELF_MACHINE_X86_64 = 62, }; +enum { + PROT_READ_VALUE = 1, + PROT_WRITE_VALUE = 2, + PROT_EXEC_VALUE = 4, + MAP_PRIVATE_VALUE = 2, + MAP_ANONYMOUS_VALUE = 0x20, +}; + typedef struct __attribute__((packed)) { unsigned char ident[16]; uint16_t type; @@ -137,23 +161,22 @@ typedef struct { typedef struct { uint64_t pid; int running; - char cwd[16]; + char cwd[USER_PATH_MAX]; syscall_frame parent_frame; int parent_waiting; + uint64_t address_space; + uint64_t parent_address_space; + uint64_t parent_free_pages; + uint64_t program_break; + uint64_t minimum_break; + uint64_t mmap_cursor; + uint64_t mmap_limit; + uint64_t parent_program_break; + uint64_t parent_minimum_break; + uint64_t parent_mmap_cursor; + uint64_t parent_mmap_limit; } process_state; -extern const unsigned char typephp_user_sh_elf_start[]; -extern const unsigned char typephp_user_sh_elf_end[]; -extern const unsigned char typephp_user_ls_elf_start[]; -extern const unsigned char typephp_user_ls_elf_end[]; -extern const unsigned char typephp_user_cd_elf_start[]; -extern const unsigned char typephp_user_cd_elf_end[]; -extern const unsigned char typephp_user_date_elf_start[]; -extern const unsigned char typephp_user_date_elf_end[]; -extern const unsigned char typephp_user_pwd_elf_start[]; -extern const unsigned char typephp_user_pwd_elf_end[]; -extern const unsigned char typephp_user_fault_elf_start[]; -extern const unsigned char typephp_user_fault_elf_end[]; extern void typephp_os_syscall_entry(void); extern void typephp_os_enter_user(uint64_t entry, uint64_t stack); extern long typephp_os_time_seconds(void); @@ -166,6 +189,8 @@ extern void typephp_os_exception_11(void); extern void typephp_os_exception_12(void); extern void typephp_os_exception_13(void); extern void typephp_os_exception_14(void); +extern int rename(const char *old_path, const char *new_path); +extern uint64_t physical_page_available(void); static uint64_t gdt[7] __attribute__((aligned(16))); static idt_gate idt[256] __attribute__((aligned(16))); @@ -184,9 +209,20 @@ static int range_inside(uint64_t address, uint64_t size, uint64_t begin, uint64_ return address >= begin && size <= end - begin && address <= end - size; } +static uint64_t page_align_up(uint64_t value) +{ + return (value + UINT64_C(4095)) & ~UINT64_C(4095); +} + +static uint64_t page_align_down(uint64_t value) +{ + return value & ~UINT64_C(4095); +} + static int user_buffer(const void *pointer, size_t size) { - return range_inside((uint64_t) (uintptr_t) pointer, size, USER_BEGIN, USER_END); + return typephp_vm_user_range(typephp_vm_current(), + (uint64_t) (uintptr_t) pointer, size, 0); } static size_t bounded_user_string(const char *string, size_t maximum) @@ -264,86 +300,199 @@ static void install_descriptor_tables(void) __asm__ volatile("lidt %0" : : "m"(pointer) : "memory"); } -static uint64_t load_user_elf(const unsigned char *image, const unsigned char *image_end) +static int read_exact(int fd, void *buffer, size_t size) { - const size_t image_size = (size_t) (image_end - image); - const elf64_header *header; + unsigned char *output = (unsigned char *) buffer; + while (size != 0) { + ssize_t result = read(fd, output, size); + if (result < 0) { + return -errno; + } + if (result == 0) { + return -ENOEXEC; + } + output += result; + size -= (size_t) result; + } + return 0; +} - if (image_size < sizeof(elf64_header)) { - panic("truncated user ELF\n"); +static int seek_and_read(int fd, uint64_t offset, void *buffer, size_t size) +{ + if (offset > INT64_MAX || lseek(fd, (off_t) offset, SEEK_SET) < 0) { + return -errno; } - header = (const elf64_header *) image; - if (header->ident[0] != 0x7f || header->ident[1] != 'E' - || header->ident[2] != 'L' || header->ident[3] != 'F' - || header->ident[4] != 2 || header->ident[5] != 1 - || header->type != ELF_ET_EXEC || header->machine != ELF_MACHINE_X86_64 - || header->phentsize != sizeof(elf64_program_header)) { - panic("invalid user ELF64 header\n"); + return read_exact(fd, buffer, size); +} + +static int load_user_elf_file( + const char *path, uint64_t address_space, + uint64_t region_begin, uint64_t region_end, + uint64_t *entry, uint64_t *image_end) +{ + elf64_header header; + uint64_t image_size; + int fd = open(path, O_RDONLY); + int result = 0; + int entry_loaded = 0; + uint64_t loaded_end = region_begin; + + if (fd < 0) { + return -errno; } - if (!range_inside(header->phoff, - (uint64_t) header->phnum * sizeof(elf64_program_header), 0, image_size)) { - panic("invalid user ELF program table\n"); + { + off_t end = lseek(fd, 0, SEEK_END); + if (end < 0) { + result = -errno; + goto done; + } + image_size = (uint64_t) end; + } + if (image_size < sizeof(header)) { + result = -ENOEXEC; + goto done; + } + result = seek_and_read(fd, 0, &header, sizeof(header)); + if (result < 0) { + goto done; + } + if (header.ident[0] != 0x7f || header.ident[1] != 'E' + || header.ident[2] != 'L' || header.ident[3] != 'F' + || header.ident[4] != 2 || header.ident[5] != 1 + || header.type != ELF_ET_EXEC || header.machine != ELF_MACHINE_X86_64 + || header.phentsize != sizeof(elf64_program_header) + || header.phnum == 0 || header.phnum > 64 + || !range_inside(header.phoff, + (uint64_t) header.phnum * sizeof(elf64_program_header), 0, image_size)) { + result = -ENOEXEC; + goto done; } - for (uint16_t index = 0; index < header->phnum; ++index) { - const elf64_program_header *segment = (const elf64_program_header *) - (image + header->phoff + (uint64_t) index * sizeof(*segment)); - if (segment->type != ELF_PT_LOAD) { + for (uint16_t index = 0; index < header.phnum; ++index) { + elf64_program_header segment; + result = seek_and_read(fd, + header.phoff + (uint64_t) index * sizeof(segment), &segment, sizeof(segment)); + if (result < 0) { + goto done; + } + if (segment.type != ELF_PT_LOAD) { continue; } - if (segment->filesz > segment->memsz - || !range_inside(segment->offset, segment->filesz, 0, image_size) - || !range_inside(segment->vaddr, segment->memsz, USER_BEGIN, USER_END)) { - panic("invalid user ELF load segment\n"); + if (segment.filesz > segment.memsz + || !range_inside(segment.offset, segment.filesz, 0, image_size) + || !range_inside(segment.vaddr, segment.memsz, region_begin, region_end)) { + result = -ENOEXEC; + goto done; + } + /* The loader needs temporary write access while copying and clearing + * the segment. The final ELF permissions are installed below. */ + if (segment.memsz != 0 && !typephp_vm_map_user(address_space, + segment.vaddr, segment.memsz, + TYPEPHP_VM_USER_WRITE + | ((segment.flags & ELF_PF_X) != 0 ? TYPEPHP_VM_USER_EXECUTE : 0))) { + result = -ENOMEM; + goto done; + } + if (segment.vaddr + segment.memsz > loaded_end) { + loaded_end = segment.vaddr + segment.memsz; + } + result = seek_and_read(fd, segment.offset, + (void *) (uintptr_t) segment.vaddr, (size_t) segment.filesz); + if (result < 0) { + goto done; + } + memset((void *) (uintptr_t) (segment.vaddr + segment.filesz), + 0, (size_t) (segment.memsz - segment.filesz)); + if (segment.memsz != 0 && !typephp_vm_protect_user(address_space, + segment.vaddr, segment.memsz, + ((segment.flags & ELF_PF_W) != 0 ? TYPEPHP_VM_USER_WRITE : 0) + | ((segment.flags & ELF_PF_X) != 0 + ? TYPEPHP_VM_USER_EXECUTE : 0))) { + result = -ENOMEM; + goto done; + } + if ((segment.flags & ELF_PF_X) != 0 + && range_inside(header.entry, 1, segment.vaddr, segment.vaddr + segment.memsz)) { + entry_loaded = 1; } - memcpy((void *) (uintptr_t) segment->vaddr, image + segment->offset, segment->filesz); - memset((void *) (uintptr_t) (segment->vaddr + segment->filesz), - 0, segment->memsz - segment->filesz); } - if (!range_inside(header->entry, 1, USER_BEGIN, USER_END)) { - panic("invalid user ELF entry\n"); + if (!entry_loaded || !range_inside(header.entry, 1, region_begin, region_end)) { + result = -ENOEXEC; + goto done; } - return header->entry; + *entry = header.entry; + *image_end = loaded_end; + +done: + if (close(fd) < 0 && result == 0) { + result = -errno; + } + return result; } -static int command_image( - const char *name, - const unsigned char **image, - const unsigned char **image_end) +static uint64_t prepare_initial_stack( + uint64_t stack_top, size_t argc, const char *const argv[]) { - if (strcmp(name, "ls") == 0) { - *image = typephp_user_ls_elf_start; - *image_end = typephp_user_ls_elf_end; - return 1; - } - if (strcmp(name, "cd") == 0) { - *image = typephp_user_cd_elf_start; - *image_end = typephp_user_cd_elf_end; - return 1; + uint64_t stack = stack_top; + uint64_t addresses[MAX_USER_ARGUMENTS]; + uint64_t *words; + size_t index = 0; + + index = argc; + while (index != 0) { + size_t length; + --index; + length = strlen(argv[index]) + 1u; + stack -= length; + memcpy((void *) (uintptr_t) stack, argv[index], length); + addresses[index] = stack; } - if (strcmp(name, "date") == 0) { - *image = typephp_user_date_elf_start; - *image_end = typephp_user_date_elf_end; - return 1; + + /* argc, argv[], NULL, envp NULL, then an AT_NULL auxv entry. */ + stack = (stack - (argc + 5u) * sizeof(uint64_t)) & ~UINT64_C(0x0f); + words = (uint64_t *) (uintptr_t) stack; + words[index++] = argc; + for (size_t argument = 0; argument < argc; ++argument) { + words[index++] = addresses[argument]; } - if (strcmp(name, "pwd") == 0) { - *image = typephp_user_pwd_elf_start; - *image_end = typephp_user_pwd_elf_end; - return 1; + words[index++] = 0; + words[index++] = 0; + words[index++] = 0; + words[index] = 0; + return stack; +} + +static int command_path(const char *name, char *path, size_t capacity) +{ + size_t length = strlen(name); + if (length == 0 || length > 8 || capacity < length + sizeof("/BIN/.ELF")) { + return 0; } - if (strcmp(name, "fault") == 0) { - *image = typephp_user_fault_elf_start; - *image_end = typephp_user_fault_elf_end; - return 1; + memcpy(path, "/BIN/", sizeof("/BIN/") - 1u); + for (size_t index = 0; index < length; ++index) { + unsigned char character = (unsigned char) name[index]; + if (!((character >= 'a' && character <= 'z') + || (character >= 'A' && character <= 'Z') + || (character >= '0' && character <= '9') + || character == '_' || character == '-')) { + return 0; + } + path[index + sizeof("/BIN/") - 1u] = (char) character; } - return 0; + memcpy(path + length + sizeof("/BIN/") - 1u, ".ELF", sizeof(".ELF")); + return 1; } +static int resolved_path(const char *path, char *resolved, size_t capacity); + static long syscall_getcwd(char *buffer, size_t size) { size_t length = strlen(foreground_process.cwd) + 1u; - if (size < length || !user_buffer(buffer, size)) { - return -1; + if (!user_buffer(buffer, size)) { + return -EFAULT; + } + if (size < length) { + return -ERANGE; } memcpy(buffer, foreground_process.cwd, length); return (long) length; @@ -351,29 +500,18 @@ static long syscall_getcwd(char *buffer, size_t size) static long syscall_chdir(const char *path) { - char normalized[16]; - size_t length = bounded_user_string(path, sizeof(normalized)); - if (length == (size_t) -1 || length == 0) { - return -1; + char normalized[USER_PATH_MAX]; + if (!resolved_path(path, normalized, sizeof(normalized))) { + return -ENOENT; } - if ((length == 1 && path[0] == '/') - || (length == 2 && path[0] == '.' && path[1] == '.')) { - normalized[0] = '/'; - normalized[1] = '\0'; - } else if (length == 1 && path[0] == '.') { - return 0; - } else { - size_t source = path[0] == '/' ? 1u : 0u; - size_t name_length = length - source; - if (name_length == 0 || name_length > 12u) { - return -1; + { + long type = typephp_os_fs_path_type(normalized); + if (type == 0) { + return -ENOENT; + } + if (type != 2) { + return -ENOTDIR; } - normalized[0] = '/'; - memcpy(normalized + 1, path + source, name_length); - normalized[name_length + 1u] = '\0'; - } - if (typephp_os_fs_path_type(normalized) != 2) { - return -1; } memcpy(foreground_process.cwd, normalized, strlen(normalized) + 1u); return 0; @@ -381,52 +519,322 @@ static long syscall_chdir(const char *path) static long syscall_readdir(const char *path, char *buffer, size_t capacity) { - char resolved[16]; + char resolved[USER_PATH_MAX]; const char *directory = foreground_process.cwd; if (path != 0) { - size_t length = bounded_user_string(path, sizeof(resolved)); - if (length == (size_t) -1) { - return -1; - } - if (length != 0) { - memcpy(resolved, path, length + 1u); - directory = resolved; + if (!resolved_path(path, resolved, sizeof(resolved))) { + return -ENOENT; } + directory = resolved; } if (!user_buffer(buffer, capacity)) { - return -1; + return -EFAULT; } return typephp_os_fs_list(directory, buffer, capacity); } -static long syscall_exec(syscall_frame *frame, const char *name, const char *argument) +static int resolved_path(const char *path, char *resolved, size_t capacity) +{ + size_t length = bounded_user_string(path, capacity); + size_t input = 0; + size_t output; + if (length == (size_t) -1 || length == 0) { + return 0; + } + if (capacity < 2) { + return 0; + } + if (path[0] == '/') { + resolved[0] = '/'; + output = 1; + } else { + output = strlen(foreground_process.cwd); + if (output + 1u > capacity) { + return 0; + } + memcpy(resolved, foreground_process.cwd, output); + } + while (input < length) { + size_t start; + size_t component_length; + while (input < length && path[input] == '/') { + ++input; + } + if (input == length) { + break; + } + start = input; + while (input < length && path[input] != '/') { + ++input; + } + component_length = input - start; + if (component_length == 1 && path[start] == '.') { + continue; + } + if (component_length == 2 && path[start] == '.' && path[start + 1u] == '.') { + while (output > 1 && resolved[output - 1u] != '/') { + --output; + } + if (output > 1) { + --output; + } + continue; + } + if (component_length > 12u) { + return 0; + } + if (output > 1) { + if (output + 1u >= capacity) { + return 0; + } + resolved[output++] = '/'; + } + if (output + component_length + 1u > capacity) { + return 0; + } + memcpy(resolved + output, path + start, component_length); + output += component_length; + } + resolved[output] = '\0'; + return 1; +} + +static long posix_syscall_result(long result) +{ + return result < 0 ? -errno : result; +} + +static long syscall_openat(long directory_fd, const char *path, int flags, int mode) +{ + char resolved[USER_PATH_MAX]; + int fd; + if ((int) directory_fd != AT_FDCWD && (path == 0 || path[0] != '/')) { + return -EBADF; + } + if (!resolved_path(path, resolved, sizeof(resolved))) { + return -EFAULT; + } + fd = open(resolved, flags, mode); + return posix_syscall_result(fd); +} + +static long syscall_time(long *result) +{ + long seconds = typephp_os_time_seconds(); + if (result != 0) { + if (!user_buffer(result, sizeof(*result))) { + return -1; + } + *result = seconds; + } + return seconds; +} + +static unsigned int vm_protection(int protection) +{ + unsigned int flags = 0; + if (protection == 0) { + return TYPEPHP_VM_USER_NONE; + } + if ((protection & PROT_WRITE_VALUE) != 0) { + flags |= TYPEPHP_VM_USER_WRITE; + } + if ((protection & PROT_EXEC_VALUE) != 0) { + flags |= TYPEPHP_VM_USER_EXECUTE; + } + return flags; +} + +static long syscall_brk(uint64_t requested) +{ + const uint64_t old_break = foreground_process.program_break; + uint64_t old_page; + uint64_t new_page; + if (requested == 0) { + return (long) old_break; + } + if (requested < foreground_process.minimum_break || requested > USER_HEAP_LIMIT) { + return (long) old_break; + } + old_page = page_align_up(old_break); + new_page = page_align_up(requested); + if (new_page > old_page) { + const uint64_t size = new_page - old_page; + if (!typephp_vm_user_range_free( + foreground_process.address_space, old_page, size) + || !typephp_vm_map_user(foreground_process.address_space, + old_page, size, TYPEPHP_VM_USER_WRITE)) { + (void) typephp_vm_unmap_user( + foreground_process.address_space, old_page, size); + return (long) old_break; + } + } else if (old_page > new_page) { + (void) typephp_vm_unmap_user( + foreground_process.address_space, new_page, old_page - new_page); + } + foreground_process.program_break = requested; + return (long) requested; +} + +static long syscall_mmap( + uint64_t address, uint64_t length, int protection, + int flags, int fd, uint64_t offset) +{ + uint64_t size; + uint64_t selected; + uint64_t previous_cursor = foreground_process.mmap_cursor; + const int supported_flags = MAP_PRIVATE_VALUE | MAP_ANONYMOUS_VALUE; + if (length == 0 || length > UINT64_MAX - UINT64_C(4095) + || (protection & ~(PROT_READ_VALUE | PROT_WRITE_VALUE | PROT_EXEC_VALUE)) != 0 + || (flags & supported_flags) != supported_flags + || (flags & ~supported_flags) != 0 || fd != -1 || offset != 0) { + return -EINVAL; + } + size = page_align_up(length); + if (address != 0) { + selected = page_align_down(address); + if (selected < USER_MMAP_BEGIN + || selected > foreground_process.mmap_limit + || size > foreground_process.mmap_limit - selected + || !typephp_vm_user_range_free( + foreground_process.address_space, selected, size)) { + selected = 0; + } + } else { + selected = 0; + } + if (selected == 0) { + if (size > foreground_process.mmap_cursor - USER_MMAP_BEGIN) { + return -ENOMEM; + } + selected = page_align_down(foreground_process.mmap_cursor - size); + if (selected < USER_MMAP_BEGIN + || !typephp_vm_user_range_free( + foreground_process.address_space, selected, size)) { + return -ENOMEM; + } + foreground_process.mmap_cursor = selected; + } + if (!typephp_vm_map_user(foreground_process.address_space, + selected, size, vm_protection(protection))) { + (void) typephp_vm_unmap_user( + foreground_process.address_space, selected, size); + foreground_process.mmap_cursor = previous_cursor; + return -ENOMEM; + } + return (long) selected; +} + +static long syscall_munmap(uint64_t address, uint64_t length) +{ + uint64_t size; + if ((address & UINT64_C(4095)) != 0 || length == 0 + || length > UINT64_MAX - UINT64_C(4095)) { + return -EINVAL; + } + size = page_align_up(length); + if (address < USER_MMAP_BEGIN || address > foreground_process.mmap_limit + || size > foreground_process.mmap_limit - address) { + return -EINVAL; + } + return typephp_vm_unmap_user( + foreground_process.address_space, address, size) ? 0 : -EINVAL; +} + +static long syscall_mprotect(uint64_t address, uint64_t length, int protection) +{ + if ((address & UINT64_C(4095)) != 0 || length == 0 + || length > UINT64_MAX - UINT64_C(4095) + || (protection & ~(PROT_READ_VALUE | PROT_WRITE_VALUE | PROT_EXEC_VALUE)) != 0) { + return -EINVAL; + } + return typephp_vm_protect_user(foreground_process.address_space, + address, page_align_up(length), vm_protection(protection)) ? 0 : -ENOMEM; +} + +static long syscall_spawn(syscall_frame *frame, const char *const *arguments) { - const unsigned char *image; - const unsigned char *image_end; - char command[8]; + uint64_t entry; + uint64_t child_address_space; + uint64_t parent_address_space; + uint64_t image_end; + char command[9]; + char path[32]; + char argument_storage[MAX_USER_ARGUMENTS][65]; + const char *kernel_arguments[MAX_USER_ARGUMENTS]; size_t name_length; + size_t argc = 0; if (foreground_process.parent_waiting) { return -1; } - name_length = bounded_user_string(name, sizeof(command)); - if (name_length == (size_t) -1 || name_length == 0) { - return -1; + while (argc < MAX_USER_ARGUMENTS) { + if (!user_buffer(arguments + argc, sizeof(*arguments))) { + return -1; + } + if (arguments[argc] == 0) { + break; + } + size_t length = bounded_user_string(arguments[argc], 64); + if (length == (size_t) -1) { + return -1; + } + memcpy(argument_storage[argc], arguments[argc], length + 1u); + kernel_arguments[argc] = argument_storage[argc]; + ++argc; } - memcpy(command, name, name_length + 1u); - if (!command_image(command, &image, &image_end)) { + if (argc == 0 || argc == MAX_USER_ARGUMENTS) { return -1; } - if (argument != 0 && bounded_user_string(argument, 64) == (size_t) -1) { + name_length = strlen(kernel_arguments[0]); + if (name_length == 0 || name_length >= sizeof(command)) { return -1; } + memcpy(command, kernel_arguments[0], name_length + 1u); + if (!command_path(command, path, sizeof(path))) { + return -ENOENT; + } + foreground_process.parent_free_pages = physical_page_available(); + child_address_space = typephp_vm_create(); + if (child_address_space == 0 + || !typephp_vm_map_user(child_address_space, + USER_COMMAND_STACK + 16u - USER_STACK_SIZE, + USER_STACK_SIZE, TYPEPHP_VM_USER_WRITE)) { + if (child_address_space != 0) { + typephp_vm_destroy(child_address_space); + } + return -ENOMEM; + } + parent_address_space = typephp_vm_current(); + typephp_vm_activate(child_address_space); + { + int result = load_user_elf_file( + path, child_address_space, + USER_COMMAND_BEGIN, USER_COMMAND_END, &entry, &image_end); + if (result < 0) { + typephp_vm_activate(parent_address_space); + typephp_vm_destroy(child_address_space); + return result; + } + } memcpy(&foreground_process.parent_frame, frame, sizeof(*frame)); foreground_process.parent_waiting = 1; foreground_process.pid = 2; - frame->rip = load_user_elf(image, image_end); - frame->rsp = 35u * 1024u * 1024u - 16u; - frame->rdi = (uint64_t) (uintptr_t) argument; + foreground_process.parent_address_space = parent_address_space; + foreground_process.address_space = child_address_space; + foreground_process.parent_program_break = foreground_process.program_break; + foreground_process.parent_minimum_break = foreground_process.minimum_break; + foreground_process.parent_mmap_cursor = foreground_process.mmap_cursor; + foreground_process.parent_mmap_limit = foreground_process.mmap_limit; + foreground_process.minimum_break = image_end; + foreground_process.program_break = image_end; + foreground_process.mmap_limit = page_align_down( + USER_COMMAND_STACK + 16u - USER_STACK_SIZE); + foreground_process.mmap_cursor = foreground_process.mmap_limit; + frame->rip = entry; + frame->rsp = prepare_initial_stack(USER_COMMAND_STACK, argc, kernel_arguments); + frame->rdi = 0; frame->rsi = 0; frame->rdx = 0; return 0; @@ -434,13 +842,25 @@ static long syscall_exec(syscall_frame *frame, const char *name, const char *arg static long syscall_exit(syscall_frame *frame, long status) { + uint64_t child_address_space; if (!foreground_process.parent_waiting) { foreground_process.running = 0; typephp_os_panic("shell process exited\n"); } + child_address_space = foreground_process.address_space; memcpy(frame, &foreground_process.parent_frame, sizeof(*frame)); + foreground_process.address_space = foreground_process.parent_address_space; + foreground_process.program_break = foreground_process.parent_program_break; + foreground_process.minimum_break = foreground_process.parent_minimum_break; + foreground_process.mmap_cursor = foreground_process.parent_mmap_cursor; + foreground_process.mmap_limit = foreground_process.parent_mmap_limit; foreground_process.parent_waiting = 0; foreground_process.pid = 1; + typephp_vm_activate(foreground_process.address_space); + typephp_vm_destroy(child_address_space); + if (physical_page_available() != foreground_process.parent_free_pages) { + panic("user address-space page leak\n"); + } return status; } @@ -489,17 +909,28 @@ static const char *exception_name(uint64_t vector) static void restore_shell_after_fault(exception_frame *frame) { + uint64_t child_address_space = foreground_process.address_space; syscall_frame *parent = &foreground_process.parent_frame; /* The general-register prefixes of both frame formats are identical. */ memcpy(frame, parent, offsetof(syscall_frame, rip)); - frame->rax = (uint64_t) -1; + frame->rax = (uint64_t) -EIO; frame->rip = parent->rip; frame->cs = parent->cs; frame->rflags = parent->rflags; frame->rsp = parent->rsp; frame->ss = parent->ss; + foreground_process.address_space = foreground_process.parent_address_space; + foreground_process.program_break = foreground_process.parent_program_break; + foreground_process.minimum_break = foreground_process.parent_minimum_break; + foreground_process.mmap_cursor = foreground_process.parent_mmap_cursor; + foreground_process.mmap_limit = foreground_process.parent_mmap_limit; foreground_process.parent_waiting = 0; foreground_process.pid = 1; + typephp_vm_activate(foreground_process.address_space); + typephp_vm_destroy(child_address_space); + if (physical_page_available() != foreground_process.parent_free_pages) { + panic("faulted address-space page leak\n"); + } } void typephp_os_exception_dispatch(exception_frame *frame) @@ -536,44 +967,124 @@ long typephp_os_syscall_dispatch(syscall_frame *frame) { switch (frame->rax) { case TYPEPHP_SYS_READ: - if (frame->rdi != 0 || !user_buffer((void *) frame->rsi, frame->rdx)) { - return -1; + if (!user_buffer((void *) frame->rsi, frame->rdx)) { + return -EFAULT; + } + if (frame->rdi == STDIN_FILENO) { + return typephp_os_console_read((void *) frame->rsi, frame->rdx); + } + if (frame->rdi <= STDERR_FILENO) { + return -EBADF; } - return typephp_os_console_read((void *) frame->rsi, frame->rdx); + return posix_syscall_result(read( + (int) frame->rdi, (void *) frame->rsi, frame->rdx)); case TYPEPHP_SYS_WRITE: - if ((frame->rdi != 1 && frame->rdi != 2) - || !user_buffer((void *) frame->rsi, frame->rdx)) { - return -1; + if (!user_buffer((void *) frame->rsi, frame->rdx)) { + return -EFAULT; + } + if (frame->rdi == STDOUT_FILENO || frame->rdi == STDERR_FILENO) { + typephp_os_write((const char *) frame->rsi, frame->rdx); + return (long) frame->rdx; + } + if (frame->rdi == STDIN_FILENO) { + return -EBADF; } - typephp_os_write((const char *) frame->rsi, frame->rdx); - return (long) frame->rdx; - case TYPEPHP_SYS_EXEC: - return syscall_exec(frame, (const char *) frame->rdi, (const char *) frame->rsi); + return posix_syscall_result(write( + (int) frame->rdi, (const void *) frame->rsi, frame->rdx)); + case TYPEPHP_SYS_CLOSE: + return posix_syscall_result(close((int) frame->rdi)); + case TYPEPHP_SYS_LSEEK: + return posix_syscall_result(lseek( + (int) frame->rdi, (off_t) frame->rsi, (int) frame->rdx)); + case TYPEPHP_SYS_MMAP: + return syscall_mmap(frame->rdi, frame->rsi, (int) frame->rdx, + (int) frame->r10, (int) frame->r8, frame->r9); + case TYPEPHP_SYS_MPROTECT: + return syscall_mprotect(frame->rdi, frame->rsi, (int) frame->rdx); + case TYPEPHP_SYS_MUNMAP: + return syscall_munmap(frame->rdi, frame->rsi); + case TYPEPHP_SYS_BRK: + return syscall_brk(frame->rdi); + case TYPEPHP_SYS_SPAWN: + return syscall_spawn(frame, (const char *const *) frame->rdi); case TYPEPHP_SYS_GETCWD: return syscall_getcwd((char *) frame->rdi, frame->rsi); case TYPEPHP_SYS_CHDIR: return syscall_chdir((const char *) frame->rdi); + case TYPEPHP_SYS_MKDIR: { + char resolved[USER_PATH_MAX]; + if (!resolved_path((const char *) frame->rdi, resolved, sizeof(resolved))) { + return -EFAULT; + } + return posix_syscall_result(mkdir(resolved, (mode_t) frame->rsi)); + } + case TYPEPHP_SYS_RMDIR: { + char resolved[USER_PATH_MAX]; + if (!resolved_path((const char *) frame->rdi, resolved, sizeof(resolved))) { + return -EFAULT; + } + return posix_syscall_result(rmdir(resolved)); + } + case TYPEPHP_SYS_UNLINK: { + char resolved[USER_PATH_MAX]; + if (!resolved_path((const char *) frame->rdi, resolved, sizeof(resolved))) { + return -EFAULT; + } + return posix_syscall_result(unlink(resolved)); + } + case TYPEPHP_SYS_RENAME: { + char old_path[USER_PATH_MAX]; + char new_path[USER_PATH_MAX]; + if (!resolved_path((const char *) frame->rdi, old_path, sizeof(old_path)) + || !resolved_path((const char *) frame->rsi, new_path, sizeof(new_path))) { + return -EFAULT; + } + return posix_syscall_result(rename(old_path, new_path)); + } case TYPEPHP_SYS_TIME: - return typephp_os_time_seconds(); - case TYPEPHP_SYS_READDIR: + return syscall_time((long *) frame->rdi); + case TYPEPHP_SYS_LISTDIR: return syscall_readdir((const char *) frame->rdi, (char *) frame->rsi, frame->rdx); + case TYPEPHP_SYS_OPENAT: + return syscall_openat((long) frame->rdi, (const char *) frame->rsi, + (int) frame->rdx, (int) frame->r10); case TYPEPHP_SYS_EXIT: return syscall_exit(frame, (long) frame->rdi); default: - return -1; + return -ENOSYS; } } void typephp_os_process_start(void) { uint64_t entry; - memset((void *) (uintptr_t) USER_BEGIN, 0, USER_END - USER_BEGIN); - entry = load_user_elf(typephp_user_sh_elf_start, typephp_user_sh_elf_end); + uint64_t address_space; + uint64_t image_end; + static const char *const shell_arguments[] = {"sh"}; + address_space = typephp_vm_create(); + if (address_space == 0 + || !typephp_vm_map_user(address_space, + USER_STACK + 16u - USER_STACK_SIZE, + USER_STACK_SIZE, TYPEPHP_VM_USER_WRITE)) { + panic("unable to create shell address space\n"); + } + typephp_vm_activate(address_space); + if (load_user_elf_file("/BIN/SH.ELF", address_space, + USER_BEGIN, USER_SHELL_END, &entry, &image_end) < 0) { + panic("unable to load /BIN/SH.ELF\n"); + } install_descriptor_tables(); foreground_process.running = 1; + foreground_process.address_space = address_space; + foreground_process.minimum_break = image_end; + foreground_process.program_break = image_end; + foreground_process.mmap_limit = page_align_down( + USER_STACK + 16u - USER_STACK_SIZE); + foreground_process.mmap_cursor = foreground_process.mmap_limit; typephp_os_write("Process 1: sh.elf (Ring 3)\n", sizeof("Process 1: sh.elf (Ring 3)\n") - 1); - typephp_os_enter_user(entry, USER_STACK); + typephp_os_enter_user(entry, + prepare_initial_stack(USER_STACK, 1, shell_arguments)); panic("Ring-3 entry returned\n"); } diff --git a/examples/typephp-os/freestanding/vm.c b/examples/typephp-os/freestanding/vm.c new file mode 100644 index 00000000..0a056162 --- /dev/null +++ b/examples/typephp-os/freestanding/vm.c @@ -0,0 +1,286 @@ +/* + +----------------------------------------------------------------------+ + | TypePHP OS | + +----------------------------------------------------------------------+ + | Per-process x86-64 page tables backed by the physical-page pool. | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include "vm.h" + +#include + +enum { + PAGE_SIZE = 4096, + PAGE_PRESENT = 1, + PAGE_WRITE = 2, + PAGE_USER = 4, + PAGE_HUGE = 128, + PAGE_OWNED = 512, + USER_FIRST_PDE = 16, + USER_LAST_PDE = 17, +}; + +#define PAGE_NX (UINT64_C(1) << 63) +#define PAGE_ADDRESS UINT64_C(0x000ffffffffff000) + +extern uint64_t physical_page_allocate(void); +extern void physical_page_free(uint64_t page); + +static uint64_t *page_table(uint64_t physical) +{ + return (uint64_t *) (uintptr_t) (physical & PAGE_ADDRESS); +} + +static uint64_t align_down(uint64_t value) +{ + return value & ~(uint64_t) (PAGE_SIZE - 1); +} + +static int align_range(uint64_t address, uint64_t size, uint64_t *begin, uint64_t *end) +{ + if (size == 0 || address > UINT64_MAX - size + || address + size > UINT64_MAX - (PAGE_SIZE - 1)) { + return 0; + } + *begin = align_down(address); + *end = (address + size + PAGE_SIZE - 1) & ~(uint64_t) (PAGE_SIZE - 1); + return *end > *begin; +} + +static uint64_t *page_directory(uint64_t address_space) +{ + uint64_t *pml4 = page_table(address_space); + if ((pml4[0] & PAGE_PRESENT) == 0) { + return 0; + } + uint64_t *pdp = page_table(pml4[0]); + if ((pdp[0] & PAGE_PRESENT) == 0) { + return 0; + } + return page_table(pdp[0]); +} + +uint64_t typephp_vm_create(void) +{ + const uint64_t pml4_page = physical_page_allocate(); + const uint64_t pdp_page = physical_page_allocate(); + const uint64_t pd_page = physical_page_allocate(); + if (pml4_page == 0 || pdp_page == 0 || pd_page == 0) { + if (pd_page != 0) physical_page_free(pd_page); + if (pdp_page != 0) physical_page_free(pdp_page); + if (pml4_page != 0) physical_page_free(pml4_page); + return 0; + } + + uint64_t *pml4 = page_table(pml4_page); + uint64_t *pdp = page_table(pdp_page); + uint64_t *pd = page_table(pd_page); + pml4[0] = pdp_page | PAGE_PRESENT | PAGE_WRITE | PAGE_USER; + pdp[0] = pd_page | PAGE_PRESENT | PAGE_WRITE | PAGE_USER; + for (uint64_t index = 0; index < 512; ++index) { + pd[index] = index * UINT64_C(0x200000) | PAGE_PRESENT | PAGE_WRITE | PAGE_HUGE; + } + pd[USER_FIRST_PDE] = 0; + pd[USER_LAST_PDE] = 0; + return pml4_page; +} + +void typephp_vm_activate(uint64_t address_space) +{ + __asm__ volatile("mov %0, %%cr3" : : "r"(address_space) : "memory"); +} + +uint64_t typephp_vm_current(void) +{ + uint64_t address_space; + __asm__ volatile("mov %%cr3, %0" : "=r"(address_space)); + return address_space & PAGE_ADDRESS; +} + +int typephp_vm_map_user( + uint64_t address_space, uint64_t address, uint64_t size, unsigned int flags) +{ + uint64_t begin; + uint64_t end; + uint64_t *pd = page_directory(address_space); + if (pd == 0 || !align_range(address, size, &begin, &end) + || begin < UINT64_C(0x2000000) || end > UINT64_C(0x2400000)) { + return 0; + } + + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + const uint64_t pd_index = (cursor >> 21) & 0x1ffu; + const uint64_t pt_index = (cursor >> 12) & 0x1ffu; + uint64_t *pt; + if ((pd[pd_index] & PAGE_PRESENT) == 0) { + const uint64_t pt_page = physical_page_allocate(); + if (pt_page == 0) { + return 0; + } + pd[pd_index] = pt_page | PAGE_PRESENT | PAGE_WRITE | PAGE_USER; + } else if ((pd[pd_index] & PAGE_HUGE) != 0) { + return 0; + } + pt = page_table(pd[pd_index]); + if ((pt[pt_index] & PAGE_OWNED) == 0) { + const uint64_t frame = physical_page_allocate(); + if (frame == 0) { + return 0; + } + pt[pt_index] = frame | PAGE_PRESENT | PAGE_USER | PAGE_OWNED | PAGE_NX; + } + if ((flags & TYPEPHP_VM_USER_NONE) != 0) { + pt[pt_index] &= ~(uint64_t) PAGE_PRESENT; + } else { + pt[pt_index] |= PAGE_PRESENT; + } + if ((flags & TYPEPHP_VM_USER_WRITE) != 0) { + pt[pt_index] |= PAGE_WRITE; + } + if ((flags & TYPEPHP_VM_USER_EXECUTE) != 0) { + pt[pt_index] &= ~PAGE_NX; + } + if (typephp_vm_current() == address_space) { + __asm__ volatile("invlpg (%0)" : : "r"((void *) (uintptr_t) cursor) : "memory"); + } + } + return 1; +} + +int typephp_vm_user_range_free( + uint64_t address_space, uint64_t address, uint64_t size) +{ + uint64_t begin; + uint64_t end; + uint64_t *pd = page_directory(address_space); + if (pd == 0 || !align_range(address, size, &begin, &end) + || begin < UINT64_C(0x2000000) || end > UINT64_C(0x2400000)) { + return 0; + } + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + const uint64_t pd_index = (cursor >> 21) & 0x1ffu; + const uint64_t pt_index = (cursor >> 12) & 0x1ffu; + if ((pd[pd_index] & PAGE_PRESENT) == 0) { + continue; + } + if ((pd[pd_index] & PAGE_HUGE) != 0 + || (page_table(pd[pd_index])[pt_index] & PAGE_OWNED) != 0) { + return 0; + } + } + return 1; +} + +int typephp_vm_unmap_user( + uint64_t address_space, uint64_t address, uint64_t size) +{ + uint64_t begin; + uint64_t end; + uint64_t *pd = page_directory(address_space); + if (pd == 0 || !align_range(address, size, &begin, &end) + || begin < UINT64_C(0x2000000) || end > UINT64_C(0x2400000)) { + return 0; + } + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + const uint64_t pd_index = (cursor >> 21) & 0x1ffu; + const uint64_t pt_index = (cursor >> 12) & 0x1ffu; + if ((pd[pd_index] & PAGE_PRESENT) == 0 || (pd[pd_index] & PAGE_HUGE) != 0) { + continue; + } + uint64_t *pt = page_table(pd[pd_index]); + if ((pt[pt_index] & PAGE_OWNED) != 0) { + physical_page_free(pt[pt_index] & PAGE_ADDRESS); + pt[pt_index] = 0; + if (typephp_vm_current() == address_space) { + __asm__ volatile("invlpg (%0)" : : "r"((void *) (uintptr_t) cursor) : "memory"); + } + } + } + return 1; +} + +int typephp_vm_protect_user( + uint64_t address_space, uint64_t address, uint64_t size, unsigned int flags) +{ + uint64_t begin; + uint64_t end; + uint64_t *pd = page_directory(address_space); + if (pd == 0 || !align_range(address, size, &begin, &end) + || begin < UINT64_C(0x2000000) || end > UINT64_C(0x2400000)) { + return 0; + } + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + const uint64_t pd_index = (cursor >> 21) & 0x1ffu; + const uint64_t pt_index = (cursor >> 12) & 0x1ffu; + if ((pd[pd_index] & PAGE_PRESENT) == 0 || (pd[pd_index] & PAGE_HUGE) != 0 + || (page_table(pd[pd_index])[pt_index] & PAGE_OWNED) == 0) { + return 0; + } + } + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + uint64_t *pt = page_table(pd[(cursor >> 21) & 0x1ffu]); + uint64_t *entry = &pt[(cursor >> 12) & 0x1ffu]; + *entry &= ~(uint64_t) (PAGE_PRESENT | PAGE_WRITE); + *entry |= PAGE_NX; + if ((flags & TYPEPHP_VM_USER_NONE) == 0) *entry |= PAGE_PRESENT; + if ((flags & TYPEPHP_VM_USER_WRITE) != 0) *entry |= PAGE_WRITE; + if ((flags & TYPEPHP_VM_USER_EXECUTE) != 0) *entry &= ~PAGE_NX; + if (typephp_vm_current() == address_space) { + __asm__ volatile("invlpg (%0)" : : "r"((void *) (uintptr_t) cursor) : "memory"); + } + } + return 1; +} + +int typephp_vm_user_range( + uint64_t address_space, uint64_t address, uint64_t size, int writable) +{ + uint64_t begin; + uint64_t end; + uint64_t *pd = page_directory(address_space); + if (size == 0) { + return 1; + } + if (pd == 0 || !align_range(address, size, &begin, &end) + || begin < UINT64_C(0x2000000) || end > UINT64_C(0x2400000)) { + return 0; + } + for (uint64_t cursor = begin; cursor < end; cursor += PAGE_SIZE) { + const uint64_t pd_index = (cursor >> 21) & 0x1ffu; + const uint64_t pt_index = (cursor >> 12) & 0x1ffu; + if ((pd[pd_index] & (PAGE_PRESENT | PAGE_USER)) != (PAGE_PRESENT | PAGE_USER) + || (pd[pd_index] & PAGE_HUGE) != 0) { + return 0; + } + uint64_t *pt = page_table(pd[pd_index]); + if ((pt[pt_index] & (PAGE_PRESENT | PAGE_USER)) != (PAGE_PRESENT | PAGE_USER) + || (writable && (pt[pt_index] & PAGE_WRITE) == 0)) { + return 0; + } + } + return 1; +} + +void typephp_vm_destroy(uint64_t address_space) +{ + uint64_t *pml4 = page_table(address_space); + uint64_t *pdp = page_table(pml4[0]); + uint64_t *pd = page_table(pdp[0]); + for (uint64_t pd_index = USER_FIRST_PDE; pd_index <= USER_LAST_PDE; ++pd_index) { + if ((pd[pd_index] & PAGE_PRESENT) == 0 || (pd[pd_index] & PAGE_HUGE) != 0) { + continue; + } + uint64_t *pt = page_table(pd[pd_index]); + for (uint64_t index = 0; index < 512; ++index) { + if ((pt[index] & PAGE_OWNED) != 0) { + physical_page_free(pt[index] & PAGE_ADDRESS); + } + } + physical_page_free(pd[pd_index] & PAGE_ADDRESS); + } + physical_page_free(pdp[0] & PAGE_ADDRESS); + physical_page_free(pml4[0] & PAGE_ADDRESS); + physical_page_free(address_space); +} diff --git a/examples/typephp-os/freestanding/vm.h b/examples/typephp-os/freestanding/vm.h new file mode 100644 index 00000000..0cbc10f8 --- /dev/null +++ b/examples/typephp-os/freestanding/vm.h @@ -0,0 +1,28 @@ +#ifndef TYPEPHP_OS_VM_H +#define TYPEPHP_OS_VM_H + +#include +#include + +enum { + TYPEPHP_VM_USER_WRITE = 1u << 0, + TYPEPHP_VM_USER_EXECUTE = 1u << 1, + TYPEPHP_VM_USER_NONE = 1u << 2, +}; + +uint64_t typephp_vm_create(void); +void typephp_vm_destroy(uint64_t address_space); +void typephp_vm_activate(uint64_t address_space); +uint64_t typephp_vm_current(void); +int typephp_vm_map_user( + uint64_t address_space, uint64_t address, uint64_t size, unsigned int flags); +int typephp_vm_user_range( + uint64_t address_space, uint64_t address, uint64_t size, int writable); +int typephp_vm_user_range_free( + uint64_t address_space, uint64_t address, uint64_t size); +int typephp_vm_unmap_user( + uint64_t address_space, uint64_t address, uint64_t size); +int typephp_vm_protect_user( + uint64_t address_space, uint64_t address, uint64_t size, unsigned int flags); + +#endif diff --git a/examples/typephp-os/project.yml b/examples/typephp-os/project.yml index 8c89533f..20deceee 100644 --- a/examples/typephp-os/project.yml +++ b/examples/typephp-os/project.yml @@ -6,6 +6,7 @@ sources: - freestanding/process-entry.S - freestanding/kernel.c - freestanding/memory.c + - freestanding/vm.c - freestanding/process.c - freestanding/time.c - freestanding/ata.c @@ -16,13 +17,6 @@ sources: - freestanding/abi/cxx.cpp - freestanding/abi/unimplemented.S - thirdparty/openlibm/src -objects: - - build/objects/sh-image.o - - build/objects/ls-image.o - - build/objects/cd-image.o - - build/objects/date-image.o - - build/objects/pwd-image.o - - build/objects/fault-image.o build-dir: build/generated output: build/kernel64.elf cxx-std: c++17 diff --git a/examples/typephp-os/src/fat16.php b/examples/typephp-os/src/fat16.php index 09f5a4f8..29ad6ae2 100644 --- a/examples/typephp-os/src/fat16.php +++ b/examples/typephp-os/src/fat16.php @@ -21,6 +21,16 @@ final class AtaBlockDevice { return kernel_disk_flush(); } + + public function cacheHits(): int + { + return kernel_disk_cache_hits(); + } + + public function cacheMisses(): int + { + return kernel_disk_cache_misses(); + } } /* @@ -238,6 +248,125 @@ final class Fat16Volume return -1; } + private function directoryEntryLocation(int $directoryCluster, string $fatName): int + { + if ($directoryCluster === 0) { + return $this->rootEntryLocation($fatName); + } + $cluster = $directoryCluster; + $visited = 0; + while ($cluster >= 2 && $cluster < 0xfff8 && $visited < $this->clusterCount) { + $firstSector = $this->clusterSector($cluster); + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $sectorNumber = $firstSector + $sectorIndex; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return -1; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return -1; + } + if ($first !== 0xe5 && substr($sector, $offset, 11) === $fatName) { + return $sectorNumber * 512 + $offset; + } + } + } + $cluster = $this->readFat($cluster); + $visited++; + } + return -1; + } + + private function pathEntryLocation(string $path): int + { + $length = strlen($path); + $index = 0; + $directoryCluster = 0; + while ($index < $length && $path[$index] === '/') { + $index++; + } + if ($index === $length) { + return -1; + } + while ($index < $length) { + $start = $index; + while ($index < $length && $path[$index] !== '/') { + $index++; + } + $name = substr($path, $start, $index - $start); + while ($index < $length && $path[$index] === '/') { + $index++; + } + $fatName = $this->fatName($name); + if (strlen($fatName) !== 11) { + return -1; + } + $location = $this->directoryEntryLocation($directoryCluster, $fatName); + if ($location < 0 || $index === $length) { + return $location; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) === 0) { + return -1; + } + $directoryCluster = $this->readU16($entry, 26); + } + return -1; + } + + private function pathDirectoryCluster(string $path): int + { + if ($path === '' || $path === '/' || $path === '.') { + return 0; + } + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return -1; + } + $entry = $this->readEntry($location); + return ($this->byteAt($entry, 11) & 0x10) !== 0 + ? $this->readU16($entry, 26) + : -1; + } + + private function pathWithoutTrailingSlash(string $path): string + { + $result = $path; + while (strlen($result) > 1 && $result[strlen($result) - 1] === '/') { + $result = substr($result, 0, strlen($result) - 1); + } + return $result; + } + + private function pathLeafName(string $path): string + { + $clean = $this->pathWithoutTrailingSlash($path); + $lastSlash = -1; + for ($index = 0; $index < strlen($clean); $index++) { + if ($clean[$index] === '/') { + $lastSlash = $index; + } + } + return substr($clean, $lastSlash + 1); + } + + private function pathParentCluster(string $path): int + { + $clean = $this->pathWithoutTrailingSlash($path); + $lastSlash = -1; + for ($index = 0; $index < strlen($clean); $index++) { + if ($clean[$index] === '/') { + $lastSlash = $index; + } + } + if ($lastSlash <= 0) { + return 0; + } + return $this->pathDirectoryCluster(substr($clean, 0, $lastSlash)); + } + private function freeRootEntryLocation(): int { for ($sectorIndex = 0; $sectorIndex < $this->rootSectorCount; $sectorIndex++) { @@ -256,6 +385,48 @@ final class Fat16Volume return -1; } + private function freeDirectoryEntryLocation(int $directoryCluster): int + { + if ($directoryCluster === 0) { + return $this->freeRootEntryLocation(); + } + $cluster = $directoryCluster; + $lastCluster = -1; + $visited = 0; + while ($cluster >= 2 && $cluster < 0xfff8 && $visited < $this->clusterCount) { + $lastCluster = $cluster; + $firstSector = $this->clusterSector($cluster); + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $sectorNumber = $firstSector + $sectorIndex; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return -1; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00 || $first === 0xe5) { + return $sectorNumber * 512 + $offset; + } + } + } + $cluster = $this->readFat($cluster); + $visited++; + } + if ($lastCluster < 2) { + return -1; + } + $newCluster = $this->findFreeCluster(2); + if ($newCluster < 0 || !$this->writeFat($newCluster, 0xffff)) { + return -1; + } + if (!$this->clearCluster($newCluster) + || !$this->writeFat($lastCluster, $newCluster)) { + $this->writeFat($newCluster, 0); + return -1; + } + return $this->clusterSector($newCluster) * 512; + } + private function readEntry(int $location): string { $sectorNumber = $this->quotient($location, 512); @@ -355,6 +526,44 @@ final class Fat16Volume return true; } + private function deleteEntry(int $location): bool + { + $sectorNumber = $this->quotient($location, 512); + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return false; + } + $sector[$location % 512] = chr(0xe5); + return $this->device->writeSector($sectorNumber, $sector); + } + + private function directoryIsEmpty(int $directoryCluster): bool + { + $cluster = $directoryCluster; + $visited = 0; + while ($cluster >= 2 && $cluster < 0xfff8 && $visited < $this->clusterCount) { + $firstSector = $this->clusterSector($cluster); + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $sector = $this->device->readSector($firstSector + $sectorIndex); + if (strlen($sector) !== 512) { + return false; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return true; + } + if ($first !== 0xe5 && $first !== 0x2e) { + return false; + } + } + } + $cluster = $this->readFat($cluster); + $visited++; + } + return true; + } + public function hasRootEntry(string $name): bool { $fatName = $this->fatName($name); @@ -391,6 +600,126 @@ final class Fat16Volume : $this->readU32($entry, 28); } + public function pathEntryType(string $path): int + { + if ($path === '' || $path === '/' || $path === '.') { + return 2; + } + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return 0; + } + $entry = $this->readEntry($location); + return ($this->byteAt($entry, 11) & 0x10) !== 0 ? 2 : 1; + } + + public function pathFileSize(string $path): int + { + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return -1; + } + $entry = $this->readEntry($location); + return ($this->byteAt($entry, 11) & 0x10) !== 0 + ? 0 + : $this->readU32($entry, 28); + } + + public function makePathDirectory(string $path): bool + { + $name = $this->pathLeafName($path); + $fatName = $this->fatName($name); + $parentCluster = $this->pathParentCluster($path); + if (strlen($fatName) !== 11 || $parentCluster < 0 + || $this->directoryEntryLocation($parentCluster, $fatName) >= 0) { + return false; + } + $location = $this->freeDirectoryEntryLocation($parentCluster); + $cluster = $this->findFreeCluster(2); + if ($location < 0 || $cluster < 0 || !$this->writeFat($cluster, 0xffff) + || !$this->clearCluster($cluster)) { + return false; + } + $sector = $this->device->readSector($this->clusterSector($cluster)); + $dot = '. '; + $dotDot = '.. '; + for ($index = 0; $index < 11; $index++) { + $sector[$index] = $dot[$index]; + $sector[32 + $index] = $dotDot[$index]; + } + $sector[11] = chr(0x10); + $sector[43] = chr(0x10); + $this->writeU16($sector, 26, $cluster); + $this->writeU16($sector, 58, $parentCluster); + if (!$this->device->writeSector($this->clusterSector($cluster), $sector) + || !$this->writeEntry($location, $fatName, 0x10, $cluster, 0)) { + $this->writeFat($cluster, 0); + return false; + } + return $this->device->flush(); + } + + public function writePathFile(string $path, string $contents): bool + { + $fatName = $this->fatName($this->pathLeafName($path)); + $parentCluster = $this->pathParentCluster($path); + if (strlen($fatName) !== 11 || $parentCluster < 0) { + return false; + } + $location = $this->directoryEntryLocation($parentCluster, $fatName); + if ($location >= 0) { + $oldEntry = $this->readEntry($location); + if (($this->byteAt($oldEntry, 11) & 0x10) !== 0 + || !$this->freeChain($this->readU16($oldEntry, 26))) { + return false; + } + } else { + $location = $this->freeDirectoryEntryLocation($parentCluster); + if ($location < 0) { + return false; + } + } + + $clusterSize = $this->sectorsPerCluster * 512; + $needed = $this->ceilQuotient(strlen($contents), $clusterSize); + $clusters = std::vector(Type::Int); + $candidate = 2; + for ($index = 0; $index < $needed; $index++) { + $candidate = $this->findFreeCluster($candidate); + if ($candidate < 0) { + for ($rollback = 0; $rollback < count($clusters); $rollback++) { + $this->writeFat($clusters[$rollback], 0); + } + return false; + } + $clusters[] = $candidate; + if (!$this->writeFat($candidate, 0xffff)) { + return false; + } + $candidate++; + } + for ($index = 0; $index < count($clusters); $index++) { + $cluster = $clusters[$index]; + $next = $index + 1 < count($clusters) ? $clusters[$index + 1] : 0xffff; + if (!$this->writeFat($cluster, $next)) { + return false; + } + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $dataOffset = $index * $clusterSize + $sectorIndex * 512; + $part = substr($contents, $dataOffset, 512); + if (!$this->device->writeSector( + $this->clusterSector($cluster) + $sectorIndex, + $this->sectorData($part) + )) { + return false; + } + } + } + $firstCluster = count($clusters) === 0 ? 0 : $clusters[0]; + return $this->writeEntry($location, $fatName, 0x20, $firstCluster, strlen($contents)) + && $this->device->flush(); + } + public function makeRootDirectory(string $name): bool { $fatName = $this->fatName($name); @@ -521,6 +850,38 @@ final class Fat16Volume return $remaining === 0 ? $result : ''; } + public function readPathFile(string $path): string + { + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return ''; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) !== 0) { + return ''; + } + $remaining = $this->readU32($entry, 28); + $cluster = $this->readU16($entry, 26); + $result = ''; + $visited = 0; + while ($remaining > 0 && $cluster >= 2 && $cluster < 0xfff8 + && $visited < $this->clusterCount) { + $firstSector = $this->clusterSector($cluster); + for ($index = 0; $index < $this->sectorsPerCluster && $remaining > 0; $index++) { + $sector = $this->device->readSector($firstSector + $index); + if (strlen($sector) !== 512) { + return ''; + } + $partLength = $remaining < 512 ? $remaining : 512; + $result .= substr($sector, 0, $partLength); + $remaining -= $partLength; + } + $cluster = $this->readFat($cluster); + $visited++; + } + return $remaining === 0 ? $result : ''; + } + public function removeRootFile(string $name): bool { $fatName = $this->fatName($name); @@ -543,6 +904,20 @@ final class Fat16Volume && $this->device->flush(); } + public function removePathFile(string $path): bool + { + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return false; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) !== 0 + || !$this->freeChain($this->readU16($entry, 26))) { + return false; + } + return $this->deleteEntry($location) && $this->device->flush(); + } + public function renameRootEntry(string $oldName, string $newName): bool { $oldFatName = $this->fatName($oldName); @@ -565,6 +940,32 @@ final class Fat16Volume && $this->device->flush(); } + public function renamePathEntry(string $oldPath, string $newPath): bool + { + $oldParent = $this->pathParentCluster($oldPath); + $newParent = $this->pathParentCluster($newPath); + $newFatName = $this->fatName($this->pathLeafName($newPath)); + if ($oldParent < 0 || $newParent !== $oldParent || strlen($newFatName) !== 11 + || $this->directoryEntryLocation($newParent, $newFatName) >= 0) { + return false; + } + $location = $this->pathEntryLocation($oldPath); + if ($location < 0) { + return false; + } + $sectorNumber = $this->quotient($location, 512); + $offset = $location % 512; + $sector = $this->device->readSector($sectorNumber); + if (strlen($sector) !== 512) { + return false; + } + for ($index = 0; $index < 11; $index++) { + $sector[$offset + $index] = $newFatName[$index]; + } + return $this->device->writeSector($sectorNumber, $sector) + && $this->device->flush(); + } + public function removeRootDirectory(string $name): bool { $fatName = $this->fatName($name); @@ -597,6 +998,23 @@ final class Fat16Volume && $this->device->flush(); } + public function removePathDirectory(string $path): bool + { + $location = $this->pathEntryLocation($path); + if ($location < 0) { + return false; + } + $entry = $this->readEntry($location); + if (($this->byteAt($entry, 11) & 0x10) === 0) { + return false; + } + $cluster = $this->readU16($entry, 26); + if (!$this->directoryIsEmpty($cluster) || !$this->freeChain($cluster)) { + return false; + } + return $this->deleteEntry($location) && $this->device->flush(); + } + public function rootEntryNames(): string { $result = ''; @@ -620,6 +1038,44 @@ final class Fat16Volume return $result; } + public function pathEntryNames(string $path): string + { + $directoryCluster = $this->pathDirectoryCluster($path); + if ($directoryCluster < 0) { + return ''; + } + if ($directoryCluster === 0) { + return $this->rootEntryNames(); + } + $result = ''; + $cluster = $directoryCluster; + $visited = 0; + while ($cluster >= 2 && $cluster < 0xfff8 && $visited < $this->clusterCount) { + $firstSector = $this->clusterSector($cluster); + for ($sectorIndex = 0; $sectorIndex < $this->sectorsPerCluster; $sectorIndex++) { + $sector = $this->device->readSector($firstSector + $sectorIndex); + if (strlen($sector) !== 512) { + return ''; + } + for ($offset = 0; $offset < 512; $offset += 32) { + $first = $this->byteAt($sector, $offset); + if ($first === 0x00) { + return $result; + } + $attributes = $this->byteAt($sector, $offset + 11); + if ($first === 0xe5 || $first === 0x2e || $attributes === 0x0f + || ($attributes & 0x08) !== 0) { + continue; + } + $result .= $this->displayName(substr($sector, $offset, 11)) . chr(10); + } + } + $cluster = $this->readFat($cluster); + $visited++; + } + return $result; + } + public function rootListing(): string { $result = ''; diff --git a/examples/typephp-os/src/filesystem.php b/examples/typephp-os/src/filesystem.php index 29109d56..5f4d0b47 100644 --- a/examples/typephp-os/src/filesystem.php +++ b/examples/typephp-os/src/filesystem.php @@ -70,12 +70,11 @@ final class KernelFileSystem if (!$this->initialize()) { return -5; } - $name = $this->rootName($path); - if (strlen($name) === 0) { - return -21; - } + $rootName = $this->rootName($path); $volume = $this->volume->toObject(Fat16Volume::class); - $type = $volume->rootEntryType($name); + $type = strlen($rootName) === 0 + ? $volume->pathEntryType($path) + : $volume->rootEntryType($rootName); if ($type === 2) { return -21; } @@ -83,10 +82,15 @@ final class KernelFileSystem $truncate = ($flags & 512) !== 0; $append = ($flags & 1024) !== 0; $writable = ($flags & 3) !== 0; + $storedName = $path; if ($type === 0 && !$create) { return -2; } - $contents = $type === 1 ? $volume->readRootFile($name) : ''; + $contents = $type === 1 + ? (strlen($rootName) === 0 + ? $volume->readPathFile($path) + : $volume->readRootFile($rootName)) + : ''; if ($truncate && $writable) { $contents = ''; } @@ -98,7 +102,7 @@ final class KernelFileSystem return -24; } } - $file = new KernelOpenFile($name, $contents, $offset, $writable); + $file = new KernelOpenFile($storedName, $contents, $offset, $writable); $file->dirty = $type === 0 || ($truncate && $writable); $this->files[$fd] = $file; return $fd; @@ -111,8 +115,14 @@ final class KernelFileSystem } $file = $this->opened($fd); $volume = $this->volume->toObject(Fat16Volume::class); - if ($file->dirty && !$volume->writeRootFile($file->name, $file->contents)) { - return -5; + if ($file->dirty) { + $rootName = $this->rootName($file->name); + $written = strlen($rootName) === 0 + ? $volume->writePathFile($file->name, $file->contents) + : $volume->writeRootFile($rootName, $file->contents); + if (!$written) { + return -5; + } } unset($this->files[$fd]); return 0; @@ -179,7 +189,11 @@ final class KernelFileSystem $file = $this->opened($fd); if ($file->dirty) { $volume = $this->volume->toObject(Fat16Volume::class); - if (!$volume->writeRootFile($file->name, $file->contents)) { + $rootName = $this->rootName($file->name); + $written = strlen($rootName) === 0 + ? $volume->writePathFile($file->name, $file->contents) + : $volume->writeRootFile($rootName, $file->contents); + if (!$written) { return -5; } $file->dirty = false; @@ -215,12 +229,15 @@ final class KernelFileSystem if ($path === '/' || $path === '') { return 0; } - $name = $this->rootName($path); + $rootName = $this->rootName($path); $volume = $this->volume->toObject(Fat16Volume::class); - if (strlen($name) === 0 || !$volume->hasRootEntry($name)) { + if (strlen($rootName) !== 0) { + return $volume->hasRootEntry($rootName) ? $volume->rootFileSize($rootName) : -2; + } + if ($volume->pathEntryType($path) === 0) { return -2; } - return $volume->rootFileSize($name); + return $volume->pathFileSize($path); } public function pathType(string $path): int @@ -231,9 +248,11 @@ final class KernelFileSystem if ($path === '/' || $path === '' || $path === '.') { return 2; } - $name = $this->rootName($path); + $rootName = $this->rootName($path); $volume = $this->volume->toObject(Fat16Volume::class); - return strlen($name) === 0 ? 0 : $volume->rootEntryType($name); + return strlen($rootName) === 0 + ? $volume->pathEntryType($path) + : $volume->rootEntryType($rootName); } public function fdSize(int $fd): int @@ -244,45 +263,60 @@ final class KernelFileSystem public function makeDirectory(string $path): int { $name = $this->rootName($path); - if (!$this->initialize() || strlen($name) === 0) { - return -22; + if (!$this->initialize()) { + return -5; } $volume = $this->volume->toObject(Fat16Volume::class); - if ($volume->hasRootEntry($name)) { + $exists = strlen($name) === 0 + ? $volume->pathEntryType($path) !== 0 + : $volume->hasRootEntry($name); + if ($exists) { return -17; } - return $volume->makeRootDirectory($name) ? 0 : -5; + $created = strlen($name) === 0 + ? $volume->makePathDirectory($path) + : $volume->makeRootDirectory($name); + return $created ? 0 : -5; } public function removeDirectory(string $path): int { $name = $this->rootName($path); - if (!$this->initialize() || strlen($name) === 0) { - return -22; + if (!$this->initialize()) { + return -5; } $volume = $this->volume->toObject(Fat16Volume::class); - return $volume->removeRootDirectory($name) ? 0 : -39; + $removed = strlen($name) === 0 + ? $volume->removePathDirectory($path) + : $volume->removeRootDirectory($name); + return $removed ? 0 : -39; } public function removeFile(string $path): int { $name = $this->rootName($path); - if (!$this->initialize() || strlen($name) === 0) { - return -22; + if (!$this->initialize()) { + return -5; } $volume = $this->volume->toObject(Fat16Volume::class); - return $volume->removeRootFile($name) ? 0 : -2; + $removed = strlen($name) === 0 + ? $volume->removePathFile($path) + : $volume->removeRootFile($name); + return $removed ? 0 : -2; } public function rename(string $oldPath, string $newPath): int { $oldName = $this->rootName($oldPath); $newName = $this->rootName($newPath); - if (!$this->initialize() || strlen($oldName) === 0 || strlen($newName) === 0) { - return -22; + if (!$this->initialize()) { + return -5; } $volume = $this->volume->toObject(Fat16Volume::class); - return $volume->renameRootEntry($oldName, $newName) ? 0 : -2; + $renamed = strlen($oldName) !== 0 && strlen($newName) !== 0 + ? $volume->renameRootEntry($oldName, $newName) + : $volume->renamePathEntry($oldPath, $newPath); + return $renamed ? 0 : -2; } public function entries(string $path): string @@ -290,13 +324,9 @@ final class KernelFileSystem if (!$this->initialize() || $this->pathType($path) !== 2) { return ''; } - if ($path !== '/' && $path !== '' && $path !== '.') { - /* The first FAT16 slice has root directory entries but no nested - * traversal yet. A root child directory is nevertheless a valid - * working directory and contains its logical dot entries. */ - return ".\n..\n"; - } $volume = $this->volume->toObject(Fat16Volume::class); - return ".\n..\n" . $volume->rootEntryNames(); + return ".\n..\n" . (($path === '/' || $path === '' || $path === '.') + ? $volume->rootEntryNames() + : $volume->pathEntryNames($path)); } } diff --git a/examples/typephp-os/src/hardware.stub.php b/examples/typephp-os/src/hardware.stub.php index 8031fc24..6988ab1e 100644 --- a/examples/typephp-os/src/hardware.stub.php +++ b/examples/typephp-os/src/hardware.stub.php @@ -33,6 +33,12 @@ function kernel_disk_write_sector(int $lba, string $data): bool { return false; #[NativeFunction] function kernel_disk_flush(): bool { return false; } +#[NativeFunction] +function kernel_disk_cache_hits(): int { return 0; } + +#[NativeFunction] +function kernel_disk_cache_misses(): int { return 0; } + /** Load the embedded ELF shell and transfer control to its Ring-3 entry. */ #[NativeFunction] function kernel_process_start(): void {} diff --git a/examples/typephp-os/src/kernel.php b/examples/typephp-os/src/kernel.php index c7d18ab5..3439dcc9 100644 --- a/examples/typephp-os/src/kernel.php +++ b/examples/typephp-os/src/kernel.php @@ -103,6 +103,13 @@ function runFilesystemSelfCheck(): void } writeLine('FAT16 file: ' . $actual, 10); writeLine('FAT16 root: ' . $volume->rootListing(), 10); + $hitsBefore = $device->cacheHits(); + if ($volume->readRootFile('HELLO.TXT') !== $expected + || $device->cacheHits() <= $hitsBefore) { + writeLine('FAT16 sector cache: FAILED', 12); + return; + } + writeLine('FAT16 sector cache: OK', 10); /* Exercise the unchanged PHP standard extension and plain file-stream * implementation through the POSIX-to-TypePHP bridge. */ @@ -133,6 +140,7 @@ function runFilesystemSelfCheck(): void } echo 'PHP file stream: ', $streamActual, "\n"; echo 'PHP directory scan: ', implode(', ', $entries), "\n"; + } function runPrimeDemo(int $limit): void diff --git a/examples/typephp-os/tools/fetch-thirdparty.sh b/examples/typephp-os/tools/fetch-thirdparty.sh new file mode 100755 index 00000000..6ca8857c --- /dev/null +++ b/examples/typephp-os/tools/fetch-thirdparty.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env sh + +set -eu + +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +version=0.8.7 +archive_sha256=e328a1d59b94748b111e022bca6a9d2fc0481fb57d23c87d90f394b559d4f062 +target="${project_dir}/thirdparty/openlibm" +marker="${target}/.typephp-source-${version}" +manifest="${project_dir}/tools/openlibm-files.txt" + +if [ -f "${marker}" ] && [ -f "${target}/src/e_sqrt.c" ]; then + exit 0 +fi + +temporary=$(mktemp -d "${TMPDIR:-/tmp}/typephp-os-thirdparty.XXXXXX") +trap 'rm -rf "${temporary}"' EXIT HUP INT TERM +archive="${temporary}/openlibm.tar.gz" +source_dir="${temporary}/openlibm-${version}" +staging="${temporary}/installed" + +echo "Downloading OpenLibm v${version}" +curl -L --fail --retry 3 \ + -o "${archive}" \ + "https://github.com/JuliaMath/openlibm/archive/refs/tags/v${version}.tar.gz" +echo "${archive_sha256} ${archive}" | sha256sum -c - +tar -xzf "${archive}" -C "${temporary}" + +while IFS= read -r relative; do + [ -n "${relative}" ] || continue + mkdir -p "${staging}/$(dirname -- "${relative}")" + cp "${source_dir}/${relative}" "${staging}/${relative}" +done < "${manifest}" + +mkdir -p "${project_dir}/thirdparty" +rm -rf "${target}" +mv "${staging}" "${target}" +printf '%s\n' "${archive_sha256}" > "${marker}" +echo "Installed OpenLibm v${version} in ${target}" diff --git a/examples/typephp-os/tools/invalid-elf.txt b/examples/typephp-os/tools/invalid-elf.txt new file mode 100644 index 00000000..e8d0ac19 --- /dev/null +++ b/examples/typephp-os/tools/invalid-elf.txt @@ -0,0 +1 @@ +not-an-elf diff --git a/examples/typephp-os/tools/openlibm-files.txt b/examples/typephp-os/tools/openlibm-files.txt new file mode 100644 index 00000000..d1d83656 --- /dev/null +++ b/examples/typephp-os/tools/openlibm-files.txt @@ -0,0 +1,73 @@ +LICENSE.md +amd64/bsd_asm.h +amd64/bsd_fpu.h +amd64/bsd_ieeefp.h +include/openlibm.h +include/openlibm_complex.h +include/openlibm_defs.h +include/openlibm_fenv.h +include/openlibm_fenv_aarch64.h +include/openlibm_fenv_amd64.h +include/openlibm_fenv_arm.h +include/openlibm_fenv_i387.h +include/openlibm_fenv_loongarch64.h +include/openlibm_fenv_mips.h +include/openlibm_fenv_powerpc.h +include/openlibm_fenv_riscv.h +include/openlibm_fenv_s390.h +include/openlibm_math.h +src/aarch64_fpmath.h +src/amd64_fpmath.h +src/bsd_cdefs.h +src/cdefs-compat.h +src/e_acos.c +src/e_acosh.c +src/e_asin.c +src/e_atan2.c +src/e_atanh.c +src/e_cosh.c +src/e_exp.c +src/e_fmod.c +src/e_hypot.c +src/e_log.c +src/e_log10.c +src/e_log2.c +src/e_pow.c +src/e_rem_pio2.c +src/e_sinh.c +src/e_sqrt.c +src/fpmath.h +src/i386_fpmath.h +src/k_cos.c +src/k_exp.c +src/k_log.h +src/k_logf.h +src/k_rem_pio2.c +src/k_sin.c +src/k_tan.c +src/loongarch64_fpmath.h +src/math_private.h +src/math_private_openbsd.h +src/mips_fpmath.h +src/powerpc_fpmath.h +src/riscv_fpmath.h +src/s390_fpmath.h +src/s_asinh.c +src/s_atan.c +src/s_ceil.c +src/s_cimag.c +src/s_copysign.c +src/s_cos.c +src/s_creal.c +src/s_expm1.c +src/s_fabs.c +src/s_floor.c +src/s_log1p.c +src/s_nextafter.c +src/s_round.c +src/s_scalbn.c +src/s_sin.c +src/s_tan.c +src/s_tanh.c +src/s_trunc.c +src/types-compat.h diff --git a/examples/typephp-os/tools/test-qemu.sh b/examples/typephp-os/tools/test-qemu.sh index 0c0d9d54..98655449 100755 --- a/examples/typephp-os/tools/test-qemu.sh +++ b/examples/typephp-os/tools/test-qemu.sh @@ -6,7 +6,7 @@ kernel=${1:?kernel ELF is required} disk=${2:?FAT16 disk image is required} log=${3:?log path is required} -if timeout 8 qemu-system-x86_64 \ +if timeout 20 qemu-system-x86_64 \ -kernel "${kernel}" \ -drive file="${disk}",format=raw,if=ide,index=0 \ -display none \ @@ -16,7 +16,7 @@ if timeout 8 qemu-system-x86_64 \ -no-shutdown \ -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ >"${log}" 2>&1 \ - <<< $'\ndate\npwd\nls\ncd DOCS\npwd\nls\ncd ..\nls\n'; then + <<< $'\ndate\necho Hello TypePHP userspace\nhello Dynamically loaded\nmissing\nbad\nls /BIN\ncat HELLO.TXT\ntouch /EXPAND/F62.TXT\nls /EXPAND\nwrite NOTE.TXT Hello from Ring 3\ncat NOTE.TXT\ntouch EMPTY.TXT\nmkdir TMP\nls\nrm NOTE.TXT\ncat NOTE.TXT\nrmdir TMP\ncd TMP\nmkdir WORK\ncd WORK\nwrite NOTE.TXT Nested directory write\ncat NOTE.TXT\nmkdir SUB\ncd SUB\nwrite DEEP.TXT Deep directory write\nmv DEEP.TXT MOVED.TXT\ncat MOVED.TXT\ncat DEEP.TXT\npwd\ncd ..\nrmdir SUB\nrm SUB/MOVED.TXT\nrmdir SUB\nls\nrm NOTE.TXT\ncd ..\nrmdir WORK\ncd WORK\nmemtest\nfault\nvmfault\nwrfault\ndate\npwd\ncd BIN\npwd\nls\ncd ..\ncd DOCS\npwd\nls\ncd ..\nls\n'; then status=0 else status=$? @@ -36,19 +36,46 @@ grep -q "Zend string/array: OK" "${log}" grep -q "Kernel is!" "${log}" grep -q "OpenLibm math: OK" "${log}" grep -q "FAT16 file: Hello from TypePHP FAT16!" "${log}" -grep -q "FAT16 root: DATA/, HELLO.TXT" "${log}" +grep -q "FAT16 root: BIN/" "${log}" +grep -q "FAT16 sector cache: OK" "${log}" grep -q "PHP file stream: PHP stream through TypePHP FAT16" "${log}" -grep -q "PHP directory scan: ., .., DATA, DOCS, HELLO.TXT, STREAM.TXT" "${log}" +grep -q "PHP directory scan: ., ..," "${log}" grep -q "Calculate primes: 0-100" "${log}" grep -q "Prime count: 25" "${log}" grep -q "Prime list: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97" "${log}" grep -q "Process 1: sh.elf (Ring 3)" "${log}" grep -q "TypePHP-OS user shell" "${log}" grep -q "Ring 3 confirmed" "${log}" -grep -q "Commands: ls, cd , pwd, date" "${log}" +grep -q "Commands: ls, cd, pwd, date, cat, echo, write, touch, mkdir, rm, rmdir, mv, memtest, fault, vmfault, wrfault" "${log}" grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} UTC' "${log}" +grep -q '^Hello TypePHP userspace' "${log}" +grep -q '^Dynamically loaded' "${log}" +grep -q '^sh: missing: No such file or directory' "${log}" +grep -q '^sh: bad: Exec format error' "${log}" +grep -q '^/BIN' "${log}" +grep -q '^Hello from TypePHP FAT16!' "${log}" +grep -q '^F62.TXT' "${log}" +grep -q '^Hello from Ring 3' "${log}" +grep -q '^EMPTY.TXT' "${log}" +grep -q '^TMP' "${log}" +grep -q '^cat: No such file or directory' "${log}" +grep -q '^cd: no such directory' "${log}" +grep -q '^Nested directory write' "${log}" +grep -q '^Deep directory write' "${log}" +grep -q '^typephp-os:/WORK/SUB\$ cat DEEP.TXT' "${log}" +grep -q '^/WORK/SUB' "${log}" +grep -q '^rmdir: Directory not empty' "${log}" +grep -Fq 'typephp-os:/WORK$ ' "${log}" +grep -q '^brk/mmap: OK' "${log}" +grep -q 'User process 2 fault: invalid opcode (#6)' "${log}" +grep -q 'sh: fault: Input/output error' "${log}" +grep -q 'User process 2 fault: page fault (#14) .*address 0x0000000002000000' "${log}" +grep -q 'sh: vmfault: Input/output error' "${log}" +grep -q 'User process 2 fault: page fault (#14) .*address 0x00000000022ef000' "${log}" +grep -q 'sh: wrfault: Input/output error' "${log}" grep -Fq 'typephp-os:/$ ' "${log}" grep -Fq 'typephp-os:/DOCS$ ' "${log}" +grep -Fq 'typephp-os:/BIN$ ' "${log}" grep -q '^/DOCS' "${log}" grep -q '^HELLO.TXT' "${log}" cat "${log}" diff --git a/examples/typephp-os/user/README.md b/examples/typephp-os/user/README.md new file mode 100644 index 00000000..4bcf6925 --- /dev/null +++ b/examples/typephp-os/user/README.md @@ -0,0 +1,67 @@ +# TypePHP-OS userspace direction + +TypePHP-OS userspace must evolve toward programs that GCC and glibc can build +and run directly. Every userspace ABI and API change must make that migration +easier, or clearly isolate temporary OS-specific behavior. + +The following rules are normative for this directory: + +1. C programs use the standard `int main(int argc, char **argv)` entry point. + The shared `crt0.S` owns `_start` and receives a Linux-style initial stack: + `argc`, `argv`, `envp`, and `auxv`. +2. Prefer ISO C and POSIX APIs and data structures. Small temporary wrappers + should match their eventual libc signatures wherever the kernel already has + the required semantics. +3. A Linux x86-64 syscall number may be used only when the operation, argument + layout, return value, and observable semantics are compatible. Temporary + TypePHP-OS services use the private syscall range instead. +4. Keep OS-specific calls behind the userspace ABI layer. Application and + command code should gradually stop including raw syscall helpers as libc + coverage grows. +5. New kernel services should first consider the interfaces glibc needs: + process startup, memory mapping, files and directories, clocks, TLS, + signals, and process lifecycle. Socket support remains out of scope for the + current system. +6. Freestanding test programs remain libc-free only as a bootstrap constraint; + it is not the final userspace programming model. + +The current bootstrap libc provides `syscall`, `read`, `write`, `openat`, +`open`, `close`, `lseek`, `getcwd`, `chdir`, `mkdir`, `rmdir`, `unlink`, +`rename`, `time`, `brk`, `sbrk`, `mmap`, `mprotect`, `munmap`, `strlen`, +`strerror`, `perror`, and `_exit` with libc-compatible C signatures. It also +translates kernel `-errno` results into `-1` plus the single-task userspace +`errno`. This list is a migration layer, not a reason to create +project-specific variants of standard functions. + +The resident shell and each launched command have independent x86-64 address +spaces backed by recyclable 4 KiB physical pages. Current ELF files use +page-separated `RX` text/rodata and `RW` data/bss segments; the 64 KiB user +stack has an unmapped guard below it. The memory syscalls reserve their Linux +x86-64 numbers and semantics, with one deliberate initial subset: `mmap()` +accepts only `MAP_PRIVATE | MAP_ANONYMOUS`, `fd == -1`, and offset zero. File +mappings, shared mappings, fixed mappings, remapping, and demand paging are not +implemented yet. `brk()` and `mmap()` eagerly allocate zero-filled pages; +`munmap()` and process teardown return them to the physical-page pool. + +`rename()` currently accepts only source and destination paths with the same +parent directory and does not replace an existing entry. Its transport uses a +private TypePHP-OS syscall number until the kernel implements the complete +Linux rename contract. + +The shell and commands are independent ELF64 files installed under `/BIN` in +the FAT16 image. Command names must fit the DOS 8.3 base-name limit. Lookup maps `name` to the path +`/BIN/name.ELF`. The kernel streams and validates each ELF from the filesystem +and confines the resident shell and transient command to separate page tables, +even when they use overlapping virtual addresses. Adding a compatible command +file does not require relinking the kernel. + +The current `int 0x80` entry is transitional. Before linking an ordinary +x86-64 glibc build, the kernel must also accept the `syscall` instruction with +the Linux register convention (`rax`, `rdi`, `rsi`, `rdx`, `r10`, `r8`, `r9`), +negative errno returns, and the expected `rcx`/`r11` clobbers. + +Using unmodified upstream glibc will require substantially more than matching +syscall numbers. The kernel must eventually provide the expected ELF process +startup contract and enough Linux-compatible syscall behavior, or TypePHP-OS +must carry a small glibc `sysdeps` port. Until then, private behavior must not +masquerade as a Linux syscall. diff --git a/examples/typephp-os/user/cat.c b/examples/typephp-os/user/cat.c new file mode 100644 index 00000000..d2b1f8ed --- /dev/null +++ b/examples/typephp-os/user/cat.c @@ -0,0 +1,33 @@ +#include +#include +#include + +int main(int argc, char **argv) +{ + char buffer[256]; + ssize_t size; + int fd; + if (argc != 2) { + (void) write(STDERR_FILENO, "usage: cat FILE\n", sizeof("usage: cat FILE\n") - 1); + return 1; + } + fd = open(argv[1], O_RDONLY); + if (fd < 0) { + perror("cat"); + return 1; + } + while ((size = read(fd, buffer, sizeof(buffer))) > 0) { + ssize_t offset = 0; + while (offset < size) { + ssize_t written = write(STDOUT_FILENO, buffer + offset, + (size_t) (size - offset)); + if (written <= 0) { + (void) close(fd); + return 1; + } + offset += written; + } + } + (void) close(fd); + return size < 0 ? 1 : 0; +} diff --git a/examples/typephp-os/user/cd.c b/examples/typephp-os/user/cd.c index 55ff0af8..15f5585f 100644 --- a/examples/typephp-os/user/cd.c +++ b/examples/typephp-os/user/cd.c @@ -1,11 +1,12 @@ -#include "syscall.h" +#include -void _start(const char *argument) +int main(int argc, char **argv) { - const char *path = argument != 0 && argument[0] != '\0' ? argument : "/"; - if (typephp_syscall(TYPEPHP_SYS_CHDIR, (long) path, 0, 0) != 0) { - typephp_write("cd: no such directory\n"); - typephp_exit(1); + const char *path = argc > 1 ? argv[1] : "/"; + if (chdir(path) != 0) { + (void) write(STDERR_FILENO, "cd: no such directory\n", + sizeof("cd: no such directory\n") - 1); + return 1; } - typephp_exit(0); + return 0; } diff --git a/examples/typephp-os/user/crt0.S b/examples/typephp-os/user/crt0.S new file mode 100644 index 00000000..393476bf --- /dev/null +++ b/examples/typephp-os/user/crt0.S @@ -0,0 +1,23 @@ +.section .text +.code64 + +.global _start +.type _start, @function +.extern main +.extern _exit +_start: + # Linux-compatible process entry stack: + # argc, argv[], NULL, envp[], NULL, auxv[] + xor %ebp, %ebp + mov (%rsp), %rdi + lea 8(%rsp), %rsi + lea 16(%rsp,%rdi,8), %rdx + and $-16, %rsp + call main + + mov %eax, %edi + call _exit + ud2 +.size _start, . - _start + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/user/date.c b/examples/typephp-os/user/date.c index 695dbed2..811223ae 100644 --- a/examples/typephp-os/user/date.c +++ b/examples/typephp-os/user/date.c @@ -1,4 +1,6 @@ -#include "syscall.h" +#include +#include +#include static int leap_year(long year) { @@ -19,14 +21,14 @@ static void append_four_digits(char *output, size_t *offset, long value) output[(*offset)++] = (char) ('0' + value % 10); } -void _start(const char *argument) +int main(int argc, char **argv) { static const int month_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, }; char output[25]; size_t offset = 0; - long seconds = typephp_syscall(TYPEPHP_SYS_TIME, 0, 0, 0); + long seconds = time(NULL); long days; long day_seconds; long year = 1970; @@ -34,10 +36,12 @@ void _start(const char *argument) int days_in_year; int days_in_month; - (void) argument; + (void) argc; + (void) argv; if (seconds < 0) { - typephp_write("date: clock unavailable\n"); - typephp_exit(1); + (void) write(STDERR_FILENO, "date: clock unavailable\n", + sizeof("date: clock unavailable\n") - 1); + return 1; } days = seconds / 86400; day_seconds = seconds % 86400; @@ -77,6 +81,6 @@ void _start(const char *argument) output[offset++] = 'T'; output[offset++] = 'C'; output[offset++] = '\n'; - typephp_write_bytes(output, offset); - typephp_exit(0); + (void) write(STDOUT_FILENO, output, offset); + return 0; } diff --git a/examples/typephp-os/user/echo.c b/examples/typephp-os/user/echo.c new file mode 100644 index 00000000..3be736e8 --- /dev/null +++ b/examples/typephp-os/user/echo.c @@ -0,0 +1,14 @@ +#include +#include + +int main(int argc, char **argv) +{ + for (int index = 1; index < argc; ++index) { + if (index != 1) { + (void) write(STDOUT_FILENO, " ", 1); + } + (void) write(STDOUT_FILENO, argv[index], strlen(argv[index])); + } + (void) write(STDOUT_FILENO, "\n", 1); + return 0; +} diff --git a/examples/typephp-os/user/fault.c b/examples/typephp-os/user/fault.c new file mode 100644 index 00000000..89b41cf5 --- /dev/null +++ b/examples/typephp-os/user/fault.c @@ -0,0 +1,9 @@ +/* Development-only command used by the QEMU smoke test to verify that a + * broken Ring-3 command is terminated without taking down the kernel. */ +int main(int argc, char **argv) +{ + (void) argc; + (void) argv; + __asm__ volatile("ud2"); + return 1; +} diff --git a/examples/typephp-os/user/include/errno.h b/examples/typephp-os/user/include/errno.h new file mode 100644 index 00000000..93d647b0 --- /dev/null +++ b/examples/typephp-os/user/include/errno.h @@ -0,0 +1,24 @@ +#ifndef TYPEPHP_OS_USER_ERRNO_H +#define TYPEPHP_OS_USER_ERRNO_H + +extern int errno; + +#define ENOENT 2 +#define EIO 5 +#define ENOEXEC 8 +#define EBADF 9 +#define ENOMEM 12 +#define EACCES 13 +#define EFAULT 14 +#define EEXIST 17 +#define ENOTDIR 20 +#define EISDIR 21 +#define EINVAL 22 +#define EMFILE 24 +#define EROFS 30 +#define ERANGE 34 +#define ENAMETOOLONG 36 +#define ENOSYS 38 +#define ENOTEMPTY 39 + +#endif diff --git a/examples/typephp-os/user/include/fcntl.h b/examples/typephp-os/user/include/fcntl.h new file mode 100644 index 00000000..cb848232 --- /dev/null +++ b/examples/typephp-os/user/include/fcntl.h @@ -0,0 +1,16 @@ +#ifndef TYPEPHP_OS_USER_FCNTL_H +#define TYPEPHP_OS_USER_FCNTL_H + +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 64 +#define O_TRUNC 512 +#define O_APPEND 1024 + +#define AT_FDCWD (-100) + +int open(const char *path, int flags, ...); +int openat(int directory_fd, const char *path, int flags, ...); + +#endif diff --git a/examples/typephp-os/user/include/stdio.h b/examples/typephp-os/user/include/stdio.h new file mode 100644 index 00000000..82142d27 --- /dev/null +++ b/examples/typephp-os/user/include/stdio.h @@ -0,0 +1,7 @@ +#ifndef TYPEPHP_OS_USER_STDIO_H +#define TYPEPHP_OS_USER_STDIO_H + +void perror(const char *prefix); +int rename(const char *old_path, const char *new_path); + +#endif diff --git a/examples/typephp-os/user/include/string.h b/examples/typephp-os/user/include/string.h new file mode 100644 index 00000000..2dac42a9 --- /dev/null +++ b/examples/typephp-os/user/include/string.h @@ -0,0 +1,9 @@ +#ifndef TYPEPHP_OS_USER_STRING_H +#define TYPEPHP_OS_USER_STRING_H + +#include + +size_t strlen(const char *string); +char *strerror(int error); + +#endif diff --git a/examples/typephp-os/user/include/sys/mman.h b/examples/typephp-os/user/include/sys/mman.h new file mode 100644 index 00000000..446b5716 --- /dev/null +++ b/examples/typephp-os/user/include/sys/mman.h @@ -0,0 +1,19 @@ +#ifndef TYPEPHP_OS_USER_SYS_MMAN_H +#define TYPEPHP_OS_USER_SYS_MMAN_H + +#include + +#define PROT_NONE 0 +#define PROT_READ 1 +#define PROT_WRITE 2 +#define PROT_EXEC 4 + +#define MAP_PRIVATE 2 +#define MAP_ANONYMOUS 0x20 +#define MAP_FAILED ((void *) -1) + +void *mmap(void *address, size_t length, int protection, int flags, int fd, long offset); +int mprotect(void *address, size_t length, int protection); +int munmap(void *address, size_t length); + +#endif diff --git a/examples/typephp-os/user/include/sys/stat.h b/examples/typephp-os/user/include/sys/stat.h new file mode 100644 index 00000000..3e112819 --- /dev/null +++ b/examples/typephp-os/user/include/sys/stat.h @@ -0,0 +1,8 @@ +#ifndef TYPEPHP_OS_USER_SYS_STAT_H +#define TYPEPHP_OS_USER_SYS_STAT_H + +typedef unsigned int mode_t; + +int mkdir(const char *path, mode_t mode); + +#endif diff --git a/examples/typephp-os/user/include/sys/syscall.h b/examples/typephp-os/user/include/sys/syscall.h new file mode 100644 index 00000000..447ec636 --- /dev/null +++ b/examples/typephp-os/user/include/sys/syscall.h @@ -0,0 +1,31 @@ +#ifndef TYPEPHP_OS_USER_SYS_SYSCALL_H +#define TYPEPHP_OS_USER_SYS_SYSCALL_H + +#include + +#define SYS_read TYPEPHP_SYS_READ +#define SYS_write TYPEPHP_SYS_WRITE +#define SYS_close TYPEPHP_SYS_CLOSE +#define SYS_lseek TYPEPHP_SYS_LSEEK +#define SYS_mmap TYPEPHP_SYS_MMAP +#define SYS_mprotect TYPEPHP_SYS_MPROTECT +#define SYS_munmap TYPEPHP_SYS_MUNMAP +#define SYS_brk TYPEPHP_SYS_BRK +#define SYS_exit TYPEPHP_SYS_EXIT +#define SYS_getcwd TYPEPHP_SYS_GETCWD +#define SYS_chdir TYPEPHP_SYS_CHDIR +#define SYS_mkdir TYPEPHP_SYS_MKDIR +#define SYS_rmdir TYPEPHP_SYS_RMDIR +#define SYS_unlink TYPEPHP_SYS_UNLINK +#define SYS_time TYPEPHP_SYS_TIME +#define SYS_openat TYPEPHP_SYS_OPENAT + +/* Private bootstrap services; these deliberately do not claim Linux syscall + * numbers or semantics. */ +#define SYS_typephp_spawn TYPEPHP_SYS_SPAWN +#define SYS_typephp_listdir TYPEPHP_SYS_LISTDIR +#define SYS_typephp_rename TYPEPHP_SYS_RENAME + +long syscall(long number, ...); + +#endif diff --git a/examples/typephp-os/user/include/time.h b/examples/typephp-os/user/include/time.h new file mode 100644 index 00000000..118d2c60 --- /dev/null +++ b/examples/typephp-os/user/include/time.h @@ -0,0 +1,8 @@ +#ifndef TYPEPHP_OS_USER_TIME_H +#define TYPEPHP_OS_USER_TIME_H + +typedef long time_t; + +time_t time(time_t *result); + +#endif diff --git a/examples/typephp-os/user/include/unistd.h b/examples/typephp-os/user/include/unistd.h new file mode 100644 index 00000000..bf654365 --- /dev/null +++ b/examples/typephp-os/user/include/unistd.h @@ -0,0 +1,30 @@ +#ifndef TYPEPHP_OS_USER_UNISTD_H +#define TYPEPHP_OS_USER_UNISTD_H + +#include +#include + +typedef long ssize_t; +typedef long off_t; + +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +ssize_t read(int fd, void *buffer, size_t count); +ssize_t write(int fd, const void *buffer, size_t count); +int close(int fd); +off_t lseek(int fd, off_t offset, int whence); +int chdir(const char *path); +char *getcwd(char *buffer, size_t size); +int unlink(const char *path); +int rmdir(const char *path); +int brk(void *address); +void *sbrk(intptr_t increment); +void _exit(int status) __attribute__((noreturn)); + +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 + +#endif diff --git a/examples/typephp-os/user/libc.c b/examples/typephp-os/user/libc.c new file mode 100644 index 00000000..c93ba215 --- /dev/null +++ b/examples/typephp-os/user/libc.c @@ -0,0 +1,188 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int errno; + +static char unknown_error[] = "Unknown error"; + +size_t strlen(const char *string) +{ + size_t length = 0; + while (string[length] != '\0') { + ++length; + } + return length; +} + +char *strerror(int error) +{ + switch (error) { + case ENOENT: return "No such file or directory"; + case EIO: return "Input/output error"; + case ENOEXEC: return "Exec format error"; + case EBADF: return "Bad file descriptor"; + case ENOMEM: return "Cannot allocate memory"; + case EACCES: return "Permission denied"; + case EFAULT: return "Bad address"; + case EEXIST: return "File exists"; + case ENOTDIR: return "Not a directory"; + case EISDIR: return "Is a directory"; + case EINVAL: return "Invalid argument"; + case EMFILE: return "Too many open files"; + case EROFS: return "Read-only file system"; + case ERANGE: return "Numerical result out of range"; + case ENAMETOOLONG: return "File name too long"; + case ENOSYS: return "Function not implemented"; + case ENOTEMPTY: return "Directory not empty"; + default: return unknown_error; + } +} + +void perror(const char *prefix) +{ + if (prefix != NULL && prefix[0] != '\0') { + (void) write(STDERR_FILENO, prefix, strlen(prefix)); + (void) write(STDERR_FILENO, ": ", 2); + } + (void) write(STDERR_FILENO, strerror(errno), strlen(strerror(errno))); + (void) write(STDERR_FILENO, "\n", 1); +} + +ssize_t read(int fd, void *buffer, size_t count) +{ + return (ssize_t) syscall(SYS_read, fd, buffer, count); +} + +ssize_t write(int fd, const void *buffer, size_t count) +{ + return (ssize_t) syscall(SYS_write, fd, buffer, count); +} + +int close(int fd) +{ + return (int) syscall(SYS_close, fd); +} + +off_t lseek(int fd, off_t offset, int whence) +{ + return (off_t) syscall(SYS_lseek, fd, offset, whence); +} + +int openat(int directory_fd, const char *path, int flags, ...) +{ + int mode = 0; + if ((flags & O_CREAT) != 0) { + va_list arguments; + va_start(arguments, flags); + mode = va_arg(arguments, int); + va_end(arguments); + } + return (int) syscall(SYS_openat, directory_fd, path, flags, mode); +} + +int open(const char *path, int flags, ...) +{ + int mode = 0; + if ((flags & O_CREAT) != 0) { + va_list arguments; + va_start(arguments, flags); + mode = va_arg(arguments, int); + va_end(arguments); + } + return openat(AT_FDCWD, path, flags, mode); +} + +int chdir(const char *path) +{ + return (int) syscall(SYS_chdir, path); +} + +int mkdir(const char *path, mode_t mode) +{ + return (int) syscall(SYS_mkdir, path, mode); +} + +int unlink(const char *path) +{ + return (int) syscall(SYS_unlink, path); +} + +int rmdir(const char *path) +{ + return (int) syscall(SYS_rmdir, path); +} + +int rename(const char *old_path, const char *new_path) +{ + return (int) syscall(SYS_typephp_rename, old_path, new_path); +} + +int brk(void *address) +{ + long result = syscall(SYS_brk, address); + if ((void *) result != address) { + errno = ENOMEM; + return -1; + } + return 0; +} + +void *sbrk(intptr_t increment) +{ + intptr_t current = syscall(SYS_brk, 0); + intptr_t requested; + if ((increment > 0 && current > INTPTR_MAX - increment) + || (increment < 0 && current < INTPTR_MIN - increment)) { + errno = ENOMEM; + return (void *) -1; + } + requested = current + increment; + if (increment != 0 && syscall(SYS_brk, requested) != requested) { + errno = ENOMEM; + return (void *) -1; + } + return (void *) current; +} + +void *mmap(void *address, size_t length, int protection, int flags, int fd, long offset) +{ + return (void *) syscall(SYS_mmap, + address, length, protection, flags, fd, offset); +} + +int mprotect(void *address, size_t length, int protection) +{ + return (int) syscall(SYS_mprotect, address, length, protection); +} + +int munmap(void *address, size_t length) +{ + return (int) syscall(SYS_munmap, address, length); +} + +char *getcwd(char *buffer, size_t size) +{ + return syscall(SYS_getcwd, buffer, size) < 0 ? NULL : buffer; +} + +time_t time(time_t *result) +{ + return (time_t) syscall(SYS_time, result); +} + +void _exit(int status) +{ + (void) syscall(SYS_exit, status); + for (;;) { + __asm__ volatile("pause"); + } +} diff --git a/examples/typephp-os/user/linker.ld b/examples/typephp-os/user/linker.ld index 7c15bfd4..d462dd1b 100644 --- a/examples/typephp-os/user/linker.ld +++ b/examples/typephp-os/user/linker.ld @@ -2,14 +2,23 @@ OUTPUT_FORMAT(elf64-x86-64) OUTPUT_ARCH(i386:x86-64) ENTRY(_start) +PHDRS +{ + text PT_LOAD FILEHDR PHDRS FLAGS(5); + data PT_LOAD FLAGS(6); + stack PT_GNU_STACK FLAGS(6); +} + SECTIONS { . = USER_BASE + SIZEOF_HEADERS; - .text : { *(.text .text.*) } - .rodata : { *(.rodata .rodata.*) } - .data : { *(.data .data.*) } - .bss (NOLOAD) : { *(COMMON) *(.bss .bss.*) } + .text : { *(.text .text.*) } :text + .rodata : { *(.rodata .rodata.*) } :text + + . = ALIGN(4K); + .data : { *(.data .data.*) } :data + .bss (NOLOAD) : { *(COMMON) *(.bss .bss.*) } :data /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) } } diff --git a/examples/typephp-os/user/ls.c b/examples/typephp-os/user/ls.c index af1ba941..7c2ba1fc 100644 --- a/examples/typephp-os/user/ls.c +++ b/examples/typephp-os/user/ls.c @@ -1,15 +1,16 @@ -#include "syscall.h" +#include +#include -void _start(const char *argument) +int main(int argc, char **argv) { char entries[512]; - const char *path = argument != 0 && argument[0] != '\0' ? argument : 0; - long length = typephp_syscall( - TYPEPHP_SYS_READDIR, (long) path, (long) entries, sizeof(entries)); + const char *path = argc > 1 ? argv[1] : 0; + long length = syscall(SYS_typephp_listdir, path, entries, sizeof(entries)); if (length < 0) { - typephp_write("ls: cannot read directory\n"); - typephp_exit(1); + (void) write(STDERR_FILENO, "ls: cannot read directory\n", + sizeof("ls: cannot read directory\n") - 1); + return 1; } - typephp_write_bytes(entries, (size_t) length); - typephp_exit(0); + (void) write(STDOUT_FILENO, entries, (size_t) length); + return 0; } diff --git a/examples/typephp-os/user/memtest.c b/examples/typephp-os/user/memtest.c new file mode 100644 index 00000000..db7df22d --- /dev/null +++ b/examples/typephp-os/user/memtest.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include + +static int zeroed(const unsigned char *memory, size_t size) +{ + for (size_t index = 0; index < size; ++index) { + if (memory[index] != 0) { + return 0; + } + } + return 1; +} + +int main(void) +{ + unsigned char *heap = sbrk(8192); + if (heap == (void *) -1 || !zeroed(heap, 8192)) { + perror("memtest: sbrk"); + return 1; + } + heap[0] = 0x12; + heap[8191] = 0x34; + if (heap[0] != 0x12 || heap[8191] != 0x34 || sbrk(-8192) == (void *) -1) { + return 1; + } + + unsigned char *mapping = mmap(NULL, 8192, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapping == MAP_FAILED || !zeroed(mapping, 8192)) { + perror("memtest: mmap"); + return 1; + } + mapping[0] = 0x56; + mapping[4096] = 0x78; + if (mprotect(mapping + 4096, 4096, PROT_NONE) != 0 + || mprotect(mapping + 4096, 4096, PROT_READ | PROT_WRITE) != 0 + || mapping[0] != 0x56 || mapping[4096] != 0x78 + || munmap(mapping, 8192) != 0) { + perror("memtest: mapping"); + return 1; + } + (void) write(STDOUT_FILENO, "brk/mmap: OK\n", sizeof("brk/mmap: OK\n") - 1); + return 0; +} diff --git a/examples/typephp-os/user/mkdir.c b/examples/typephp-os/user/mkdir.c new file mode 100644 index 00000000..8dce94df --- /dev/null +++ b/examples/typephp-os/user/mkdir.c @@ -0,0 +1,17 @@ +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 2) { + (void) write(STDERR_FILENO, "usage: mkdir DIRECTORY\n", + sizeof("usage: mkdir DIRECTORY\n") - 1); + return 1; + } + if (mkdir(argv[1], 0777) != 0) { + (void) write(STDERR_FILENO, "mkdir: cannot create directory\n", + sizeof("mkdir: cannot create directory\n") - 1); + return 1; + } + return 0; +} diff --git a/examples/typephp-os/user/mv.c b/examples/typephp-os/user/mv.c new file mode 100644 index 00000000..00a968a3 --- /dev/null +++ b/examples/typephp-os/user/mv.c @@ -0,0 +1,16 @@ +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 3) { + (void) write(STDERR_FILENO, "usage: mv SOURCE DESTINATION\n", + sizeof("usage: mv SOURCE DESTINATION\n") - 1); + return 1; + } + if (rename(argv[1], argv[2]) != 0) { + perror("mv"); + return 1; + } + return 0; +} diff --git a/examples/typephp-os/user/pwd.c b/examples/typephp-os/user/pwd.c index 6ef4115a..5f5c92c8 100644 --- a/examples/typephp-os/user/pwd.c +++ b/examples/typephp-os/user/pwd.c @@ -1,14 +1,17 @@ -#include "syscall.h" +#include +#include -void _start(const char *argument) +int main(int argc, char **argv) { char cwd[16]; - (void) argument; - if (typephp_syscall(TYPEPHP_SYS_GETCWD, (long) cwd, sizeof(cwd), 0) < 0) { - typephp_write("pwd: working directory unavailable\n"); - typephp_exit(1); + (void) argc; + (void) argv; + if (getcwd(cwd, sizeof(cwd)) == 0) { + (void) write(STDERR_FILENO, "pwd: working directory unavailable\n", + sizeof("pwd: working directory unavailable\n") - 1); + return 1; } - typephp_write(cwd); - typephp_write("\n"); - typephp_exit(0); + (void) write(STDOUT_FILENO, cwd, strlen(cwd)); + (void) write(STDOUT_FILENO, "\n", 1); + return 0; } diff --git a/examples/typephp-os/user/rm.c b/examples/typephp-os/user/rm.c new file mode 100644 index 00000000..f1458eb4 --- /dev/null +++ b/examples/typephp-os/user/rm.c @@ -0,0 +1,15 @@ +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 2) { + (void) write(STDERR_FILENO, "usage: rm FILE\n", sizeof("usage: rm FILE\n") - 1); + return 1; + } + if (unlink(argv[1]) != 0) { + perror("rm"); + return 1; + } + return 0; +} diff --git a/examples/typephp-os/user/rmdir.c b/examples/typephp-os/user/rmdir.c new file mode 100644 index 00000000..71ed3ae7 --- /dev/null +++ b/examples/typephp-os/user/rmdir.c @@ -0,0 +1,16 @@ +#include +#include + +int main(int argc, char **argv) +{ + if (argc != 2) { + (void) write(STDERR_FILENO, "usage: rmdir DIRECTORY\n", + sizeof("usage: rmdir DIRECTORY\n") - 1); + return 1; + } + if (rmdir(argv[1]) != 0) { + perror("rmdir"); + return 1; + } + return 0; +} diff --git a/examples/typephp-os/user/sh.c b/examples/typephp-os/user/sh.c index db7166be..4388d55b 100644 --- a/examples/typephp-os/user/sh.c +++ b/examples/typephp-os/user/sh.c @@ -1,37 +1,31 @@ -#include "syscall.h" - -static int string_equal(const char *left, const char *right) -{ - while (*left != '\0' && *left == *right) { - ++left; - ++right; - } - return *left == *right; -} +#include +#include +#include +#include static size_t read_line(char *buffer, size_t capacity) { size_t length = 0; while (capacity > 1) { char character = 0; - if (typephp_syscall(TYPEPHP_SYS_READ, 0, (long) &character, 1) != 1) { + if (read(STDIN_FILENO, &character, 1) != 1) { continue; } if (character == '\r' || character == '\n') { - typephp_write("\n"); + (void) write(STDOUT_FILENO, "\n", 1); break; } if (character == 8 || character == 127) { if (length != 0) { --length; - typephp_write("\b \b"); + (void) write(STDOUT_FILENO, "\b \b", 3); } continue; } if (character >= 32 && character < 127) { buffer[length++] = character; --capacity; - typephp_write_bytes(&character, 1); + (void) write(STDOUT_FILENO, &character, 1); } } buffer[length] = '\0'; @@ -40,55 +34,69 @@ static size_t read_line(char *buffer, size_t capacity) static void show_prompt(void) { - char cwd[16]; - typephp_write("typephp-os:"); - if (typephp_syscall(TYPEPHP_SYS_GETCWD, (long) cwd, sizeof(cwd), 0) > 0) { - typephp_write(cwd); + char cwd[128]; + (void) write(STDOUT_FILENO, "typephp-os:", sizeof("typephp-os:") - 1); + if (getcwd(cwd, sizeof(cwd)) != 0) { + (void) write(STDOUT_FILENO, cwd, strlen(cwd)); } else { - typephp_write("?"); + (void) write(STDOUT_FILENO, "?", 1); } - typephp_write("$ "); + (void) write(STDOUT_FILENO, "$ ", 2); } -void _start(void) +int main(int argc, char **argv) { char line[80]; unsigned short code_selector; + (void) argc; + (void) argv; __asm__ volatile("mov %%cs, %0" : "=r"(code_selector)); if ((code_selector & 3u) != 3u) { - typephp_write("sh: Ring-3 transition failed\n"); - typephp_exit(1); + (void) write(STDERR_FILENO, "sh: Ring-3 transition failed\n", + sizeof("sh: Ring-3 transition failed\n") - 1); + return 1; } - typephp_write("TypePHP-OS user shell\n"); - typephp_write("Ring 3 confirmed\n"); - typephp_write("Commands: ls, cd , pwd, date, fault\n"); + (void) write(STDOUT_FILENO, "TypePHP-OS user shell\n", + sizeof("TypePHP-OS user shell\n") - 1); + (void) write(STDOUT_FILENO, "Ring 3 confirmed\n", + sizeof("Ring 3 confirmed\n") - 1); + (void) write(STDOUT_FILENO, + "Commands: ls, cd, pwd, date, cat, echo, write, touch, mkdir, rm, rmdir, mv, memtest, fault, vmfault, wrfault\n", + sizeof("Commands: ls, cd, pwd, date, cat, echo, write, touch, mkdir, rm, rmdir, mv, memtest, fault, vmfault, wrfault\n") - 1); for (;;) { - char *argument; + char *arguments[9]; + int argument_count = 0; + char *cursor; show_prompt(); if (read_line(line, sizeof(line)) == 0) { continue; } - argument = line; - while (*argument != '\0' && *argument != ' ') { - ++argument; - } - if (*argument != '\0') { - *argument++ = '\0'; - while (*argument == ' ') { - ++argument; + cursor = line; + while (*cursor != '\0' && argument_count < 8) { + while (*cursor == ' ') { + ++cursor; } - } - if (string_equal(line, "ls") || string_equal(line, "cd") - || string_equal(line, "pwd") || string_equal(line, "date") - || string_equal(line, "fault")) { - if (typephp_syscall(TYPEPHP_SYS_EXEC, - (long) line, (long) argument, 0) < 0) { - typephp_write("sh: unable to execute command\n"); + if (*cursor == '\0') { + break; } - } else { - typephp_write("sh: command not found: "); - typephp_write(line); - typephp_write("\n"); + arguments[argument_count++] = cursor; + while (*cursor != '\0' && *cursor != ' ') { + ++cursor; + } + if (*cursor != '\0') { + *cursor++ = '\0'; + } + } + arguments[argument_count] = 0; + if (argument_count == 0) { + continue; + } + if (syscall(SYS_typephp_spawn, arguments) < 0) { + (void) write(STDERR_FILENO, "sh: ", sizeof("sh: ") - 1); + (void) write(STDERR_FILENO, arguments[0], strlen(arguments[0])); + (void) write(STDERR_FILENO, ": ", 2); + (void) write(STDERR_FILENO, strerror(errno), strlen(strerror(errno))); + (void) write(STDERR_FILENO, "\n", 1); } } } diff --git a/examples/typephp-os/user/syscall.S b/examples/typephp-os/user/syscall.S new file mode 100644 index 00000000..d08b8e50 --- /dev/null +++ b/examples/typephp-os/user/syscall.S @@ -0,0 +1,28 @@ +.section .text +.code64 + +.global syscall +.type syscall, @function +syscall: + # Translate the System V function-call ABI used by glibc's syscall(2) + # wrapper into the Linux x86-64 syscall register ABI. The temporary + # kernel entry is int 0x80; replacing it with `syscall` will not change + # the C-visible contract. + mov %rdi, %rax + mov %rsi, %rdi + mov %rdx, %rsi + mov %rcx, %rdx + mov %r8, %r10 + mov %r9, %r8 + mov 8(%rsp), %r9 + int $0x80 + cmp $-4095, %rax + jb 1f + neg %eax + mov %eax, errno(%rip) + mov $-1, %rax +1: + ret +.size syscall, . - syscall + +.section .note.GNU-stack, "", @progbits diff --git a/examples/typephp-os/user/syscall.h b/examples/typephp-os/user/syscall.h deleted file mode 100644 index 66e862e8..00000000 --- a/examples/typephp-os/user/syscall.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef TYPEPHP_OS_USER_SYSCALL_H -#define TYPEPHP_OS_USER_SYSCALL_H - -#include - -typedef unsigned long size_t; - -static inline long typephp_syscall( - long number, long first, long second, long third) -{ - register long rax __asm__("rax") = number; - register long rdi __asm__("rdi") = first; - register long rsi __asm__("rsi") = second; - register long rdx __asm__("rdx") = third; - __asm__ volatile("int $0x80" - : "+a"(rax) - : "D"(rdi), "S"(rsi), "d"(rdx) - : "rcx", "r11", "memory"); - return rax; -} - -static inline size_t typephp_strlen(const char *value) -{ - size_t length = 0; - while (value[length] != '\0') { - ++length; - } - return length; -} - -static inline void typephp_write_bytes(const char *value, size_t length) -{ - (void) typephp_syscall(TYPEPHP_SYS_WRITE, 1, (long) value, (long) length); -} - -static inline void typephp_write(const char *value) -{ - typephp_write_bytes(value, typephp_strlen(value)); -} - -static inline void typephp_exit(long status) -{ - (void) typephp_syscall(TYPEPHP_SYS_EXIT, status, 0, 0); - for (;;) { - __asm__ volatile("pause"); - } -} - -#endif diff --git a/examples/typephp-os/user/touch.c b/examples/typephp-os/user/touch.c new file mode 100644 index 00000000..5107f9da --- /dev/null +++ b/examples/typephp-os/user/touch.c @@ -0,0 +1,19 @@ +#include +#include +#include + +int main(int argc, char **argv) +{ + int fd; + if (argc != 2) { + (void) write(STDERR_FILENO, "usage: touch FILE\n", + sizeof("usage: touch FILE\n") - 1); + return 1; + } + fd = open(argv[1], O_WRONLY | O_CREAT, 0666); + if (fd < 0) { + perror("touch"); + return 1; + } + return close(fd) == 0 ? 0 : 1; +} diff --git a/examples/typephp-os/user/vmfault.c b/examples/typephp-os/user/vmfault.c new file mode 100644 index 00000000..0abbd5fd --- /dev/null +++ b/examples/typephp-os/user/vmfault.c @@ -0,0 +1,9 @@ +#include + +int main(void) +{ + /* The resident shell is linked at 32 MiB. A command has a distinct CR3, + * so this address must be unmapped even though the shell remains alive. */ + volatile const uint8_t *shell_memory = (const uint8_t *) UINT64_C(0x2000000); + return *shell_memory; +} diff --git a/examples/typephp-os/user/wrfault.c b/examples/typephp-os/user/wrfault.c new file mode 100644 index 00000000..c0514780 --- /dev/null +++ b/examples/typephp-os/user/wrfault.c @@ -0,0 +1,16 @@ +#include +#include + +int main(void) +{ + volatile uint8_t *memory = mmap(NULL, 4096, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (memory == MAP_FAILED) { + return 1; + } + + /* CR0.WP and the final user PTE permissions must turn this into a + * protection fault rather than silently modifying a read-only page. */ + memory[0] = 0x5a; + return 1; +} diff --git a/examples/typephp-os/user/write.c b/examples/typephp-os/user/write.c new file mode 100644 index 00000000..d26cf16a --- /dev/null +++ b/examples/typephp-os/user/write.c @@ -0,0 +1,33 @@ +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + int fd; + if (argc < 3) { + (void) write(STDERR_FILENO, "usage: write FILE TEXT...\n", + sizeof("usage: write FILE TEXT...\n") - 1); + return 1; + } + fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) { + perror("write"); + return 1; + } + for (int index = 2; index < argc; ++index) { + if (index != 2 && write(fd, " ", 1) != 1) { + (void) close(fd); + return 1; + } + if (write(fd, argv[index], strlen(argv[index])) < 0) { + (void) close(fd); + return 1; + } + } + if (write(fd, "\n", 1) != 1 || close(fd) != 0) { + return 1; + } + return 0; +}