Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ include mk/config.mk
# Source files.
SRCS := \
main.c \
dynamic-array.c \
string-builder.c \
core/guest.c \
core/elf.c \
core/stack.c \
Expand Down Expand Up @@ -192,6 +194,23 @@ $(BUILD_DIR)/test-teardown-live-vcpu-host: \
@echo " LD $@"
$(Q)$(CC) $(CFLAGS) -o $@ $^ $(HVF_LDFLAGS)

## Build the string builder host unit test (native macOS binary)
# This is a pure-C unit test; link the string builder and generic container
# implementations directly and skip the Hypervisor framework and codesign.
$(BUILD_DIR)/test-string-builder-host: \
$(BUILD_DIR)/test-string-builder-host.o \
$(BUILD_DIR)/string-builder.o \
$(BUILD_DIR)/dynamic-array.o | $(BUILD_DIR)
@echo " LD $@"
$(Q)$(CC) $(CFLAGS) -o $@ $^

## Build the generic dynamic-array host unit test (native macOS binary)
$(BUILD_DIR)/test-dynamic-array-host: \
$(BUILD_DIR)/test-dynamic-array-host.o \
$(BUILD_DIR)/dynamic-array.o | $(BUILD_DIR)
@echo " LD $@"
$(Q)$(CC) $(CFLAGS) -o $@ $^


# Guest test binaries (cross-compiled, aarch64-linux)
# Only used when GUEST_TEST_BINARIES is not set.
Expand Down Expand Up @@ -356,6 +375,11 @@ endif

endif

## Build the libc-based file-backed mremap EMFILE regression probe
$(BUILD_DIR)/test-mremap-tail-emfile: tests/test-mremap-tail-emfile.c | $(BUILD_DIR)
@echo " CROSS $<"
$(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $<

include mk/tests.mk
include mk/analysis.mk
include mk/help.mk
73 changes: 73 additions & 0 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,32 @@ Key files:
| `src/runtime/proctitle.c` | argv / comm rewriting for `prctl PR_SET_NAME` |
| `src/debug/gdbstub.c`, `gdbstub-rsp.c`, `gdbstub-reg.c` | GDB RSP stub |

## Generic Dynamic Containers

`src/dynamic-array.h` and `src/dynamic-array.c` provide the raw
`dynamic_array_t` used by the procfs VMA snapshot and the string builder. Capacity
is measured in element slots, while `count` is the number of logical elements.
The allocation is one contiguous block of `capacity * element_size` bytes;
both the count addition and the multiplication are checked before a growth.
Arithmetic overflow reports `EOVERFLOW`, invalid arguments report `EINVAL`,
and an allocation failure reports `ENOMEM`. Growth is transactional: on any
failure the old pointer, count, and capacity remain valid.

The generated typed facades own only their array storage. Elements are copied
as trivially-copyable bytes, so the container does not call destructors and
does not manage pointers or other resources held by an element. `destroy`
frees the contiguous block and restores the zero state. A facade can therefore
be declared as `{0}` and initialized lazily on its first operation.

`string_builder_t` is a thin facade over a generated `char` container. The
container count is the C-string length and excludes the terminator; its
capacity accessor reports bytes including the trailing NUL slot. Reserve and
append account for that slot, and every successful mutation restores
`data[count] == '\0'`. `string_builder_append` accepts a C string and uses its
first NUL as the end of the input. Formatted appends retain the existing
two-pass `vsnprintf` behavior and commit only the prefix through the first NUL,
matching standard C string semantics.

## Hypervisor.framework Constraints

Apple HVF imposes a handful of constraints that shape the rest of the design:
Expand Down Expand Up @@ -768,6 +794,53 @@ under `/proc`, `/dev`, and a few Linux-expected compatibility files:
- Guest cwd handling preserves a virtual `/proc` working directory even
though the host operates on synthetic backing directories.

`/proc/self/smaps` and `/proc/<pid>/smaps` are generated from the same tracked
VMA list as `/proc/self/maps`. The complete field set currently emitted for
each VMA is the maps header followed by these 25 fields, in this order:

```text
Size, KernelPageSize, MMUPageSize, Rss, Pss, Pss_Dirty,
Shared_Clean, Shared_Dirty, Private_Clean, Private_Dirty, Referenced,
Anonymous, KSM, LazyFree, AnonHugePages, ShmemPmdMapped, FilePmdMapped,
Shared_Hugetlb, Private_Hugetlb, Swap, SwapPss, Locked, THPeligible,
ProtectionKey, VmFlags
```

`Size` is the VMA length in KiB. `KernelPageSize` and `MMUPageSize` are
reported as 4 KiB. `Shared_Dirty` is the one page-accounting field with a
non-zero value: in a fork child, writable private anonymous VMAs that existed
in the parent's CoW snapshot report their full VMA size because they are
logically shared with that snapshot. Every other numeric counter, including
`THPeligible` and `ProtectionKey`, is emitted as zero. `VmFlags` is
evidence-based and contains only the permission/sharing flags represented by
the tracked VMA (`rd`, `wr`,
`ex`, `sh`, and `nr` when applicable); no untracked kernel flags are invented.
elfuse always emits `THPeligible` and `ProtectionKey`; consumers comparing
against a real Linux kernel should tolerate either conditional field being
absent.
These are coarse VMA-level values, not host page-residency, dirty-bit, or
proportional-sharing accounting, so they are suitable for fork-safety checks
but not precise memory profiling. The fork-child marker is tracked per VMA, so
writable private anonymous mappings created after fork are excluded from the
compatibility signal. `smaps_rollup` is not implemented.

Synthetic proc directories have explicit snapshot boundaries. The backing
trees reached by opening `/proc` or `/proc/self` are materialized once, on the
first access, and their initial `stat`, `status`, `cmdline`, `maps`, and `smaps`
files remain fixed for the process lifetime. An absolute open of one of those
proc paths is intercepted directly and generates fresh content; opening the
same name through an already-open synthetic directory reads that directory's
snapshot. `/proc/self/fd` and `/proc/self/fdinfo` are rebuilt into an
independent scratch directory on every open, so each directory fd sees the
guest-fd table as it existed at that open and concurrent enumerations cannot
mutate one another. `/proc/self/task` is repopulated from the current thread
set whenever the directory is opened. These boundaries are intentional: a
directory stream is stable while it is being read, while dynamic task and fd
listings refresh only when a new directory is opened. The synthetic
`/sys/devices/system/cpu` tree follows the one-shot rule: its CPU count,
cpumask files, and `cpuN` directories are captured on first access and then
remain fixed.

Related implementation: `src/runtime/procemu.c`, `src/syscall/path.c`,
`src/syscall/fs.c`, `src/syscall/proc-state.c`.

Expand Down
8 changes: 5 additions & 3 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ What they do:
- the unit suite from `tests/manifest.txt` -- deliberately narrow: only
tests that assert elfuse-internal implementation details with no real
Linux counterpart (the EL1 shim fast-path suite, `test-mremap-infra`,
`test-oom-proc`), plus whatever `mk/tests.mk`'s `SANITIZER_SECTIONS`
`test-mremap-fork-tracking`, `test-oom-proc`), plus whatever
`mk/tests.mk`'s `SANITIZER_SECTIONS`
needs for the `check-{asan,ubsan,tsan}` lanes. Everything that is
meaningful to cross-check against a real Linux kernel lives exclusively
in `tests/test-matrix.sh`'s `run_unit_tests` instead (see Test Matrix
Expand Down Expand Up @@ -168,8 +169,9 @@ surface -- every binary that is meaningful to run against a real kernel, which
is almost everything. It deliberately excludes only the handful of tests that
assert elfuse-internal implementation details with no meaningful counterpart
on a real kernel (the EL1 shim fast-path suite, `test-mremap-infra`,
`test-oom-proc` -- these live solely in `tests/manifest.txt` / `make check`,
see that file's header for the full split rationale). There is no separate
`test-mremap-fork-tracking`, `test-oom-proc` -- these live solely in
`tests/manifest.txt` / `make check`, see that file's header for the full split
rationale). There is no separate
"core" vs "extended" test set inside the matrix; a test that has a real,
understood divergence from the qemu reference kernel is listed in
`QEMU_SKIP` with a comment explaining why instead -- see that variable in
Expand Down
4 changes: 3 additions & 1 deletion mk/config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ endif
# Exclude native macOS test files from cross-compilation
NATIVE_TESTS := tests/test-multi-vcpu.c tests/test-rwx.c \
tests/test-tlbi-encoder-host.c \
tests/test-fork-ipc-protocol-host.c
tests/test-fork-ipc-protocol-host.c \
tests/test-dynamic-array-host.c \
tests/test-string-builder-host.c
SPECIAL_TEST_SRCS := tests/test-lowbase-mem.c
SPECIAL_TEST_BINS := $(BUILD_DIR)/test-lowbase-mem-200000 $(BUILD_DIR)/test-lowbase-mem-300000

Expand Down
34 changes: 32 additions & 2 deletions mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
test-sysroot-host-fallback test-sysroot-case-exact \
test-sysroot-create-paths test-fork-ipc-protocol-host \
test-vcpu-run-hooks-host test-identity-override-host \
test-dynamic-array-host \
test-string-builder-host \
test-mremap-tail-emfile \
test-proctitle-host test-proctitle-low-stack \
test-sysroot-procfs-exec test-timeout-disable test-fuse-alpine \
test-sysroot-nofollow test-sysroot-chdir test-sysroot-symlink-escape \
Expand All @@ -24,6 +27,11 @@ test-hello: $(ELFUSE_BIN) $(TEST_HELLO_DEP)
@printf "$(BLUE)▸ Running$(RESET) test-hello\n"
$(ELFUSE_BIN) $(TEST_DIR)/test-hello

## Run the libc-based file-backed region removal EMFILE regression probe
test-mremap-tail-emfile: $(ELFUSE_BIN) $(BUILD_DIR)/test-mremap-tail-emfile
@printf "$(BLUE)▸ Running$(RESET) test-mremap-tail-emfile\n"
$(ELFUSE_BIN) $(BUILD_DIR)/test-mremap-tail-emfile

## Verify dispatch.tbl coverage of the kernel-supported syscall set
check-syscall-coverage:
@python3 scripts/check-syscall-coverage.py
Expand Down Expand Up @@ -80,7 +88,9 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) \
$(BUILD_DIR)/test-fork-ipc-protocol-host \
$(BUILD_DIR)/test-vcpu-run-hooks-host \
$(BUILD_DIR)/test-identity-override-host \
$(BUILD_DIR)/test-teardown-live-vcpu-host
$(BUILD_DIR)/test-teardown-live-vcpu-host \
$(BUILD_DIR)/test-dynamic-array-host \
$(BUILD_DIR)/test-string-builder-host
@bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v -s '$(SANITIZER_SECTIONS)'
@printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-tlbi-encoder-host
Expand All @@ -92,14 +102,20 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) \
@$(BUILD_DIR)/test-identity-override-host
@printf "\n$(BLUE)━━━ teardown live-worker accounting unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-teardown-live-vcpu-host
@printf "\n$(BLUE)━━━ dynamic array unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-dynamic-array-host
@printf "\n$(BLUE)━━━ string builder unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-string-builder-host

## Run the unit test suite plus busybox applet validation
check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage \
$(BUILD_DIR)/test-tlbi-encoder-host \
$(BUILD_DIR)/test-fork-ipc-protocol-host \
$(BUILD_DIR)/test-vcpu-run-hooks-host \
$(BUILD_DIR)/test-identity-override-host \
$(BUILD_DIR)/test-teardown-live-vcpu-host
$(BUILD_DIR)/test-teardown-live-vcpu-host \
$(BUILD_DIR)/test-dynamic-array-host \
$(BUILD_DIR)/test-string-builder-host
@bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v
@printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-tlbi-encoder-host
Expand All @@ -111,6 +127,10 @@ check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage \
@$(BUILD_DIR)/test-identity-override-host
@printf "\n$(BLUE)━━━ teardown live-worker accounting unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-teardown-live-vcpu-host
@printf "\n$(BLUE)━━━ dynamic array unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-dynamic-array-host
@printf "\n$(BLUE)━━━ string builder unit test ━━━$(RESET)\n"
@$(BUILD_DIR)/test-string-builder-host
@printf "\n$(BLUE)━━━ shebang parser unit test ━━━$(RESET)\n"
@$(MAKE) --no-print-directory test-shebang-host
@printf "\n$(BLUE)━━━ proctitle argv-tail regression ━━━$(RESET)\n"
Expand Down Expand Up @@ -770,6 +790,16 @@ test-fork-ipc-protocol-host: $(BUILD_DIR)/test-fork-ipc-protocol-host
test-vcpu-run-hooks-host: $(BUILD_DIR)/test-vcpu-run-hooks-host
$(BUILD_DIR)/test-vcpu-run-hooks-host

# String builder unit test
## Run the growable string builder host unit test
test-string-builder-host: $(BUILD_DIR)/test-string-builder-host
$(BUILD_DIR)/test-string-builder-host

# Generic dynamic array unit test
## Run the raw/typed dynamic array host unit test
test-dynamic-array-host: $(BUILD_DIR)/test-dynamic-array-host
$(BUILD_DIR)/test-dynamic-array-host

# Proctitle argv-tail regression
## Run the deterministic argv-tail overshoot guard test
test-proctitle-host: $(BUILD_DIR)/test-proctitle-host
Expand Down
Loading
Loading