diff --git a/.ci/boot-linux-prepare.sh b/.ci/boot-linux-prepare.sh new file mode 100755 index 000000000..c72108970 --- /dev/null +++ b/.ci/boot-linux-prepare.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +. .ci/common.sh + +check_platform + +VBLK_IMG=build/disk.img +which dd >/dev/null 2>&1 || { echo "Error: dd not found"; exit 1; } +which mkfs.ext4 >/dev/null 2>&1 || which $(brew --prefix e2fsprogs)/sbin/mkfs.ext4 >/dev/null 2>&1 || \ + { echo "Error: mkfs.ext4 not found"; exit 1; } +which 7z >/dev/null 2>&1 || { echo "Error: 7z not found"; exit 1; } + +ACTION=$1 + +case "$ACTION" in + setup) + # Setup a disk image + dd if=/dev/zero of=${VBLK_IMG} bs=4M count=32 + + # Setup a /dev/ block device with ${VBLK_IMG} to test guestOS access to hostOS /dev/ block device + case "${OS_TYPE}" in + Linux) + mkfs.ext4 ${VBLK_IMG} + BLK_DEV=$(losetup -f) + losetup ${BLK_DEV} ${VBLK_IMG} + ;; + Darwin) + $(brew --prefix e2fsprogs)/sbin/mkfs.ext4 ${VBLK_IMG} + BLK_DEV=$(hdiutil attach -nomount ${VBLK_IMG}) + ;; + esac + + # On Linux, ${VBLK_IMG} will be created by root and owned by root:root. + # Even if "others" have read and write (rw) permissions, accessing the file for certain operations may + # still require elevated privileges (e.g., setuid). + # To simplify this, we change the ownership to a non-root user. + # Use this with caution—changing ownership to runner:runner is specific to the GitHub CI environment. + chown runner: ${VBLK_IMG} + # Add other's rw permission to the disk image and device, so non-superuser can rw them + chmod o+r,o+w ${VBLK_IMG} + chmod o+r,o+w ${BLK_DEV} + + # Export ${BLK_DEV} to a tmp file. Then, source to "$GITHUB_ENV" in job step. + echo "export BLK_DEV=${BLK_DEV}" > "${TMP_FILE}" + ;; + cleanup) + # Detach the /dev/loopx(Linux) or /dev/diskx(Darwin) + case "${OS_TYPE}" in + Linux) + losetup -d ${BLK_DEV} + ;; + Darwin) + hdiutil detach ${BLK_DEV} + ;; + esac + + # delete disk image + rm -f ${VBLK_IMG} + + # delete tmp file + rm "${TMP_FILE}" + ;; + *) + printf "Usage: %s {setup|cleanup}\n" "$0" + exit 1 + ;; +esac diff --git a/.ci/boot-linux.sh b/.ci/boot-linux.sh index f439fadb3..21feaefb4 100755 --- a/.ci/boot-linux.sh +++ b/.ci/boot-linux.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +. .ci/common.sh + +check_platform + function cleanup { sleep 1 pkill -9 rv32emu @@ -8,9 +12,9 @@ function cleanup { function ASSERT { $* local RES=$? - if [ $RES -ne 0 ]; then + if [ ${RES} -ne 0 ]; then echo 'Assert failed: "' $* '"' - exit $RES + exit ${RES} fi } @@ -18,61 +22,88 @@ cleanup ENABLE_VBLK=1 VBLK_IMG=build/disk.img -which dd >/dev/null 2>&1 || ENABLE_VBLK=0 -which mkfs.ext4 >/dev/null 2>&1 || which $(brew --prefix e2fsprogs)/sbin/mkfs.ext4 >/dev/null 2>&1 || ENABLE_VBLK=0 -which 7z >/dev/null 2>&1 || ENABLE_VBLK=0 +[ -f "${VBLK_IMG}" ] || ENABLE_VBLK=0 TIMEOUT=50 -OPTS=" -k build/linux-image/Image " -OPTS+=" -i build/linux-image/rootfs.cpio " -if [ "$ENABLE_VBLK" -eq "1" ]; then - dd if=/dev/zero of=$VBLK_IMG bs=4M count=32 - mkfs.ext4 $VBLK_IMG || $(brew --prefix e2fsprogs)/sbin/mkfs.ext4 $VBLK_IMG - OPTS+=" -x vblk:$VBLK_IMG " -else - printf "Virtio-blk Test...Passed\n" -fi -RUN_LINUX="build/rv32emu ${OPTS}" +OPTS_BASE=" -k build/linux-image/Image" +OPTS_BASE+=" -i build/linux-image/rootfs.cpio" -if [ "$ENABLE_VBLK" -eq "1" ]; then -ASSERT expect < mnt/emu.txt\n" } timeout { exit 3 } -expect "# " { send "sync\n" } timeout { exit 3 } -expect "# " { send "umount mnt\n" } timeout { exit 3 } -expect "# " { send "\x01"; send "x" } timeout { exit 3 } -DONE -else -ASSERT expect </dev/null 2>&1 || ret=4 - printf "Virtio-blk Test: [ ${MESSAGES[$ret]}${COLOR_N} ]\n" +if [ "${ENABLE_VBLK}" -eq "1" ]; then + # Read-only + TEST_OPTIONS+=("${OPTS_BASE} -x vblk:${VBLK_IMG},readonly") + EXPECT_CMDS+=(' + expect "buildroot login:" { send "root\n" } timeout { exit 1 } + expect "# " { send "uname -a\n" } timeout { exit 2 } + expect "riscv32 GNU/Linux" { send "mkdir mnt && mount /dev/vda mnt\n" } timeout { exit 3 } + expect "# " { send "echo rv32emu > mnt/emu.txt\n" } timeout { exit 3 } + expect -ex "-sh: can'\''t create mnt/emu.txt: Read-only file system" {} timeout { exit 3 } + expect "# " { send "\x01"; send "x" } timeout { exit 3 } + ') + + # Read-write using disk image + TEST_OPTIONS+=("${OPTS_BASE} -x vblk:${VBLK_IMG}") + VBLK_EXPECT_CMDS=' + expect "buildroot login:" { send "root\n" } timeout { exit 1 } + expect "# " { send "uname -a\n" } timeout { exit 2 } + expect "riscv32 GNU/Linux" { send "mkdir mnt && mount /dev/vda mnt\n" } timeout { exit 3 } + expect "# " { send "echo rv32emu > mnt/emu.txt\n" } timeout { exit 3 } + expect "# " { send "sync\n" } timeout { exit 3 } + expect "# " { send "umount mnt\n" } timeout { exit 3 } + expect "# " { send "\x01"; send "x" } timeout { exit 3 } + ' + EXPECT_CMDS+=("${VBLK_EXPECT_CMDS}") + + # Read-write using /dev/loopx(Linux) or /dev/diskx(Darwin) block device + TEST_OPTIONS+=("${OPTS_BASE} -x vblk:${BLK_DEV}") + EXPECT_CMDS+=("${VBLK_EXPECT_CMDS}") fi +for i in "${!TEST_OPTIONS[@]}"; do + printf "${COLOR_Y}===== Test option: ${TEST_OPTIONS[$i]} =====${COLOR_N}\n" + + OPTS="${OPTS_BASE}" + # No need to add option when running base test + if [[ ! "${TEST_OPTIONS[$i]}" =~ "base" ]]; then + OPTS+="${TEST_OPTIONS[$i]}" + fi + RUN_LINUX="build/rv32emu ${OPTS}" + + ASSERT expect <<-DONE + set timeout ${TIMEOUT} + spawn ${RUN_LINUX} + ${EXPECT_CMDS[$i]} + DONE + + ret=$? + cleanup + + printf "\nBoot Linux Test: [ ${MESSAGES[$ret]}${COLOR_N} ]\n" + if [[ "${TEST_OPTIONS[$i]}" =~ vblk ]]; then + # read-only test first, so the emu.txt definitely does not exist, skipping the check + if [[ ! "${TEST_OPTIONS[$i]}" =~ readonly ]]; then + 7z l ${VBLK_IMG} | grep emu.txt >/dev/null 2>&1 || ret=4 + fi + printf "Virtio-blk Test: [ ${MESSAGES[$ret]}${COLOR_N} ]\n" + fi +done + exit ${ret} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a4fbd528..b5ce486a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,7 +65,14 @@ jobs: actions-cache-folder: 'emsdk-cache' - name: Set parallel jobs variable run: | - echo "PARALLEL=-j$(nproc)" >> $GITHUB_ENV + echo "PARALLEL=-j$(nproc)" >> "$GITHUB_ENV" + echo "BOOT_LINUX_TEST=TMP_FILE=\$(mktemp "$RUNNER_TEMP/tmpfile.XXXXXX"); \ + sudo env TMP_FILE=\${TMP_FILE} .ci/boot-linux-prepare.sh setup; \ + . \${TMP_FILE}; \ + .ci/boot-linux.sh; \ + EXIT_CODE=\$?; \ + sudo env TMP_FILE=\${TMP_FILE} BLK_DEV=\${BLK_DEV} .ci/boot-linux-prepare.sh cleanup; \ + exit \${EXIT_CODE};" >> "$GITHUB_ENV" - name: fetch artifact first to reduce HTTP requests env: CC: ${{ steps.install_cc.outputs.cc }} @@ -265,7 +272,7 @@ jobs: CC: ${{ steps.install_cc.outputs.cc }} run: | make distclean && make INITRD_SIZE=32 ENABLE_SYSTEM=1 $PARALLEL && make ENABLE_SYSTEM=1 artifact $PARALLEL - .ci/boot-linux.sh + bash -c "${BOOT_LINUX_TEST}" make ENABLE_SYSTEM=1 clean if: ${{ always() }} - name: boot Linux kernel test (JIT) @@ -273,7 +280,7 @@ jobs: CC: ${{ steps.install_cc.outputs.cc }} run: | make distclean && make INITRD_SIZE=32 ENABLE_SYSTEM=1 ENABLE_JIT=1 ENABLE_T2C=0 ENABLE_MOP_FUSION=0 $PARALLEL && make ENABLE_SYSTEM=1 artifact $PARALLEL - .ci/boot-linux.sh + bash -c "${BOOT_LINUX_TEST}" make ENABLE_SYSTEM=1 ENABLE_JIT=1 ENABLE_T2C=0 ENABLE_MOP_FUSION=0 clean if: ${{ always() }} - name: Architecture test @@ -292,7 +299,7 @@ jobs: uses: actions/checkout@v4 - name: Set parallel jobs variable run: | - echo "PARALLEL=-j$(nproc)" >> $GITHUB_ENV + echo "PARALLEL=-j$(nproc)" >> "$GITHUB_ENV" - name: build artifact # The GitHub Action for non-x86 CPU uses: allinurl/run-on-arch-action@master @@ -351,7 +358,14 @@ jobs: actions-cache-folder: 'emsdk-cache' - name: Set parallel jobs variable run: | - echo "PARALLEL=-j$(sysctl -n hw.logicalcpu)" >> $GITHUB_ENV + echo "PARALLEL=-j$(sysctl -n hw.logicalcpu)" >> "$GITHUB_ENV" + echo "BOOT_LINUX_TEST=TMP_FILE=\$(mktemp "$RUNNER_TEMP/tmpfile.XXXXXX"); \ + sudo env TMP_FILE=\${TMP_FILE} .ci/boot-linux-prepare.sh setup; \ + . \${TMP_FILE}; \ + .ci/boot-linux.sh; \ + EXIT_CODE=\$?; \ + sudo env TMP_FILE=\${TMP_FILE} BLK_DEV=\${BLK_DEV} .ci/boot-linux-prepare.sh cleanup; \ + exit \${EXIT_CODE};" >> "$GITHUB_ENV" - name: Symlink gcc-14 due to the default /usr/local/bin/gcc links to system's clang run: | ln -s /opt/homebrew/opt/gcc/bin/gcc-14 /usr/local/bin/gcc-14 @@ -463,7 +477,7 @@ jobs: run: | make distclean && make INITRD_SIZE=32 ENABLE_SYSTEM=1 $PARALLEL && \ make ENABLE_SYSTEM=1 artifact $PARALLEL - .ci/boot-linux.sh + bash -c "${BOOT_LINUX_TEST}" make ENABLE_SYSTEM=1 clean if: ${{ always() }} - name: Architecture test diff --git a/README.md b/README.md index 092a57572..dd218d54c 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Generate ext4 image file for virtio block device in Unix-like system: $ dd if=/dev/zero of=disk.img bs=4M count=32 $ mkfs.ext4 disk.img ``` +Instead of creating a new block device image, you can share the hostOS's existing block devices. For example, on macOS host, specify the block device path as `-x vblk:/dev/disk3`, or on Linux host as `-x vblk:/dev/loop3`, assuming these paths point to valid block devices. + Mount the virtual block device and create a test file after booting, note that root privilege is required to mount and unmount a disk: ```shell # mkdir mnt diff --git a/src/devices/virtio-blk.c b/src/devices/virtio-blk.c index a93f04275..5ee340857 100644 --- a/src/devices/virtio-blk.c +++ b/src/devices/virtio-blk.c @@ -4,15 +4,30 @@ */ #include +#include #include +#include #include #include #include #include +#include #include #include #include +/* + * The /dev/ block devices cannot be embedded to the part of the wasm. + * Thus, accessing /dev/ block devices is not supported for wasm. + */ +#if !defined(__EMSCRIPTEN__) +#if defined(__APPLE__) +#include /* DKIOCGETBLOCKCOUNT and DKIOCGETBLOCKSIZE */ +#else +#include /* BLKGETSIZE64 */ +#endif +#endif /* !defined(__EMSCRIPTEN__) */ + #include "virtio.h" #define DISK_BLK_SIZE 512 @@ -97,12 +112,16 @@ static void virtio_blk_update_status(virtio_blk_state_t *vblk, uint32_t status) uint32_t device_features = vblk->device_features; uint32_t *ram = vblk->ram; uint32_t *disk = vblk->disk; + uint64_t disk_size = vblk->disk_size; + int disk_fd = vblk->disk_fd; void *priv = vblk->priv; uint32_t capacity = VBLK_PRIV(vblk)->capacity; memset(vblk, 0, sizeof(*vblk)); vblk->device_features = device_features; vblk->ram = ram; vblk->disk = disk; + vblk->disk_size = disk_size; + vblk->disk_fd = disk_fd; vblk->priv = priv; VBLK_PRIV(vblk)->capacity = capacity; } @@ -388,6 +407,12 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, exit(EXIT_FAILURE); } + /* + * For mmap_fallback, if vblk is not specified, disk_fd should remain -1 and + * no fsync should be performed on exit. + */ + vblk->disk_fd = -1; + /* Allocate memory for the private member */ vblk->priv = &vblk_configs[vblk_dev_cnt++]; @@ -402,14 +427,53 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, /* Open disk file */ int disk_fd = open(disk_file, readonly ? O_RDONLY : O_RDWR); if (disk_fd < 0) { - rv_log_error("Could not open %s", disk_file); - exit(EXIT_FAILURE); + rv_log_error("Could not open %s: %s", disk_file, strerror(errno)); + goto fail; } - /* Get the disk image size */ struct stat st; - fstat(disk_fd, &st); - VBLK_PRIV(vblk)->disk_size = st.st_size; + if (fstat(disk_fd, &st) == -1) { + rv_log_error("fstat failed: %s", strerror(errno)); + goto disk_size_fail; + } + + const char *disk_file_dirname = dirname(disk_file); + if (!disk_file_dirname) { + rv_log_error("Fail dirname disk_file: %s: %s", disk_file, + strerror(errno)); + goto disk_size_fail; + } + /* Get the disk size */ + uint64_t disk_size; + if (!strcmp(disk_file_dirname, "/dev")) { /* from /dev/, leverage ioctl */ + if ((st.st_mode & S_IFMT) != S_IFBLK) { + rv_log_error("%s is not block device", disk_file); + goto fail; + } +#if !defined(__EMSCRIPTEN__) +#if defined(__APPLE__) + uint32_t block_size; + uint64_t block_count; + if (ioctl(disk_fd, DKIOCGETBLOCKCOUNT, &block_count) == -1) { + rv_log_error("DKIOCGETBLOCKCOUNT failed: %s", strerror(errno)); + goto disk_size_fail; + } + if (ioctl(disk_fd, DKIOCGETBLOCKSIZE, &block_size) == -1) { + rv_log_error("DKIOCGETBLOCKSIZE failed: %s", strerror(errno)); + goto disk_size_fail; + } + disk_size = block_count * block_size; +#else /* Linux */ + if (ioctl(disk_fd, BLKGETSIZE64, &disk_size) == -1) { + rv_log_error("BLKGETSIZE64 failed: %s", strerror(errno)); + goto disk_size_fail; + } +#endif +#endif /* !defined(__EMSCRIPTEN__) */ + } else { /* other path, get the size of block device via stat buffer */ + disk_size = st.st_size; + } + VBLK_PRIV(vblk)->disk_size = disk_size; /* Set up the disk memory */ uint32_t *disk_mem; @@ -417,15 +481,38 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, disk_mem = mmap(NULL, VBLK_PRIV(vblk)->disk_size, readonly ? PROT_READ : (PROT_READ | PROT_WRITE), MAP_SHARED, disk_fd, 0); - if (disk_mem == MAP_FAILED) - goto err; -#else + if (disk_mem == MAP_FAILED) { + if (errno != EINVAL) + goto disk_mem_err; + /* + * On Apple platforms, mmap() on block devices appears to be unsupported + * and EINVAL is set to errno. + */ + rv_log_trace( + "Fallback to malloc-based block device due to mmap() failure"); + goto mmap_fallback; + } + /* + * disk_fd should be closed on exit after flushing heap data back to the + * device when using mmap_fallback. + */ + close(disk_fd); + goto disk_mem_ok; +#endif + +mmap_fallback: disk_mem = malloc(VBLK_PRIV(vblk)->disk_size); if (!disk_mem) - goto err; -#endif + goto disk_mem_err; + vblk->disk_fd = disk_fd; + vblk->disk_size = disk_size; + if (pread(disk_fd, disk_mem, disk_size, 0) == -1) { + rv_log_error("pread block device failed: %s", strerror(errno)); + goto disk_mem_err; + } + +disk_mem_ok: assert(!(((uintptr_t) disk_mem) & 0b11)); - close(disk_fd); vblk->disk = disk_mem; VBLK_PRIV(vblk)->capacity = @@ -436,9 +523,14 @@ uint32_t *virtio_blk_init(virtio_blk_state_t *vblk, return disk_mem; -err: - rv_log_error("Could not map disk %s", disk_file); - return NULL; +disk_mem_err: + rv_log_error("Could not map disk %s: %s", disk_file, strerror(errno)); + +disk_size_fail: + close(disk_fd); + +fail: + exit(EXIT_FAILURE); } virtio_blk_state_t *vblk_new() @@ -450,10 +542,12 @@ virtio_blk_state_t *vblk_new() void vblk_delete(virtio_blk_state_t *vblk) { + /* mmap_fallback is used */ + if (vblk->disk_fd != -1) + free(vblk->disk); #if HAVE_MMAP - munmap(vblk->disk, VBLK_PRIV(vblk)->disk_size); -#else - free(vblk->disk); + else + munmap(vblk->disk, VBLK_PRIV(vblk)->disk_size); #endif free(vblk); } diff --git a/src/devices/virtio.h b/src/devices/virtio.h index b0cbadd3a..28da79f78 100644 --- a/src/devices/virtio.h +++ b/src/devices/virtio.h @@ -103,6 +103,8 @@ typedef struct { /* supplied by environment */ uint32_t *ram; uint32_t *disk; + uint64_t disk_size; + int disk_fd; /* implementation-specific */ void *priv; } virtio_blk_state_t; diff --git a/src/main.c b/src/main.c index bed7bc7a5..8e366519c 100644 --- a/src/main.c +++ b/src/main.c @@ -298,6 +298,11 @@ int main(int argc, char **args) /* finalize the RISC-V runtime */ rv_delete(rv); + /* + * Other translation units cannot update the pointer, update it here + * to prevent multiple atexit()'s callback be called. + */ + rv = NULL; rv_log_info("RISC-V emulator is destroyed"); end: diff --git a/src/riscv.c b/src/riscv.c index a5f344ec0..36cabefb5 100644 --- a/src/riscv.c +++ b/src/riscv.c @@ -390,7 +390,43 @@ static void rv_async_block_clear() return; #endif /* !RV32_HAS(JIT) */ } -#endif + +static void rv_fsync_device() +{ + if (!rv) + return; + + vm_attr_t *attr = PRIV(rv); + /* + * mmap_fallback, may need to write and sync the device + * + * vblk is optional, so it could be NULL + */ + if (attr->vblk) { + if (attr->vblk->disk_fd >= 3) { + if (attr->vblk->device_features & VIRTIO_BLK_F_RO) /* readonly */ + goto end; + + if (pwrite(attr->vblk->disk_fd, attr->vblk->disk, + attr->vblk->disk_size, 0) == -1) { + rv_log_error("pwrite block device failed: %s", strerror(errno)); + return; + } + + if (fsync(attr->vblk->disk_fd) == -1) { + rv_log_error("fsync block device failed: %s", strerror(errno)); + return; + } + rv_log_info("Sync block device OK"); + + end: + close(attr->vblk->disk_fd); + } + + vblk_delete(attr->vblk); + } +} +#endif /* RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER) */ riscv_t *rv_create(riscv_user_t rv_attr) { @@ -402,6 +438,8 @@ riscv_t *rv_create(riscv_user_t rv_attr) #if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER) /* register cleaning callback for CTRL+a+x exit */ atexit(rv_async_block_clear); + /* register device sync callback for CTRL+a+x exit */ + atexit(rv_fsync_device); #endif /* copy over the attr */ @@ -549,6 +587,7 @@ riscv_t *rv_create(riscv_user_t rv_attr) attr->uart->out_fd = attr->fd_stdout; /* setup virtio-blk */ + attr->vblk = NULL; if (attr->data.system.vblk_device) { /* Currently, only used for block image path and permission */ #define MAX_OPTS 2 @@ -719,7 +758,8 @@ void rv_delete(riscv_t *rv) #if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER) u8250_delete(attr->uart); plic_delete(attr->plic); - vblk_delete(attr->vblk); + /* sync device, cleanup inside the callee */ + rv_fsync_device(); #endif free(rv); }