From 2873ecdb524ab1737203591bbbe68fbbe3279f39 Mon Sep 17 00:00:00 2001 From: Smart Agents Cluster Date: Thu, 11 Jun 2026 01:46:48 +0800 Subject: [PATCH] feat(starry): expose root block device /dev/vda + strengthen busybox applet tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the busybox unit for the six applets we own (resize · remove-shell · rdev · setlogcons · killall5 · fdflush): one kernel feature (rdev's root block node) plus real-behavior regression tests for all six. ## Kernel: /dev/vda root block device (for busybox rdev) busybox `rdev` (reports the device mounted at `/`) stats `"/"`, takes its `st_dev`, then scans `/dev` for a block node whose `st_rdev` matches and prints ` /`. starry has no real block backend for the root mount, so `/dev` had no such node and `rdev` resolved nothing. Expose a placeholder block node `/dev/vda` whose `rdev` equals the root filesystem's `st_dev` (the root mount is the first mount, device id 1 = `DeviceId::new(0, 1)`), so `rdev` resolves `/dev/vda /`. It is a **resolver placeholder, not a usable disk**: `RootBlk`'s `read_at`/`write_at` return `EIO` rather than silently succeeding, so it never masquerades as a working disk for `dd`/`blkid`/`fsck`. Kernel-side regression (test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda): `stat "/"`; `/dev/vda` exists and is `S_ISBLK`; `/dev/vda` `st_rdev ==` root `st_dev` (busybox rdev's resolution premise); `open` then `read` returns `EIO` (placeholder, no fake disk I/O). x86_64 / aarch64 / riscv64 / loongarch64 each 6/6 PASS. ## App: strengthen the six busybox applet sh tests apps/starry/qemu/busybox/sh/busybox-tests.sh carried weak smoke tests (bare `rc != 124` or `-h` usage) for our applets. Replace them with carpet-coverage real-behavior cases — every invocation form / option / edge per applet — and add the missing `remove-shell`. The app-qemu busybox harness aborts on the first `^FAIL:`, so every case must pass; assertions stay tolerant where StarryOS lacks a kernel feature (forward-compatible) while still rejecting stubs / "applet not found". 15 cases across the six applets: - **rdev**: require exactly one `^/dev/ /$` line (with /dev/vda above, prints `/dev/vda /`) — was `rc != 124`. - **fdflush** (×3): missing device → file/ioctl diagnostic; no-operand → usage error (it requires DEVICE); `/dev/null` → FDFLUSH ioctl error on a non-floppy. All reject "applet not found". - **killall5**: reject "applet not found"; the real SIGSTOP/SIGCONT job control it relies on is covered safely (forked child, no effect on this suite) by the dedicated test-job-control-stop case — running a session-wide SIGSTOP inside this suite would risk stopping the test harness itself. - **setlogcons** (×3): real TIOCLINUX for `setlogcons 0`, no-arg (default 0), and console 1; accept rc=0 or a `/dev/tty0`/console diagnostic; reject "applet not found". - **resize** (×3): the size probe (accept the full `COLUMNS=;LINES=;export` trailer OR a clean non-zero exit — the stdio console does not yet honor the TCSETS raw-mode ioctl, so the cursor probe can't complete; forward-compatible once it does), applet-registered, and redirected-stdin (stays bounded). Reject silent exit-0 stub / "applet not found". - **remove-shell** (×4, new): single remove, multi-remove (survivors kept), remove-absent (no-op, file intact), and no-arg (no-op rc=0 leaving the file intact, or usage error) — real `/etc/shells` edits, not `-h`. Verified: aarch64 / riscv64 / loongarch64 busybox app-qemu each `PASS: 329 FAIL: 0` (SUCCESS PATTERN MATCHED), all applet cases PASS; x86_64 via CI (local x86 app-qemu is blocked by the PVH ELF-note loader). > 把困困投入生产后更名 `智慧集群`(Smart Cluster) Signed-off-by: Smart Agents Cluster Signed-off-by: 林晨 (Leo Cheng) --- apps/starry/qemu/busybox/sh/busybox-tests.sh | 172 ++++++++++++++++-- os/StarryOS/kernel/src/pseudofs/dev/mod.rs | 40 ++++ .../syscall-test-rdev-vda/CMakeLists.txt | 9 + .../system/syscall-test-rdev-vda/src/main.c | 50 +++++ .../src/test_framework.h | 85 +++++++++ 5 files changed, 343 insertions(+), 13 deletions(-) create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c create mode 100644 test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h diff --git a/apps/starry/qemu/busybox/sh/busybox-tests.sh b/apps/starry/qemu/busybox/sh/busybox-tests.sh index 2f92b48961..6283bdcd27 100644 --- a/apps/starry/qemu/busybox/sh/busybox-tests.sh +++ b/apps/starry/qemu/busybox/sh/busybox-tests.sh @@ -1471,16 +1471,43 @@ else echo "FAIL_DETAIL: busybox_insmod (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_fdflush — issues the BLKFLSBUF ioctl. On a missing / non-block target +# it fails with a file/ioctl diagnostic. Require that diagnostic AND reject +# "applet not found" so an unregistered applet (which also exits non-zero with +# "fdflush: applet not found") can't pass on a bare rc!=0. bb_case_start "busybox_fdflush" _t=$({ timeout 10 sh -c 'busybox fdflush /tmp/bb_no_such_device 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] && echo "$_t" | grep -qiE "No such|not found|can't open|cannot open|device|ioctl"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] \ + && echo "$_t" | grep -qiE "No such|can't open|cannot open|not a tty|inappropriate|ioctl" \ + && ! echo "$_t" | grep -qiF "applet not found"; then echo "PASS: busybox_fdflush"; bb_case_pass else echo "FAIL_DETAIL: busybox_fdflush (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# fdflush no operand: usage error (non-zero), not "applet not found". +bb_case_start "busybox_fdflush_noarg" +_t=$({ timeout 10 sh -c 'busybox fdflush 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -ne 0 ] && ! echo "$_t" | grep -qiF "applet not found" && echo "$_t" | grep -qiE "usage|fdflush|argument"; then + echo "PASS: busybox_fdflush_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_fdflush_noarg (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# fdflush on a real (non-floppy) device node /dev/null: opens OK, FDFLUSH ioctl +# yields an error diagnostic (tolerant: rc!=0 with a diagnostic, or rc=0). +bb_case_start "busybox_fdflush_devnull" +_t=$({ timeout 10 sh -c 'busybox fdflush /dev/null 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "ioctl|not a tty|inappropriate|invalid|error|No such"; }; then + echo "PASS: busybox_fdflush_devnull"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_fdflush_devnull (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + bb_case_start "busybox_raidautorun" _t=$({ timeout 10 sh -c 'busybox raidautorun /tmp/bb_no_such_device 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') @@ -1491,46 +1518,165 @@ else echo "FAIL_DETAIL: busybox_raidautorun (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_killall5 — the real SIGSTOP/SIGCONT job-control killall5 relies on is +# verified safely (forked child, no effect on this suite) by the dedicated +# test-job-control-stop case. Here we smoke the applet and reject "applet not +# found" so an unregistered applet can't pass: running the real session-wide +# SIGSTOP inside this suite would risk stopping the test harness itself. bb_case_start "busybox_killall5" _t=$({ timeout 10 sh -c 'busybox killall5 -h 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|killall5"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|killall5" && ! echo "$_t" | grep -qiF "applet not found"; then echo "PASS: busybox_killall5"; bb_case_pass else echo "FAIL_DETAIL: busybox_killall5 (rc=$_rc)"; echo "$_t"; bb_case_fail fi -# busybox_rdev — rdev outputs nothing on StarryOS (no /proc/kcore etc.) -# so we only verify: applet exists, executes without hang, returns non-timeout +# busybox_rdev — reports the device mounted at "/": stats "/", takes st_dev, +# scans /dev for a block node with matching st_rdev. With the /dev/vda root +# block node (its rdev == root st_dev) rdev prints "/dev/vda /". Require exactly +# one "/dev/ /" line so stray output can't slip through. bb_case_start "busybox_rdev" _t=$({ timeout 10 sh -c 'busybox rdev 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ]; then +_t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$(printf '%s\n' "$_t" | grep -cE '^/dev/[^[:space:]]+[[:space:]]+/$')" = 1 ]; then echo "PASS: busybox_rdev"; bb_case_pass else echo "FAIL_DETAIL: busybox_rdev (rc=$_rc)"; echo "$_t"; bb_case_fail fi +# busybox_setlogcons — real TIOCLINUX subcmd 11 ioctl on /dev/tty0 (not just +# -h). Accept rc=0 (kernel handled it) or a "/dev/tty0" open-failure fallback +# (the applet itself works; starry may lack /dev/tty0). Reject "applet not +# found" so an unregistered applet can't pass. bb_case_start "busybox_setlogcons" -_t=$({ timeout 10 sh -c 'busybox setlogcons -h 2>&1'; echo "EXIT:$?"; } 2>&1) +_t=$({ timeout 10 sh -c 'busybox setlogcons 0 2>&1'; echo "EXIT:$?"; } 2>&1) _rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && echo "$_t" | grep -qiE "Usage|setlogcons"; then +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" \ + && { [ "$_rc" = 0 ] || echo "$_t" | grep -qF "/dev/tty0"; }; then echo "PASS: busybox_setlogcons"; bb_case_pass else echo "FAIL_DETAIL: busybox_setlogcons (rc=$_rc)"; echo "$_t"; bb_case_fail fi -# busybox_resize — outputs terminal escape sequences that corrupt EXIT: parsing -# redirect all output to /dev/null, only capture return code +# setlogcons with no positional arg: defaults to console 0 (same TIOCLINUX path). +bb_case_start "busybox_setlogcons_noarg" +_t=$({ timeout 10 sh -c 'busybox setlogcons 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "/dev/tty0|console|ioctl|usage|TIOCLINUX"; }; then + echo "PASS: busybox_setlogcons_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_setlogcons_noarg (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# setlogcons with explicit console 1 exercises the N argument path. +bb_case_start "busybox_setlogcons_n1" +_t=$({ timeout 10 sh -c 'busybox setlogcons 1 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "not found" && { [ "$_rc" = 0 ] || echo "$_t" | grep -qiE "/dev/tty|console|ioctl|TIOCLINUX"; }; then + echo "PASS: busybox_setlogcons_n1"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_setlogcons_n1 (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# busybox_resize — sets the tty to raw mode (TCSETS), writes an ESC[6n cursor +# query, reads the report, then prints a `COLUMNS=N;LINES=N;export COLUMNS LINES;` +# trailer scripts can `eval`. The busybox-shell stdio console on starry does not +# yet honor the TCSETS raw-mode ioctl (it returns ENOTTY — see notes/51), so +# resize cannot complete the probe and exits non-zero before emitting the +# trailer. Accept EITHER outcome: the full trailer (the ideal, once the console +# gains TCSETS) OR a clean non-zero exit that is NOT "applet not found" (the +# current starry reality). A stub that silently exits 0 without a trailer, or a +# dropped/unregistered applet ("applet not found"), still FAILs. bb_case_start "busybox_resize" -_t=$({ timeout 10 sh -c 'busybox resize >/dev/null 2>&1'; echo "EXIT:$?"; } 2>&1) -_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') -if [ -n "$_rc" ] && [ "$_rc" -ne 124 ]; then +timeout 10 sh -c 'busybox resize >/tmp/bb_resize.out 2>/tmp/bb_resize.err'; _rc=$? +_rerr=$(cat /tmp/bb_resize.err 2>/dev/null) +_rtrailer=0 +if grep -qF "COLUMNS=" /tmp/bb_resize.out && grep -qF "LINES=" /tmp/bb_resize.out && grep -qF "export COLUMNS LINES" /tmp/bb_resize.out; then _rtrailer=1; fi +if [ "$_rc" -ne 124 ] && ! echo "$_rerr" | grep -qiF "applet not found" && { [ "$_rtrailer" = 1 ] || [ "$_rc" -ne 0 ]; }; then echo "PASS: busybox_resize"; bb_case_pass else - echo "FAIL_DETAIL: busybox_resize (rc=$_rc)"; echo "$_t"; bb_case_fail + echo "FAIL_DETAIL: busybox_resize (rc=$_rc trailer=$_rtrailer)"; echo "OUT:"; cat /tmp/bb_resize.out; echo "ERR: $_rerr"; bb_case_fail +fi + +# resize must be a registered applet (built in), not "applet not found". +bb_case_start "busybox_resize_registered" +_t=$({ timeout 10 sh -c 'busybox resize --help 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found"; then + echo "PASS: busybox_resize_registered"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_resize_registered (rc=$_rc)"; echo "$_t"; bb_case_fail +fi + +# resize with stdin redirected from a non-tty must stay bounded (no hang) and +# must not be reported as "applet not found". +bb_case_start "busybox_resize_redir_stdin" +timeout 10 sh -c 'busybox resize /tmp/bb_resize2.out 2>&1'; _rc=$? +if [ "$_rc" -ne 124 ] && ! grep -qiF "applet not found" /tmp/bb_resize2.out 2>/dev/null; then + echo "PASS: busybox_resize_redir_stdin"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_resize_redir_stdin (rc=$_rc)"; cat /tmp/bb_resize2.out 2>/dev/null; bb_case_fail +fi + +# busybox_remove_shell — removes the named shell(s) from /etc/shells. Seed +# /etc/shells with two entries, remove one, and require the removed entry is +# gone while the survivor remains — a real edit of the file, not just --help. +bb_case_start "busybox_remove_shell" +printf '/bin/sh\n/bin/bb_dummy_shell\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/bb_dummy_shell 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p') +_t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] \ + && ! echo "$_t" | grep -qiF "applet not found" \ + && ! grep -qF "/bin/bb_dummy_shell" /etc/shells 2>/dev/null \ + && grep -qF "/bin/sh" /etc/shells 2>/dev/null; then + echo "PASS: busybox_remove_shell"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell removes multiple shells in one invocation; survivors retained. +bb_case_start "busybox_remove_shell_multi" +printf '/bin/sh\n/bin/aaa\n/bin/bbb\n/bin/ccc\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/aaa /bin/ccc 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && ! grep -qF "/bin/aaa" /etc/shells && ! grep -qF "/bin/ccc" /etc/shells \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/bbb" /etc/shells; then + echo "PASS: busybox_remove_shell_multi"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_multi (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell of a shell not present is a no-op success; file left intact. +bb_case_start "busybox_remove_shell_absent" +printf '/bin/sh\n/bin/zsh\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell /bin/nonexistent_xyz 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && [ "$_rc" -eq 0 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/zsh" /etc/shells; then + echo "PASS: busybox_remove_shell_absent"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_absent (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail +fi + +# remove-shell with no operand: the per-arg removal loop has nothing to do, so +# BusyBox treats it as a no-op (rc 0) that leaves /etc/shells intact (some +# builds instead print usage with rc!=0). Accept either, but the applet must be +# registered ("applet not found" rejected) and must not corrupt the file. +bb_case_start "busybox_remove_shell_noarg" +printf '/bin/sh\n/bin/ksh\n' > /etc/shells 2>/dev/null +_t=$({ timeout 10 sh -c 'busybox remove-shell 2>&1'; echo "EXIT:$?"; } 2>&1) +_rc=$(printf '%s\n' "$_t" | sed -n 's/^EXIT://p'); _t=$(printf '%s\n' "$_t" | sed '/^EXIT:/d') +if [ -n "$_rc" ] && [ "$_rc" -ne 124 ] && ! echo "$_t" | grep -qiF "applet not found" \ + && grep -qF "/bin/sh" /etc/shells && grep -qF "/bin/ksh" /etc/shells; then + echo "PASS: busybox_remove_shell_noarg"; bb_case_pass +else + echo "FAIL_DETAIL: busybox_remove_shell_noarg (rc=$_rc)"; echo "$_t"; cat /etc/shells 2>/dev/null; bb_case_fail fi diff --git a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs index ade86a203f..86bb6cd023 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs @@ -83,6 +83,32 @@ impl DeviceOps for Null { } } +/// Placeholder root block device. starry has no real block-device backend for +/// the root mount; this node exists only so tools that resolve the root device +/// by scanning /dev (e.g. busybox `rdev`) can find a block node whose `rdev` +/// matches the root filesystem's `st_dev`. Real block I/O is unsupported: +/// read/write return `EIO` rather than silently succeeding, so the node never +/// masquerades as a working disk for `dd`/`blkid`/`fsck`. +struct RootBlk; + +impl DeviceOps for RootBlk { + fn read_at(&self, _buf: &mut [u8], _offset: u64) -> VfsResult { + Err(AxError::Io) + } + + fn write_at(&self, _buf: &[u8], _offset: u64) -> VfsResult { + Err(AxError::Io) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn flags(&self) -> NodeFlags { + NodeFlags::NON_CACHEABLE + } +} + struct Zero; impl DeviceOps for Zero { @@ -223,6 +249,20 @@ fn builder(fs: Arc) -> DirMaker { Arc::new(Random::new()), ), ); + // Root block device node. Its rdev must equal the root filesystem's st_dev + // so that tools resolving the root device by scanning /dev (e.g. busybox + // `rdev`, which stats "/" then looks for a block node with a matching + // st_rdev) can find it. The root mount is the first mount, so its + // `DEVICE_COUNTER` id is 1 (== `DeviceId::new(0, 1).0`). + root.add( + "vda", + Device::new( + fs.clone(), + NodeType::BlockDevice, + DeviceId::new(0, 1), + Arc::new(RootBlk), + ), + ); if ax_display::has_display() { root.add( "fb0", diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt new file mode 100644 index 0000000000..094258649c --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-rdev-vda C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-rdev-vda src/main.c) +target_include_directories(test-rdev-vda PRIVATE src) +target_compile_options(test-rdev-vda PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-rdev-vda RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c new file mode 100644 index 0000000000..00c8187ec0 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/main.c @@ -0,0 +1,50 @@ +/* + * test-rdev-vda — root block device node /dev/vda. + * + * starry has no real block backend for the root mount, but tools that resolve + * the root device by scanning /dev (notably busybox `rdev`, which stats "/", + * takes its st_dev, then looks for a block node in /dev whose st_rdev matches) + * need such a node to exist. The kernel exposes /dev/vda as a placeholder block + * device whose rdev equals the root filesystem's st_dev. Real I/O is + * unsupported (read/write return EIO) so it never masquerades as a working disk. + * + * This is the kernel-side regression for busybox_rdev printing `/dev/vda /`. + */ + +#include "test_framework.h" + +#include +#include +#include +#include + +int main(void) +{ + TEST_START("rdev: /dev/vda root block device"); + + struct stat root_st, vda_st; + CHECK(stat("/", &root_st) == 0, "stat / (root mount)"); + + int have_vda = (stat("/dev/vda", &vda_st) == 0); + CHECK(have_vda, "stat /dev/vda (root block device node exists)"); + if (have_vda) { + CHECK(S_ISBLK(vda_st.st_mode), "/dev/vda is a block device (S_ISBLK)"); + CHECK(vda_st.st_rdev == root_st.st_dev, + "/dev/vda st_rdev == root filesystem st_dev (busybox rdev resolves \"/\" -> /dev/vda)"); + + /* RootBlk returns EIO on real I/O — it is a resolver placeholder, not a + * working disk; it must not silently succeed for dd/blkid/fsck. */ + int fd = open("/dev/vda", O_RDONLY); + CHECK(fd >= 0, "open /dev/vda O_RDONLY"); + if (fd >= 0) { + char buf[16]; + errno = 0; + ssize_t n = read(fd, buf, sizeof buf); + CHECK(n < 0 && errno == EIO, + "read /dev/vda returns EIO (placeholder, no fake disk I/O)"); + close(fd); + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-rdev-vda/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0