diff --git a/Kconfig b/Kconfig index 84b897e..8ca866f 100644 --- a/Kconfig +++ b/Kconfig @@ -9,3 +9,4 @@ source "boot/Kconfig" source "mem/Kconfig" source "sched/Kconfig" source "dev/Kconfig" +source "tests/Kconfig" diff --git a/meson.build b/meson.build index 11f3207..d180462 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,7 @@ add_languages('c', native: false, required: true) # Check if we're using a sane compiler. c_compiler = meson.get_compiler('c') +python = import('python').find_installation('python3') # We require a compiler that supports the GNU C dialect. assert(c_compiler.has_define('__GNUC__')) @@ -176,6 +177,10 @@ kernel = executable( subdir('boot') +if kconfig.get('CONFIG_KTEST', 'n') == 'y' + subdir('tests') +endif + foreach name, v : modular_modules shared_library( name, diff --git a/meson.options b/meson.options index 389704e..2a89a7d 100644 --- a/meson.options +++ b/meson.options @@ -18,3 +18,10 @@ option( value: '.config', description: 'Relative path to the generated Kconfig file', ) + +option( + 'ovmf', + type: 'string', + value: '', + description: 'OVMF firmware image used to run the QEMU tests', +) diff --git a/tests/Kconfig b/tests/Kconfig new file mode 100644 index 0000000..a452c45 --- /dev/null +++ b/tests/Kconfig @@ -0,0 +1,10 @@ +menu "Testing" + +config KTEST + bool "Kernel test suite" + depends on BOOT_EFISTUB + default n + help + Builds the kernel unit tests. To run them, add "ktest=1" to the cmdline. + +endmenu diff --git a/tests/cmdline.in b/tests/cmdline.in new file mode 100644 index 0000000..1c0ac19 --- /dev/null +++ b/tests/cmdline.in @@ -0,0 +1 @@ +@CMDLINE@ diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..477e1af --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,121 @@ +fs = import('fs') + +assert( + kconfig.get('CONFIG_BOOT_EFISTUB', 'n') == 'y', + 'CONFIG_KTEST depends on CONFIG_BOOT_EFISTUB', +) + +assert( + target_machine.cpu_family() == 'x86_64', + 'CONFIG_KTEST is only supported on x86_64', +) + +# All userspace tests are a single executable, so they have to be idempotent. +utest_init = executable( + 'init', + sources: files( + 'user/lib/crt0.S', + 'user/lib/utest.c', + 'user/memory.c', + 'user/system.c', + ), + include_directories: include_directories('../include'), + c_args: [ + '-ffreestanding', + '-nostdinc', + '-fno-pic', + '-fno-stack-protector', + '-masm=intel', + ], + link_args: [ + '-nostdlib', + '-static', + '-Wl,--no-dynamic-linker', + '-Wl,-z,max-page-size=0x1000', + '-Wl,-z,noexecstack', + '-Wl,--build-id=none', + ], + pie: false, +) + +test_initramfs = custom_target( + 'initramfs.tar', + input: utest_init, + output: 'initramfs.tar', + command: [ + find_program('tar'), + '--format=ustar', + '-cf', '@OUTPUT@', + '-C', '@OUTDIR@', + 'init', + ], +) + +# Try to find an OVMF. +ovmf = get_option('ovmf') +if ovmf == '' + foreach path : [ + '/usr/share/qemu/ovmf-x86_64-4m.bin', + '/usr/share/qemu/ovmf-x86_64.bin', + '/usr/share/ovmf/OVMF.fd', + '/usr/share/edk2/x64/OVMF.4m.fd', + '/usr/share/edk2/x64/OVMF.fd', + '/usr/share/edk2/ovmf/OVMF.fd', + ] + if ovmf == '' and fs.is_file(path) + ovmf = path + endif + endforeach +endif +assert(ovmf != '', 'CONFIG_KTEST needs an OVMF image, set it with -Dovmf=') + +qemu = [ + find_program('qemu-system-x86_64'), + '-M', 'q35', + '-m', '512M', + '-smp', '4', + '-accel', 'kvm', + '-accel', 'tcg', + '-cpu', 'max', + '-nic', 'none', + '-display', 'none', + '-monitor', 'none', + '-serial', 'stdio', + '-no-reboot', + '-bios', ovmf, +] + +test_images = { + 'user': { + 'cmdline': 'init=/init initrd=/initramfs.tar', + 'files': [test_initramfs], + }, +} + +# Build an ESP with the cmdline file and an initramfs. +foreach name, image : test_images + conf = configure_file( + input: 'cmdline.in', + output: name + '.cmdline', + configuration: {'CMDLINE': image['cmdline']}, + ) + + esp = [ + zinnia_efi.full_path() + '=EFI/BOOT/BOOTX64.EFI', + conf.full_path() + '=EFI/zinnia/cmdline', + ] + + foreach file : image['files'] + esp += file.full_path() + '=' + fs.name(file.full_path()) + endforeach + + test( + name, + python, + args: [files('run.py'), esp, '--', qemu], + depends: [zinnia_efi, image['files']], + protocol: 'tap', + suite: 'qemu', + timeout: 60, + ) +endforeach diff --git a/tests/run.py b/tests/run.py new file mode 100644 index 0000000..0044c4b --- /dev/null +++ b/tests/run.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: ISC + +import os +import re +import shutil +import subprocess +import sys +import tempfile + +NOISE_RE = re.compile(r"\x1b\[[0-9;]*m|\r|^\[[ \d.]+\] (?:syslog\(\w+\): )?") +PLAN_RE = re.compile(r"^1\.\.\d+") +TAP_RE = re.compile(r"^(?:ok|not ok|#|1\.\.\d+|Bail out!)") + +sep = sys.argv.index("--") +with tempfile.TemporaryDirectory(prefix="zinnia-esp-") as esp: + for spec in sys.argv[1:sep]: + src, dest = spec.split("=", 1) + path = os.path.join(esp, dest) + os.makedirs(os.path.dirname(path), exist_ok=True) + shutil.copyfile(src, path) + + proc = subprocess.Popen( + sys.argv[sep + 1 :] + ["-drive", f"format=raw,file=fat:{esp},if=virtio,readonly=on"], + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + text=True, + errors="replace", + ) + for line in proc.stdout: + line = NOISE_RE.sub("", line).rstrip() + print(line if TAP_RE.match(line) else f"# {line}", flush=True) + if PLAN_RE.match(line): + proc.kill() + proc.wait() + sys.exit(0) + proc.wait() +sys.exit(1) diff --git a/tests/user/lib/crt0.S b/tests/user/lib/crt0.S new file mode 100644 index 0000000..667f5b6 --- /dev/null +++ b/tests/user/lib/crt0.S @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: ISC + +.intel_syntax noprefix +.text + +.globl _start +.type _start, @function +_start: + xor ebp, ebp + and rsp, -16 + call main + mov edi, eax + xor eax, eax + syscall + ud2 +.size _start, . - _start + +.section .note.GNU-stack, "", @progbits diff --git a/tests/user/lib/syscall.h b/tests/user/lib/syscall.h new file mode 100644 index 0000000..031d431 --- /dev/null +++ b/tests/user/lib/syscall.h @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: ISC + +#pragma once + +#include +#include + +enum syscall_number : uintptr_t { +#define SYSCALL_DECLARE(number, name, num_args) SYS_##name = number, +#include +#undef SYSCALL_DECLARE +}; + +enum syslog_kind : uintptr_t { + SYSLOG_ERR = 3, + SYSLOG_INFO = 6, +}; + +struct sys_ret { + uintptr_t value; + uintptr_t error; +}; + +static inline struct sys_ret syscall6( + enum syscall_number num, + uintptr_t a0, + uintptr_t a1, + uintptr_t a2, + uintptr_t a3, + uintptr_t a4, + uintptr_t a5 +) { + register uintptr_t r10 asm("r10") = a3; + register uintptr_t r8 asm("r8") = a4; + register uintptr_t r9 asm("r9") = a5; + uintptr_t value; + uintptr_t error; + asm volatile("syscall" + : "=a"(value), "=d"(error) + : "a"(num), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8), "r"(r9) + : "rcx", "r11", "memory"); + return (struct sys_ret){value, error}; +} + +static inline struct sys_ret syscall3(enum syscall_number num, uintptr_t a0, uintptr_t a1, uintptr_t a2) { + return syscall6(num, a0, a1, a2, 0, 0, 0); +} + +static inline struct sys_ret syscall4(enum syscall_number num, uintptr_t a0, uintptr_t a1, uintptr_t a2, uintptr_t a3) { + return syscall6(num, a0, a1, a2, a3, 0, 0); +} + +static inline struct sys_ret syscall5( + enum syscall_number num, + uintptr_t a0, + uintptr_t a1, + uintptr_t a2, + uintptr_t a3, + uintptr_t a4 +) { + return syscall6(num, a0, a1, a2, a3, a4, 0); +} + +[[noreturn]] +static inline void sys_exit(int status) { + syscall3(SYS_exit, (uintptr_t)status, 0, 0); + __builtin_unreachable(); +} + +static inline struct sys_ret sys_syslog(enum syslog_kind kind, const char* msg, size_t len) { + return syscall3(SYS_syslog, kind, (uintptr_t)msg, len); +} + +static inline struct sys_ret sys_mmap(void* addr, size_t len, uint32_t prot, uint32_t flags, int fd, int64_t off) { + return syscall6(SYS_mmap, (uintptr_t)addr, len, prot, flags, (uintptr_t)fd, (uintptr_t)off); +} + +static inline struct sys_ret sys_munmap(void* addr, size_t len) { + return syscall3(SYS_munmap, (uintptr_t)addr, len, 0); +} + +static inline struct sys_ret sys_openat(int dirfd, const char* path, uint32_t flags, uint32_t mode) { + return syscall4(SYS_openat, (uintptr_t)dirfd, (uintptr_t)path, flags, mode); +} + +static inline struct sys_ret sys_close(int fd) { + return syscall3(SYS_close, (uintptr_t)fd, 0, 0); +} + +static inline struct sys_ret sys_pread(int fd, void* buf, size_t len, uint64_t off) { + return syscall4(SYS_pread, (uintptr_t)fd, (uintptr_t)buf, len, off); +} + +static inline struct sys_ret sys_fstat(int fd, void* buf) { + return syscall3(SYS_fstat, (uintptr_t)fd, (uintptr_t)buf, 0); +} + +static inline struct sys_ret sys_getuname(void* buf) { + return syscall3(SYS_getuname, (uintptr_t)buf, 0, 0); +} + +static inline struct sys_ret sys_setuname(const void* buf) { + return syscall3(SYS_setuname, (uintptr_t)buf, 0, 0); +} + +static inline struct sys_ret sys_mprotect(void* addr, size_t len, uint32_t prot) { + return syscall3(SYS_mprotect, (uintptr_t)addr, len, prot); +} + +static inline struct sys_ret sys_madvise(void* addr, size_t len, int advice) { + return syscall3(SYS_madvise, (uintptr_t)addr, len, (uintptr_t)advice); +} + +static inline struct sys_ret sys_msync(void* addr, size_t len, int flags) { + return syscall3(SYS_msync, (uintptr_t)addr, len, (uintptr_t)flags); +} + +static inline struct sys_ret sys_pwrite(int fd, const void* buf, size_t len, uint64_t off) { + return syscall4(SYS_pwrite, (uintptr_t)fd, (uintptr_t)buf, len, off); +} + +static inline struct sys_ret sys_readv(int fd, const void* iov, size_t count) { + return syscall3(SYS_readv, (uintptr_t)fd, (uintptr_t)iov, count); +} + +static inline struct sys_ret sys_writev(int fd, const void* iov, size_t count) { + return syscall3(SYS_writev, (uintptr_t)fd, (uintptr_t)iov, count); +} + +static inline struct sys_ret sys_seek(int fd, int64_t off, int whence) { + return syscall3(SYS_seek, (uintptr_t)fd, (uintptr_t)off, (uintptr_t)whence); +} + +static inline struct sys_ret sys_fstatat(int dirfd, const char* path, void* buf, uint32_t flags) { + return syscall4(SYS_fstatat, (uintptr_t)dirfd, (uintptr_t)path, (uintptr_t)buf, flags); +} + +static inline struct sys_ret sys_faccessat(int dirfd, const char* path, uint32_t mode, uint32_t flags) { + return syscall4(SYS_faccessat, (uintptr_t)dirfd, (uintptr_t)path, mode, flags); +} + +static inline struct sys_ret sys_fcntl(int fd, int cmd, uintptr_t arg) { + return syscall3(SYS_fcntl, (uintptr_t)fd, (uintptr_t)cmd, arg); +} + +static inline struct sys_ret sys_ftruncate(int fd, int64_t len) { + return syscall3(SYS_ftruncate, (uintptr_t)fd, (uintptr_t)len, 0); +} + +static inline struct sys_ret sys_truncate(const char* path, int64_t len) { + return syscall3(SYS_truncate, (uintptr_t)path, (uintptr_t)len, 0); +} + +static inline struct sys_ret sys_getcwd(char* buf, size_t len) { + return syscall3(SYS_getcwd, (uintptr_t)buf, len, 0); +} + +static inline struct sys_ret sys_chdir(const char* path) { + return syscall3(SYS_chdir, (uintptr_t)path, 0, 0); +} + +static inline struct sys_ret sys_fchdir(int fd) { + return syscall3(SYS_fchdir, (uintptr_t)fd, 0, 0); +} + +static inline struct sys_ret sys_mkdirat(int dirfd, const char* path, uint32_t mode) { + return syscall3(SYS_mkdirat, (uintptr_t)dirfd, (uintptr_t)path, mode); +} + +static inline struct sys_ret sys_rmdirat(int dirfd, const char* path) { + return syscall3(SYS_rmdirat, (uintptr_t)dirfd, (uintptr_t)path, 0); +} + +static inline struct sys_ret sys_unlinkat(int dirfd, const char* path, uint32_t flags) { + return syscall3(SYS_unlinkat, (uintptr_t)dirfd, (uintptr_t)path, flags); +} + +static inline struct sys_ret sys_getdents(int fd, void* buf, size_t len) { + return syscall3(SYS_getdents, (uintptr_t)fd, (uintptr_t)buf, len); +} + +static inline struct sys_ret sys_renameat(int old_dirfd, const char* old_path, int new_dirfd, const char* new_path) { + return syscall4(SYS_renameat, (uintptr_t)old_dirfd, (uintptr_t)old_path, (uintptr_t)new_dirfd, (uintptr_t)new_path); +} + +static inline struct sys_ret sys_linkat( + int old_dirfd, + const char* old_path, + int new_dirfd, + const char* new_path, + uint32_t flags +) { + return syscall5( + SYS_linkat, + (uintptr_t)old_dirfd, + (uintptr_t)old_path, + (uintptr_t)new_dirfd, + (uintptr_t)new_path, + flags + ); +} + +static inline struct sys_ret sys_symlinkat(const char* target, int dirfd, const char* path) { + return syscall3(SYS_symlinkat, (uintptr_t)target, (uintptr_t)dirfd, (uintptr_t)path); +} + +static inline struct sys_ret sys_readlinkat(int dirfd, const char* path, char* buf, size_t len) { + return syscall4(SYS_readlinkat, (uintptr_t)dirfd, (uintptr_t)path, (uintptr_t)buf, len); +} + +static inline struct sys_ret sys_dup(int fd) { + return syscall3(SYS_dup, (uintptr_t)fd, 0, 0); +} + +static inline struct sys_ret sys_dup3(int old_fd, int new_fd, uint32_t flags) { + return syscall3(SYS_dup3, (uintptr_t)old_fd, (uintptr_t)new_fd, flags); +} + +static inline struct sys_ret sys_umask(uint32_t mask) { + return syscall3(SYS_umask, mask, 0, 0); +} + +static inline struct sys_ret sys_pipe(int* fds, uint32_t flags) { + return syscall3(SYS_pipe, (uintptr_t)fds, flags, 0); +} + +static inline struct sys_ret sys_ppoll(void* fds, size_t nfds, const void* timeout, const void* sigmask) { + return syscall4(SYS_ppoll, (uintptr_t)fds, nfds, (uintptr_t)timeout, (uintptr_t)sigmask); +} diff --git a/tests/user/lib/utest.c b/tests/user/lib/utest.c new file mode 100644 index 0000000..0234bfe --- /dev/null +++ b/tests/user/lib/utest.c @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: ISC + +#include "utest.h" +#include +#include +#include +#include +#include "syscall.h" + +constexpr size_t LINE_MAX = 240; + +extern const struct utest_case __start_utest_cases[]; +extern const struct utest_case __stop_utest_cases[]; + +size_t strlen(const char* str) { + size_t len = 0; + while (str[len]) + len++; + return len; +} + +int memcmp(const void* s1, const void* s2, size_t size) { + const uint8_t* a = s1; + const uint8_t* b = s2; + for (size_t i = 0; i < size; i++) { + if (a[i] != b[i]) + return a[i] < b[i] ? -1 : 1; + } + return 0; +} + +void* memcpy(void* restrict dest, const void* restrict src, size_t n) { + uint8_t* d = dest; + const uint8_t* s = src; + for (size_t i = 0; i < n; i++) + d[i] = s[i]; + return dest; +} + +void* memset(void* dest, int value, size_t n) { + uint8_t* d = dest; + for (size_t i = 0; i < n; i++) + d[i] = (uint8_t)value; + return dest; +} + +bool utest_streq(const char* a, const char* b) { + if (!a || !b) + return a == b; + while (*a && *a == *b) { + a++; + b++; + } + return *a == *b; +} + +struct line { + char buf[LINE_MAX]; + size_t len; +}; + +static void put(struct line* line, char c) { + if (line->len < sizeof(line->buf) - 1) + line->buf[line->len++] = c; +} + +static void put_str(struct line* line, const char* str) { + for (; *str; str++) + put(line, *str); +} + +static void put_num(struct line* line, unsigned long long value, unsigned base, bool negative, bool prefix) { + char digits[24]; + size_t n = 0; + do { + digits[n++] = "0123456789abcdef"[value % base]; + value /= base; + } while (value); + if (negative) + put(line, '-'); + if (prefix) + put_str(line, "0x"); + while (n) + put(line, digits[--n]); +} + +static void format(struct line* line, const char* fmt, va_list args) { + for (; *fmt; fmt++) { + if (*fmt != '%') { + put(line, *fmt); + continue; + } + fmt++; + bool alt = false; + if (*fmt == '#') { + alt = true; + fmt++; + } + int longs = 0; + bool size = false; + for (;; fmt++) { + if (*fmt == 'l') + longs++; + else if (*fmt == 'z') + size = true; + else + break; + } + + switch (*fmt) { + case 'd': + case 'i': { + long long v = size ? (long long)va_arg(args, ptrdiff_t) + : longs ? va_arg(args, long long) + : va_arg(args, int); + put_num(line, v < 0 ? -(unsigned long long)v : (unsigned long long)v, 10, v < 0, false); + break; + } + case 'u': + case 'x': { + unsigned long long v = size ? va_arg(args, size_t) + : longs ? va_arg(args, unsigned long long) + : va_arg(args, unsigned int); + put_num(line, v, *fmt == 'x' ? 16 : 10, false, alt && *fmt == 'x'); + break; + } + case 'p': + put_num(line, (uintptr_t)va_arg(args, void*), 16, false, true); + break; + case 's': { + const char* s = va_arg(args, const char*); + put_str(line, s ? s : "(null)"); + break; + } + case 'c': + put(line, (char)va_arg(args, int)); + break; + case '%': + put(line, '%'); + break; + default: + put(line, '?'); + break; + } + } +} + +static void emit(struct line* line) { + line->buf[line->len] = 0; + sys_syslog(SYSLOG_INFO, line->buf, line->len); +} + +void utest_log(const char* fmt, ...) { + struct line line = {}; + va_list args; + va_start(args, fmt); + format(&line, fmt, args); + va_end(args); + emit(&line); +} + +void utest_fail(struct utest* test, const char* file, int line_nr, const char* fmt, ...) { + struct line line = {}; + put_str(&line, "# "); + put_str(&line, test->suite); + put(&line, '.'); + put_str(&line, test->name); + put_str(&line, ": "); + put_str(&line, file); + put(&line, ':'); + put_num(&line, (unsigned)line_nr, 10, false, false); + put_str(&line, ": "); + va_list args; + va_start(args, fmt); + format(&line, fmt, args); + va_end(args); + emit(&line); + test->failed = true; +} + +void utest_run_all() { + size_t index = 0; + for (const struct utest_case* c = __start_utest_cases; c < __stop_utest_cases; c++) { + struct utest test = {.suite = c->suite, .name = c->name}; + c->func(&test); + + index++; + if (test.skipped) + utest_log("ok %zu %s.%s # SKIP %s", index, c->suite, c->name, test.skip_reason ? test.skip_reason : ""); + else if (c->xfail_reason) + utest_log( + "%s %zu %s.%s # TODO %s", + test.failed ? "not ok" : "ok", + index, + c->suite, + c->name, + c->xfail_reason + ); + else + utest_log("%s %zu %s.%s", test.failed ? "not ok" : "ok", index, c->suite, c->name); + } + + utest_log("1..%zu", index); +} + +int main() { + utest_run_all(); + for (;;) + ; +} diff --git a/tests/user/lib/utest.h b/tests/user/lib/utest.h new file mode 100644 index 0000000..2ab8993 --- /dev/null +++ b/tests/user/lib/utest.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: ISC + +#pragma once + +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +struct utest { + const char* suite; + const char* name; + bool failed; + bool skipped; + const char* skip_reason; +}; + +struct utest_case { + const char* suite; + const char* name; + const char* xfail_reason; + void (*func)(struct utest* test); +}; + +#define _UTEST(suite_name, case_name, reason) \ + static void suite_name##_##case_name(struct utest* test); \ + [[gnu::used, gnu::retain, gnu::section("utest_cases"), gnu::aligned(8)]] \ + static const struct utest_case suite_name##_##case_name##_case = { \ + .suite = #suite_name, \ + .name = #case_name, \ + .xfail_reason = (reason), \ + .func = suite_name##_##case_name, \ + }; \ + static void suite_name##_##case_name(struct utest* test) + +// Expected success +#define UTEST(suite_name, case_name) _UTEST(suite_name, case_name, nullptr) +// Expected failure +#define UTEST_XFAIL(suite_name, case_name, reason) _UTEST(suite_name, case_name, reason) + +[[gnu::format(printf, 1, 2)]] +void utest_log(const char* fmt, ...); + +[[gnu::format(printf, 4, 5)]] +void utest_fail(struct utest* test, const char* file, int line, const char* fmt, ...); + +void utest_run_all(); + +bool utest_streq(const char* a, const char* b); + +#define _UTEST_CHECK(cond, on_fail, ...) \ + do { \ + if (!(cond)) { \ + utest_fail(test, __FILE__, __LINE__, __VA_ARGS__); \ + on_fail; \ + } \ + } while (0) + +#define _UTEST_CMP(a, op, b, on_fail) \ + do { \ + const typeof(true ? (a) : (b)) _utest_a = (a); \ + const typeof(true ? (a) : (b)) _utest_b = (b); \ + _UTEST_CHECK( \ + _utest_a op _utest_b, \ + on_fail, \ + "expected %s %s %s, got %#llx and %#llx", \ + #a, \ + #op, \ + #b, \ + (unsigned long long)_utest_a, \ + (unsigned long long)_utest_b \ + ); \ + } while (0) + +#define EXPECT_TRUE(cond) _UTEST_CHECK(cond, (void)0, "expected %s", #cond) +#define EXPECT_FALSE(cond) _UTEST_CHECK(!(cond), (void)0, "expected !(%s)", #cond) +#define EXPECT_EQ(a, b) _UTEST_CMP(a, ==, b, (void)0) +#define EXPECT_NE(a, b) _UTEST_CMP(a, !=, b, (void)0) +#define EXPECT_GT(a, b) _UTEST_CMP(a, >, b, (void)0) +#define EXPECT_GE(a, b) _UTEST_CMP(a, >=, b, (void)0) +#define EXPECT_STREQ(a, b) _UTEST_CHECK(utest_streq((a), (b)), (void)0, "expected %s == \"%s\"", #a, (b)) + +#define ASSERT_TRUE(cond) _UTEST_CHECK(cond, return, "expected %s", #cond) +#define ASSERT_EQ(a, b) _UTEST_CMP(a, ==, b, return) +#define ASSERT_NE(a, b) _UTEST_CMP(a, !=, b, return) + +#define SKIP(reason) \ + do { \ + test->skipped = true; \ + test->skip_reason = (reason); \ + return; \ + } while (0) diff --git a/tests/user/memory.c b/tests/user/memory.c new file mode 100644 index 0000000..9f5034a --- /dev/null +++ b/tests/user/memory.c @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: ISC + +#include +#include +#include +#include +#include +#include "lib/syscall.h" +#include "lib/utest.h" + +constexpr size_t PAGE_SIZE = 0x1000; +constexpr uint32_t PROT_RW = __PROT_READ | __PROT_WRITE; +constexpr uint32_t MAP_ANON_PRIVATE = __MAP_PRIVATE | __MAP_ANON; + +static uint8_t* map_anon(size_t len) { + struct sys_ret ret = sys_mmap(nullptr, len, PROT_RW, MAP_ANON_PRIVATE, -1, 0); + return ret.error ? nullptr : (uint8_t*)ret.value; +} + +UTEST(mman, anonymous_mapping) { + const size_t len = 4 * PAGE_SIZE; + struct sys_ret ret = sys_mmap(nullptr, len, __PROT_READ | __PROT_WRITE, __MAP_PRIVATE | __MAP_ANON, -1, 0); + ASSERT_EQ(ret.error, 0); + + uint8_t* mem = (uint8_t*)ret.value; + EXPECT_EQ((uintptr_t)mem % PAGE_SIZE, 0); + for (size_t i = 0; i < len; i += PAGE_SIZE) + EXPECT_EQ(mem[i], 0); + for (size_t i = 0; i < len; i++) + mem[i] = (uint8_t)i; + for (size_t i = 0; i < len; i++) { + if (mem[i] != (uint8_t)i) { + utest_fail(test, __FILE__, __LINE__, "byte %zu reads back as %u", i, mem[i]); + break; + } + } + + EXPECT_EQ(sys_munmap(mem, len).error, 0); +} + +UTEST(mman, rejects_zero_length) { + struct sys_ret ret = sys_mmap(nullptr, 0, __PROT_READ, __MAP_PRIVATE | __MAP_ANON, -1, 0); + EXPECT_EQ(ret.error, __EINVAL); +} + +UTEST(mman, shared_anonymous_mapping) { + struct sys_ret ret = sys_mmap(nullptr, PAGE_SIZE, PROT_RW, __MAP_SHARED | __MAP_ANON, -1, 0); + ASSERT_EQ(ret.error, 0); + uint8_t* mem = (uint8_t*)ret.value; + EXPECT_EQ(mem[0], 0); + mem[0] = 0x5a; + mem[PAGE_SIZE - 1] = 0xa5; + EXPECT_EQ(mem[0], 0x5a); + EXPECT_EQ(mem[PAGE_SIZE - 1], 0xa5); + EXPECT_EQ(sys_munmap(mem, PAGE_SIZE).error, 0); +} + +UTEST(mman, file_mapping) { + struct sys_ret ret = sys_openat(__AT_FDCWD, "/init", __O_RDONLY, 0); + ASSERT_EQ(ret.error, 0); + const int fd = (int)ret.value; + + ret = sys_mmap(nullptr, PAGE_SIZE, __PROT_READ, __MAP_PRIVATE, fd, 0); + EXPECT_EQ(ret.error, 0); + if (!ret.error) { + const uint8_t elf_header[] = {0x7f, 'E', 'L', 'F'}; + EXPECT_EQ(memcmp((const void*)ret.value, elf_header, sizeof(elf_header)), 0); + EXPECT_EQ(sys_munmap((void*)ret.value, PAGE_SIZE).error, 0); + } + EXPECT_EQ(sys_close(fd).error, 0); +} + +UTEST(mman, fixed_mapping_replaces) { + uint8_t* mem = map_anon(3 * PAGE_SIZE); + ASSERT_TRUE(mem != nullptr); + memset(mem, 0xcc, 3 * PAGE_SIZE); + + struct sys_ret ret = sys_mmap(mem + PAGE_SIZE, PAGE_SIZE, PROT_RW, MAP_ANON_PRIVATE | __MAP_FIXED, -1, 0); + EXPECT_EQ(ret.error, 0); + EXPECT_EQ(ret.value, (uintptr_t)(mem + PAGE_SIZE)); + EXPECT_EQ(mem[0], 0xcc); + EXPECT_EQ(mem[PAGE_SIZE], 0); + EXPECT_EQ(mem[2 * PAGE_SIZE], 0xcc); + EXPECT_EQ(sys_munmap(mem, 3 * PAGE_SIZE).error, 0); +} + +UTEST(mman, partial_unmap) { + uint8_t* mem = map_anon(3 * PAGE_SIZE); + ASSERT_TRUE(mem != nullptr); + EXPECT_EQ(sys_munmap(mem + PAGE_SIZE, PAGE_SIZE).error, 0); + mem[0] = 1; + mem[2 * PAGE_SIZE] = 2; + EXPECT_EQ(mem[0], 1); + EXPECT_EQ(mem[2 * PAGE_SIZE], 2); + EXPECT_EQ(sys_munmap(mem, 3 * PAGE_SIZE).error, 0); +} + +UTEST(mman, rejects_invalid_arguments) { + EXPECT_EQ(sys_mmap(nullptr, PAGE_SIZE, 0x80, MAP_ANON_PRIVATE, -1, 0).error, __EINVAL); + EXPECT_EQ(sys_mmap(nullptr, PAGE_SIZE, PROT_RW, __MAP_ANON, -1, 0).error, __EINVAL); + EXPECT_EQ(sys_mmap(nullptr, PAGE_SIZE, PROT_RW, MAP_ANON_PRIVATE, -1, 1).error, __EINVAL); + EXPECT_EQ(sys_mmap((void*)0x1001, PAGE_SIZE, PROT_RW, MAP_ANON_PRIVATE | __MAP_FIXED, -1, 0).error, __EINVAL); + EXPECT_EQ(sys_mmap(nullptr, PAGE_SIZE, __PROT_READ, __MAP_PRIVATE, 1234, 0).error, __EBADF); +} + +UTEST(mman, mprotect) { + uint8_t* mem = map_anon(2 * PAGE_SIZE); + ASSERT_TRUE(mem != nullptr); + mem[0] = 7; + EXPECT_EQ(sys_mprotect(mem, PAGE_SIZE, __PROT_READ).error, 0); + EXPECT_EQ(mem[0], 7); + EXPECT_EQ(sys_mprotect(mem, PAGE_SIZE, PROT_RW).error, 0); + mem[0] = 8; + EXPECT_EQ(mem[0], 8); + EXPECT_EQ(sys_mprotect(mem, PAGE_SIZE, 0x80).error, __EINVAL); + EXPECT_EQ(sys_mprotect(mem, 0, __PROT_READ).error, __EINVAL); + EXPECT_EQ(sys_munmap(mem, 2 * PAGE_SIZE).error, 0); +} + +UTEST(mman, msync) { + uint8_t* mem = map_anon(PAGE_SIZE); + ASSERT_TRUE(mem != nullptr); + EXPECT_EQ(sys_msync(mem, PAGE_SIZE, __MS_SYNC).error, 0); + EXPECT_EQ(sys_msync(mem, PAGE_SIZE, __MS_ASYNC).error, 0); + EXPECT_EQ(sys_msync(mem, PAGE_SIZE, __MS_ASYNC | __MS_SYNC).error, __EINVAL); + EXPECT_EQ(sys_msync(mem + 1, PAGE_SIZE, __MS_SYNC).error, __EINVAL); + EXPECT_EQ(sys_munmap(mem, PAGE_SIZE).error, 0); + EXPECT_EQ(sys_msync(mem, PAGE_SIZE, __MS_ASYNC).error, __ENOMEM); +} + +UTEST(mman, madvise) { + uint8_t* mem = map_anon(PAGE_SIZE); + ASSERT_TRUE(mem != nullptr); + EXPECT_EQ(sys_madvise(mem, PAGE_SIZE, 0).error, 0); + EXPECT_EQ(sys_madvise(mem + 1, PAGE_SIZE, 0).error, __EINVAL); + EXPECT_EQ(sys_munmap(mem, PAGE_SIZE).error, 0); +} diff --git a/tests/user/system.c b/tests/user/system.c new file mode 100644 index 0000000..492b0b1 --- /dev/null +++ b/tests/user/system.c @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: ISC + +#include +#include +#include +#include "lib/syscall.h" +#include "lib/utest.h" + +UTEST(syslog, writes_message) { + const char msg[] = "utest: syslog test"; + struct sys_ret ret = sys_syslog(SYSLOG_INFO, msg, sizeof(msg) - 1); + EXPECT_EQ(ret.error, 0); +} + +UTEST(syslog, rejects_bad_pointer) { + struct sys_ret ret = sys_syslog(SYSLOG_INFO, (const char*)0x10, 8); + EXPECT_EQ(ret.error, __EFAULT); +} + +UTEST(system, uname) { + struct __utsname uts = {}; + struct sys_ret ret = sys_getuname(&uts); + ASSERT_EQ(ret.error, 0); + EXPECT_STREQ(uts.sysname, "Zinnia"); + EXPECT_STREQ(uts.machine, "x86_64"); +} + +UTEST(syslog, long_message) { + char msg[1024]; + memset(msg, 'x', sizeof(msg)); + EXPECT_EQ(sys_syslog(SYSLOG_INFO, msg, sizeof(msg)).error, 0); +} + +UTEST(system, uname_rejects_null) { + EXPECT_EQ(sys_getuname(nullptr).error, __EINVAL); + EXPECT_EQ(sys_setuname(nullptr).error, __EINVAL); +} + +UTEST(system, uname_rejects_bad_pointer) { + EXPECT_EQ(sys_getuname((void*)0x10).error, __EFAULT); + EXPECT_EQ(sys_setuname((const void*)0x10).error, __EFAULT); +} + +UTEST(system, setuname) { + struct __utsname original = {}; + ASSERT_EQ(sys_getuname(&original).error, 0); + + struct __utsname changed = original; + memset(changed.nodename, 0, sizeof(changed.nodename)); + memcpy(changed.nodename, "foobar", sizeof("foobar")); + ASSERT_EQ(sys_setuname(&changed).error, 0); + + struct __utsname now = {}; + EXPECT_EQ(sys_getuname(&now).error, 0); + EXPECT_STREQ(now.nodename, "foobar"); + EXPECT_STREQ(now.sysname, original.sysname); + + EXPECT_EQ(sys_setuname(&original).error, 0); + EXPECT_EQ(sys_getuname(&now).error, 0); + EXPECT_STREQ(now.nodename, original.nodename); +}