diff --git a/apps/starry/gdb/build-x86_64-unknown-none.toml b/apps/starry/gdb/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..1c8a2eeca2 --- /dev/null +++ b/apps/starry/gdb/build-x86_64-unknown-none.toml @@ -0,0 +1,5 @@ +target = "x86_64-unknown-none" +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +log = "Warn" +features = ["qemu"] +plat_dyn = false diff --git a/apps/starry/gdb/prebuild.sh b/apps/starry/gdb/prebuild.sh new file mode 100755 index 0000000000..960d42acdb --- /dev/null +++ b/apps/starry/gdb/prebuild.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +base_rootfs="${STARRY_BASE_ROOTFS:-}" +staging_root="${STARRY_STAGING_ROOT:-}" +overlay_dir="${STARRY_OVERLAY_DIR:-}" + +READELF="${READELF:-/opt/homebrew/opt/binutils/bin/readelf}" + +require_env() { + local name="$1" + local value="$2" + if [[ -z "$value" ]]; then + echo "error: $name is required" >&2 + exit 1 + fi +} + +ensure_host_tools() { + local missing=() + + command -v debugfs >/dev/null 2>&1 || missing+=(e2fsprogs) + command -v install >/dev/null 2>&1 || missing+=(coreutils) + command -v docker >/dev/null 2>&1 || missing+=(docker) + command -v "$READELF" >/dev/null 2>&1 || missing+=("readelf (binutils)") + + if [[ ${#missing[@]} -eq 0 ]]; then + return + fi + + echo "error: missing required host tools: ${missing[*]}" >&2 + exit 1 +} + +extract_base_rootfs() { + debugfs -R "rdump / $staging_root" "$base_rootfs" +} + +install_gdb_package() { + echo "Installing gdb via Docker (Alpine x86_64)..." + docker run --rm --platform linux/amd64 \ + -v "$staging_root:/staging" \ + alpine:edge apk --root /staging --no-scripts add gdb +} + +copy_file_to_overlay() { + local guest_path="$1" + local mode="$2" + local source="$staging_root${guest_path}" + local target="$overlay_dir${guest_path}" + + if [[ ! -e "$source" ]]; then + echo "error: missing guest file after gdb package install: $guest_path" >&2 + exit 1 + fi + + if [[ -L "$source" ]]; then + source="$(readlink -f "$source")" + fi + + install -Dm"$mode" "$source" "$target" +} + +find_library_path() { + local library="$1" + local dir + + for dir in lib usr/lib usr/local/lib; do + if [[ -e "$staging_root/$dir/$library" ]]; then + printf '/%s/%s\n' "$dir" "$library" + return 0 + fi + done + + return 1 +} + +copy_runtime_dependencies() { + local pending=("$@") + local seen=" " + local guest_path library + + while [[ ${#pending[@]} -gt 0 ]]; do + guest_path="${pending[0]}" + pending=("${pending[@]:1}") + + if [[ "$seen" == *" $guest_path "* ]]; then + continue + fi + seen+="$guest_path " + + while IFS= read -r library; do + local library_path + if ! library_path="$(find_library_path "$library")"; then + continue + fi + copy_file_to_overlay "$library_path" 0644 + pending+=("$library_path") + done < <( + "$READELF" -d "$staging_root$guest_path" 2>/dev/null | + sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' + ) + done +} + +populate_overlay() { + copy_file_to_overlay /usr/bin/gdb 0755 + copy_runtime_dependencies /usr/bin/gdb +} + +require_env STARRY_BASE_ROOTFS "$base_rootfs" +require_env STARRY_STAGING_ROOT "$staging_root" +require_env STARRY_OVERLAY_DIR "$overlay_dir" + +ensure_host_tools +extract_base_rootfs +install_gdb_package +populate_overlay diff --git a/apps/starry/gdb/qemu-x86_64.toml b/apps/starry/gdb/qemu-x86_64.toml new file mode 100644 index 0000000000..6f54f5bb99 --- /dev/null +++ b/apps/starry/gdb/qemu-x86_64.toml @@ -0,0 +1,20 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "gdb --version && echo GDB_SMOKE_PASSED" +success_regex = ["(?m)^GDB_SMOKE_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b'] +timeout = 120 diff --git a/os/StarryOS/docs/mount-umount2-linux-compat.md b/os/StarryOS/docs/mount-umount2-linux-compat.md index 6d1f7c808b..05f535ebd5 100644 --- a/os/StarryOS/docs/mount-umount2-linux-compat.md +++ b/os/StarryOS/docs/mount-umount2-linux-compat.md @@ -3,10 +3,14 @@ 本文档说明本次在 StarryOS 中围绕 `sys_mount()` / `sys_umount2()` 做的 Linux 兼容性补齐,重点覆盖: - `mount(2)` 中的 propagation flags 组合校验 +- `MS_SHARED` / `MS_PRIVATE` / `MS_SLAVE` / `MS_UNBINDABLE` - `MS_BIND` / `MS_BIND|MS_REC` +- bind 子目录 - `MS_MOVE` +- `MS_MOVE` 对 descendant target 的 `ELOOP` - `MS_REMOUNT` - `MS_RDONLY` +- `MS_REMOUNT|MS_BIND|MS_RDONLY` - `umount2(2)` 中的 `MNT_EXPIRE` - `UMOUNT_NOFOLLOW` - `MNT_DETACH` @@ -84,19 +88,20 @@ Linux 中: #### 本次 StarryOS 的实现边界 -这次 StarryOS 只实现了: +这次 StarryOS 已经实现到“可被用户态测试验证”的 shared subtree 语义: -- 非法组合检查 -- propagation-only 调用不再错误地新建一个 fresh mount -- 对已有 mount 返回成功,并保持已有内容不变 +- propagation flags 非法组合检查 +- `MS_SHARED`:shared mount 的 bind peer 进入同一传播组 +- `MS_PRIVATE`:从 shared/slave 关系中脱离,停止后续传播 +- `MS_SLAVE`:接收 master 的传播,但不向 master 反向传播 +- `MS_UNBINDABLE`:禁止直接 bind,并在 recursive bind 时剪掉 unbindable child mount +- shared mount 下新增 child mount 时,对 peer / slave 做传播 -这次 **没有** 实现真实的 shared subtree 传播机制,也就是: +仍未完全实现的,是 Linux mount namespace 更细的传播细节,例如: -- 新 mount 在 shared peer 之间自动传播 -- slave 接收而不反向传播 -- unbindable 禁止被 bind - -这些属于后续更深层的 mount namespace / propagation tree 语义。 +- 更完整的 peer group 生命周期 +- 更复杂的递归 propagation corner case +- namespace 之间更深层的传播隔离规则 ### 3.2 `MS_BIND` @@ -114,6 +119,7 @@ Linux 中: - 顶层目录内容应该一致 - 但 source 树下面如果有更深一层的子挂载点,默认不应该自动出现在 bind 目标里 +- source 不必是 mount root,也可以是 mount 内部的普通子目录 #### `MS_BIND|MS_REC` @@ -130,6 +136,7 @@ Linux 中: | `MS_BIND|MS_REC` | 递归 bind,nested submount 也可见 | | `MS_BIND|MS_RDONLY` | 新的 bind mount 视图只读 | | `MS_REMOUNT|MS_BIND|MS_RDONLY` | 把已有 bind mount remount 成只读 | +| `MS_BIND` on unbindable source | `EINVAL` | ### 3.3 `MS_MOVE` @@ -142,6 +149,7 @@ Linux 中: | 新路径 | 应看到原 mount 的内容 | | 旧路径 | 不再是那个 mount 的可见入口 | | mount 本身 | 不是复制,而是移动 | +| target 是 source 后代 | `ELOOP` | 这类操作的关键不是“新建一个 mount”,而是“修改 mount tree 中 parent-child 关系”。 @@ -177,6 +185,14 @@ Linux 中: | `mount(..., MS_REMOUNT|MS_RDONLY, ...)` | remount 成功,已有内容保留,后续写入 `EROFS` | | `mount(..., MS_REMOUNT|MS_BIND|MS_RDONLY, ...)` | bind mount 视图只读,但源 mount 仍可写 | +除了普通 `write()`,Linux 风格的只读 mount 还应拒绝: + +- `chmod` +- `rename` +- `unlink` +- `mkdir` +- 已打开 fd 的后续 `write` / `append` + ## 4. Linux `umount2(2)` 语义矩阵 Linux `umount2()` 原型: @@ -284,7 +300,7 @@ match fs_type.as_str() { - `MS_MOVE` / `MS_BIND` 都属于 mount tree 操作,不应该落进 `tmpfs` / `ext4` 分支 - 只有当这些“特殊 mount 操作”都不命中时,才说明这是普通挂载 -## 5.2 propagation flags:先校验合法性,再对已有 mount 返回成功 +## 5.2 propagation flags:先校验合法性,再按 mountpoint 状态更新传播关系 本次实现: @@ -305,6 +321,16 @@ if propagation != 0 { if !target.is_root_of_mount() { return Err(AxError::InvalidInput); } + let mountpoint = target.mountpoint(); + if (propagation & MS_PRIVATE) != 0 { + mountpoint.set_private(); + } else if (propagation & MS_SHARED) != 0 { + mountpoint.set_shared(); + } else if (propagation & MS_SLAVE) != 0 { + mountpoint.set_slave(); + } else if (propagation & MS_UNBINDABLE) != 0 { + mountpoint.set_unbindable(); + } return Ok(0); } ``` @@ -313,7 +339,8 @@ if propagation != 0 { - `count_ones() > 1` 对应 Linux 的“多个 propagation type flags 同时出现是 `EINVAL`” - `flags & !allowed != 0` 对应 Linux 的“propagation type flags 只能和 `MS_REC` / `MS_SILENT` 共存” -- 这里当前只是让 StarryOS 具备“不要误挂新 fs”的语义边界,真实传播机制仍是后续工作 +- propagation-only 调用仍然不新建 fs,而是修改已有 mountpoint 的传播属性 +- 传播状态现在是 mountpoint 级别的,而不是底层 inode 级别的 ## 5.3 `axfs-ng-vfs::Mountpoint`:把 mountpoint 当成 mount 语义状态承载体 @@ -327,6 +354,7 @@ pub struct Mountpoint { device: u64, readonly: AtomicBool, expired: AtomicBool, + propagation: Mutex, } ``` @@ -341,6 +369,7 @@ Linux 里的这些语义,本质上都是“对 mount 实例”的属性,而 - bind mount 视图只读,不应必然把 source mount 一起变只读 - `MNT_EXPIRE` 的 expired 标记属于 mount,不属于底层文件 - lazy detach 也是从 mount tree 摘除,而不是销毁底层文件对象 +- shared/private/slave/unbindable 也属于 mount 之间的关系,而不是目录项自己的关系 因此在 StarryOS 里,把这类状态绑定到 `Mountpoint` 是合理且稳定的。 @@ -371,11 +400,15 @@ fn bind(source: &Location, location_in_parent: Location, recursive: bool) -> Arc ### 讲解 -- 顶层 bind 的本质是:目标 mountpoint 的 root 指向与 source 相同的目录入口 +- 顶层 bind 的本质是:目标 mountpoint 的 root 指向与 source 当前可见的目录入口 +- source 可以是 mount 内部的普通子目录,不必是 mount root - 普通 bind 时,不拷贝 `children` - 所以 nested submount 在目标树下不可见 - recursive bind 时,把 `children` 也带过去 - 所以 nested submount 在目标树下继续可见 +- 如果 source mount 被标记为 unbindable: + - 直接 bind 返回 `EINVAL` + - recursive bind 时会跳过 unbindable child mount 这正对应 Linux 中: @@ -415,6 +448,15 @@ fn resolve_mountpoint(self) -> Self { 这让“是否递归”真正体现在 mount tree 本身,而不是体现在共享的底层目录节点上。 +同样的机制也被用在 shared/slave propagation 上: + +- shared mount 新增 child mount 时 +- 先计算 child 相对 mount root 的路径 +- 再在 peer / slave 那边找到同一路径位置 +- 最后把 child mount 暴露到对端对应的 `children` map 中 + +这样 peer 看到的是“对端树中相同相对路径上的 child mount”,而不是错误地复用源侧目录项位置。 + ## 5.6 `MS_MOVE`:修改 mount tree 的 parent-child 关系 实现片段: @@ -446,6 +488,12 @@ pub fn move_to(self: &Arc, new_location: &Location) -> VfsResult<()> { 这正是 Linux `MS_MOVE` 的语义:移动现有 mount,而不是复制。 +另外本次还补了一个 Linux 风格错误条件: + +- 如果 target 是 source 后代,返回 `ELOOP` + +这个检查的本质是:沿着 target 的 parent 链向上走,若遇到当前被移动的 mount root,则说明会形成循环。 + ## 5.7 `MS_RDONLY` / `MS_REMOUNT|MS_RDONLY`:按 mountpoint 拒绝写入 ### 在 `sys_mount()` 中设置只读位 @@ -500,6 +548,13 @@ if self.inner.location().is_readonly() - `MS_REMOUNT|MS_RDONLY` 后已有内容保留,但新增写入 `EROFS` - `MS_REMOUNT|MS_BIND|MS_RDONLY` 只让 bind 视图只读,而源 mount 保持可写 +此外,本次还把只读拦截扩展到了元数据/目录修改路径: + +- `Location::update_metadata()` 上拒绝 `chmod` +- 目录创建/删除/重命名路径拒绝 `mkdir` / `unlink` / `rename` + +这样用户态就能观察到更完整的 Linux 风格 `EROFS` 行为,而不只是普通文件写入失败。 + ## 5.8 `MNT_EXPIRE`:两阶段过期标记 实现片段: @@ -607,13 +662,16 @@ pub fn detach(self: &Arc) -> VfsResult<()> { |---|---|---| | `MS_SHARED | MS_PRIVATE` | `EINVAL` | 已支持 | | `MS_SHARED | MS_BIND` | `EINVAL` | 已支持 | -| `MS_PRIVATE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | -| `MS_SHARED` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | -| `MS_SLAVE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | -| `MS_UNBINDABLE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | +| `MS_PRIVATE` | 修改已有 mount 的传播属性 | 已支持当前测试覆盖的 private 语义 | +| `MS_SHARED` | 修改已有 mount 的传播属性 | 已支持当前测试覆盖的 shared peer 传播语义 | +| `MS_SLAVE` | 修改已有 mount 的传播属性 | 已支持当前测试覆盖的 slave 单向传播语义 | +| `MS_UNBINDABLE` | 修改已有 mount 的传播属性 | 已支持 bind 禁止与 recursive bind prune | | `MS_BIND` | 普通 bind,不带 nested submount | 已支持 | +| `MS_BIND` on subdirectory | bind mount 内部子目录 | 已支持 | | `MS_BIND|MS_REC` | recursive bind,带 nested submount | 已支持 | +| `MS_BIND|MS_REC` with unbindable child | 剪掉 unbindable child mount | 已支持 | | `MS_MOVE` | 移动已有 mount | 已支持 | +| `MS_MOVE` to descendant target | `ELOOP` | 已支持 | | `MS_REMOUNT` | remount 现有 mount,内容保留 | 已支持 | | `MS_RDONLY` | 挂成只读,写入 `EROFS` | 已支持 | | `MS_REMOUNT|MS_RDONLY` | remount 成只读,写入 `EROFS` | 已支持 | @@ -641,8 +699,13 @@ pub fn detach(self: &Arc) -> VfsResult<()> { 它覆盖的核心点是: - propagation flags 的非法组合和无副作用保证 +- shared/private/slave/unbindable 的真实传播或隔离效果 - propagation-only 调用不应替换现有 mount 内容 - bind / recursive bind / move / remount / readonly +- bind 子目录 +- recursive bind 对 unbindable child 的 prune +- move descendant target 的 `ELOOP` +- readonly mount 上的 `chmod` / `rename` / `unlink` / `mkdir` - `umount2` 的 invalid flags / invalid combo / expire / nofollow / detach 这也是为什么这次修复能够比较扎实: @@ -654,15 +717,16 @@ pub fn detach(self: &Arc) -> VfsResult<()> { 虽然这次 `util-linux` 测试已经全绿,但仍有一些 Linux mount/namespace 语义没有在本轮实现: -- 真实 shared subtree propagation -- `MS_UNBINDABLE` 对后续 bind 的禁止效果 -- 更完整的 mount namespace 传播行为 +- 更完整的 shared subtree propagation corner case +- 更复杂 namespace 拓扑下的传播行为 +- 真实 peer group 生命周期管理 - 其他普通 mount flags,如 `MS_NODEV`、`MS_NOSUID`、`MS_NOEXEC` 等 - `MNT_FORCE` 的真实强制卸载语义 因此更准确地说: - 这次已经把本轮补的 `mount/umount2` Linux 兼容测试全部打绿 +- 而且 shared/private/slave/unbindable / bind / move / readonly / umount2 核心路径已经具备稳定可验证的 Linux 风格语义 - 但并不意味着 Linux 整体 mount namespace 语义已经“全部实现” ## 9. 参考 diff --git a/os/StarryOS/kernel/src/syscall/task/ptrace.rs b/os/StarryOS/kernel/src/syscall/task/ptrace.rs index ca3903eb72..ad356c48a5 100644 --- a/os/StarryOS/kernel/src/syscall/task/ptrace.rs +++ b/os/StarryOS/kernel/src/syscall/task/ptrace.rs @@ -1,6 +1,6 @@ use alloc::{sync::Arc, vec, vec::Vec}; use core::mem::{MaybeUninit, size_of}; -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] use core::slice; use ax_errno::{AxError, AxResult, LinuxError}; @@ -108,6 +108,40 @@ struct RiscvFpRegs { fcsr: usize, } +/// Linux `user_regs_struct` for x86_64 (`arch/x86/include/uapi/asm/user.h`). +#[cfg(target_arch = "x86_64")] +#[repr(C)] +#[derive(Clone, Copy)] +struct X8664UserRegs { + r15: u64, + r14: u64, + r13: u64, + r12: u64, + rbp: u64, + rbx: u64, + r11: u64, + r10: u64, + r9: u64, + r8: u64, + rax: u64, + rcx: u64, + rdx: u64, + rsi: u64, + rdi: u64, + orig_rax: u64, + rip: u64, + cs: u64, + eflags: u64, + rsp: u64, + ss: u64, + fs_base: u64, + gs_base: u64, + ds: u64, + es: u64, + fs: u64, + gs: u64, +} + pub fn sys_ptrace(request: u32, pid: usize, addr: usize, data: usize) -> AxResult { info!("sys_ptrace <= request: {request}, pid: {pid}, addr: {addr:#x}, data: {data:#x}"); @@ -268,7 +302,7 @@ fn ptrace_getsiginfo(pid: usize, data: usize) -> AxResult { .ptrace_stop_siginfo() .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; - #[cfg(target_arch = "riscv64")] + #[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] { let bytes = unsafe { slice::from_raw_parts( @@ -280,14 +314,14 @@ fn ptrace_getsiginfo(pid: usize, data: usize) -> AxResult { Ok(0) } - #[cfg(not(target_arch = "riscv64"))] + #[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] { let _ = (data, siginfo); Err(AxError::Unsupported) } } -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] fn ptrace_setsiginfo(pid: usize, data: usize) -> AxResult { if data == 0 { return Err(AxError::InvalidInput); @@ -301,7 +335,7 @@ fn ptrace_setsiginfo(pid: usize, data: usize) -> AxResult { Ok(0) } -#[cfg(not(target_arch = "riscv64"))] +#[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] fn ptrace_setsiginfo(pid: usize, data: usize) -> AxResult { let _ = (pid, data); Err(AxError::Unsupported) @@ -341,7 +375,23 @@ fn ptrace_getregs(pid: usize, data: usize) -> AxResult { Ok(0) } -#[cfg(not(target_arch = "riscv64"))] +#[cfg(target_arch = "x86_64")] +fn ptrace_getregs(pid: usize, data: usize) -> AxResult { + if data == 0 { + return Err(AxError::InvalidInput); + } + let regs = ptrace_read_stopped_user_regs_x86_64(pid)?; + let bytes = unsafe { + slice::from_raw_parts( + (®s as *const X8664UserRegs).cast::(), + size_of::(), + ) + }; + vm_write_slice(data as *mut u8, bytes)?; + Ok(0) +} + +#[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] fn ptrace_getregs(pid: usize, data: usize) -> AxResult { let _ = (pid, data); Err(AxError::Unsupported) @@ -356,7 +406,16 @@ fn ptrace_setregs(pid: usize, data: usize) -> AxResult { ptrace_write_stopped_user_regs(pid, regs) } -#[cfg(not(target_arch = "riscv64"))] +#[cfg(target_arch = "x86_64")] +fn ptrace_setregs(pid: usize, data: usize) -> AxResult { + if data == 0 { + return Err(AxError::InvalidInput); + } + let regs = ptrace_read_user_regs_x86_64_from_user(data)?; + ptrace_write_stopped_user_regs_x86_64(pid, regs) +} + +#[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] fn ptrace_setregs(pid: usize, data: usize) -> AxResult { let _ = (pid, data); Err(AxError::Unsupported) @@ -435,48 +494,80 @@ fn ptrace_interrupt(pid: usize) -> AxResult { Ok(0) } -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] fn ptrace_getregset_prstatus(pid: usize, data: usize) -> AxResult { if data == 0 { return Err(AxError::InvalidInput); } + #[cfg(target_arch = "riscv64")] let regs = ptrace_read_stopped_user_regs(pid)?; - let mut iov = (data as *const IoVec).vm_read()?; - if iov.iov_len < 0 { - return Err(AxError::InvalidInput); - } - - let bytes = unsafe { + #[cfg(target_arch = "riscv64")] + let reg_bytes = unsafe { slice::from_raw_parts( (®s as *const RiscvUserRegs).cast::(), size_of::(), ) }; - let copy_len = (iov.iov_len as usize).min(bytes.len()); - vm_write_slice(iov.iov_base, &bytes[..copy_len])?; + #[cfg(target_arch = "x86_64")] + let regs = ptrace_read_stopped_user_regs_x86_64(pid)?; + #[cfg(target_arch = "x86_64")] + let reg_bytes = unsafe { + slice::from_raw_parts( + (®s as *const X8664UserRegs).cast::(), + size_of::(), + ) + }; + + let mut iov = (data as *const IoVec).vm_read()?; + if iov.iov_len < 0 { + return Err(AxError::InvalidInput); + } + + let copy_len = (iov.iov_len as usize).min(reg_bytes.len()); + vm_write_slice(iov.iov_base, ®_bytes[..copy_len])?; iov.iov_len = copy_len as isize; (data as *mut IoVec).vm_write(iov)?; Ok(0) } -#[cfg(not(target_arch = "riscv64"))] +#[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] fn ptrace_getregset_prstatus(pid: usize, data: usize) -> AxResult { let _ = (pid, data); Err(AxError::Unsupported) } -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] fn ptrace_setregset_prstatus(pid: usize, data: usize) -> AxResult { if data == 0 { return Err(AxError::InvalidInput); } + + #[cfg(target_arch = "riscv64")] + let reg_size = size_of::() as isize; + #[cfg(target_arch = "x86_64")] + let reg_size = size_of::() as isize; + let iov = (data as *const IoVec).vm_read()?; - if iov.iov_len < size_of::() as isize { + if iov.iov_len < reg_size { return Err(AxError::InvalidInput); } - let regs = ptrace_read_user_regs(iov.iov_base as usize)?; - ptrace_write_stopped_user_regs(pid, regs) + #[cfg(target_arch = "riscv64")] + { + let regs = ptrace_read_user_regs(iov.iov_base as usize)?; + ptrace_write_stopped_user_regs(pid, regs) + } + #[cfg(target_arch = "x86_64")] + { + let regs = ptrace_read_user_regs_x86_64_from_user(iov.iov_base as usize)?; + ptrace_write_stopped_user_regs_x86_64(pid, regs) + } +} + +#[cfg(not(any(target_arch = "riscv64", target_arch = "x86_64")))] +fn ptrace_setregset_prstatus(pid: usize, data: usize) -> AxResult { + let _ = (pid, data); + Err(AxError::Unsupported) } #[cfg(target_arch = "riscv64")] @@ -514,10 +605,39 @@ fn ptrace_write_stopped_user_regs(pid: usize, regs: RiscvUserRegs) -> AxResult AxResult { - let _ = (pid, data); - Err(AxError::Unsupported) +#[cfg(target_arch = "x86_64")] +fn ptrace_read_stopped_user_regs_x86_64(pid: usize) -> AxResult { + let tracee = ptrace_stopped_tracee(pid)?; + let uctx = tracee + .ptrace_stop_user_context() + .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; + Ok(X8664UserRegs::from(&uctx)) +} + +#[cfg(target_arch = "x86_64")] +fn ptrace_read_user_regs_x86_64_from_user(data: usize) -> AxResult { + let mut regs = MaybeUninit::::uninit(); + let bytes = unsafe { + slice::from_raw_parts_mut( + regs.as_mut_ptr().cast::>(), + size_of::(), + ) + }; + starry_vm::vm_read_slice(data as *const u8, bytes)?; + Ok(unsafe { regs.assume_init() }) +} + +#[cfg(target_arch = "x86_64")] +fn ptrace_write_stopped_user_regs_x86_64(pid: usize, regs: X8664UserRegs) -> AxResult { + let tracee = ptrace_stopped_tracee(pid)?; + let mut uctx = tracee + .ptrace_stop_user_context() + .ok_or_else(|| AxError::from(LinuxError::ESRCH))?; + regs.write_to(&mut uctx); + if !tracee.set_ptrace_stop_user_context(uctx) { + return Err(AxError::from(LinuxError::ESRCH)); + } + Ok(0) } #[cfg(target_arch = "riscv64")] @@ -580,7 +700,7 @@ fn ptrace_read_user_fpregs(data: usize) -> AxResult { Ok(unsafe { regs.assume_init() }) } -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", target_arch = "x86_64"))] fn ptrace_read_user_siginfo(data: usize) -> AxResult { let mut siginfo = MaybeUninit::::uninit(); let bytes = unsafe { @@ -593,7 +713,7 @@ fn ptrace_read_user_siginfo(data: usize) -> AxResult AxResult { let signo = unsafe { siginfo.__bindgen_anon_1.__bindgen_anon_1.si_signo }; Signo::from_repr(signo as u8).ok_or(AxError::InvalidInput) @@ -822,6 +942,18 @@ fn ptrace_stopped_tracee(pid: usize) -> AxResult> { Ok(tracee) } +#[cfg(target_arch = "x86_64")] +pub fn ptrace_setup_singlestep( + _tracee: &ProcessData, + uctx: &mut ax_runtime::hal::cpu::uspace::UserContext, +) { + // Set Trap Flag (TF, bit 8) in RFLAGS. + // The CPU will generate a #DB debug exception after executing one + // instruction and will automatically clear TF before pushing RFLAGS + // onto the exception stack frame (Intel SDM Vol 3A §17.3.2). + uctx.rflags |= 1 << 8; +} + #[cfg(target_arch = "riscv64")] pub fn ptrace_setup_singlestep( tracee: &ProcessData, @@ -1226,3 +1358,66 @@ impl RiscvUserRegs { r.t6 = self.t6; } } + +#[cfg(target_arch = "x86_64")] +impl From<&ax_runtime::hal::cpu::uspace::UserContext> for X8664UserRegs { + fn from(uctx: &ax_runtime::hal::cpu::uspace::UserContext) -> Self { + Self { + r15: uctx.r15, + r14: uctx.r14, + r13: uctx.r13, + r12: uctx.r12, + rbp: uctx.rbp, + rbx: uctx.rbx, + r11: uctx.r11, + r10: uctx.r10, + r9: uctx.r9, + r8: uctx.r8, + rax: uctx.rax, + rcx: uctx.rcx, + rdx: uctx.rdx, + rsi: uctx.rsi, + rdi: uctx.rdi, + orig_rax: u64::MAX, + rip: uctx.rip, + cs: uctx.cs, + eflags: uctx.rflags, + rsp: uctx.rsp, + ss: uctx.ss, + fs_base: uctx.fs_base, + gs_base: uctx.gs_base, + ds: 0, + es: 0, + fs: 0, + gs: 0, + } + } +} + +#[cfg(target_arch = "x86_64")] +impl X8664UserRegs { + fn write_to(&self, uctx: &mut ax_runtime::hal::cpu::uspace::UserContext) { + uctx.r15 = self.r15; + uctx.r14 = self.r14; + uctx.r13 = self.r13; + uctx.r12 = self.r12; + uctx.rbp = self.rbp; + uctx.rbx = self.rbx; + uctx.r11 = self.r11; + uctx.r10 = self.r10; + uctx.r9 = self.r9; + uctx.r8 = self.r8; + uctx.rax = self.rax; + uctx.rcx = self.rcx; + uctx.rdx = self.rdx; + uctx.rsi = self.rsi; + uctx.rdi = self.rdi; + uctx.rip = self.rip; + uctx.cs = self.cs; + uctx.rflags = self.eflags; + uctx.rsp = self.rsp; + uctx.ss = self.ss; + uctx.fs_base = self.fs_base; + uctx.gs_base = self.gs_base; + } +} diff --git a/os/StarryOS/kernel/src/task/user.rs b/os/StarryOS/kernel/src/task/user.rs index 423ce48ad4..92e5b864c5 100644 --- a/os/StarryOS/kernel/src/task/user.rs +++ b/os/StarryOS/kernel/src/task/user.rs @@ -34,6 +34,8 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> { #[cfg(target_arch = "riscv64")] crate::syscall::ptrace_setup_singlestep(&thr.proc_data, &mut uctx); + #[cfg(target_arch = "x86_64")] + crate::syscall::ptrace_setup_singlestep(&thr.proc_data, &mut uctx); } let reason = uctx.run(); @@ -123,6 +125,27 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> break 'exc; } } + // On x86_64, PTRACE_SINGLESTEP sets TF in RFLAGS; + // the resulting #DB exception arrives here. + // ExceptionKind::Debug and uctx.rflags only exist on + // x86_64, so this whole block is arch-gated. + #[cfg(target_arch = "x86_64")] + if matches!(kind, ExceptionKind::Debug) + && (thr.proc_data.is_ptrace_traceme() + || thr.proc_data.is_ptrace_attached()) + { + // Clear TF (bit 8) in the saved RFLAGS. The Intel + // SDM (Vol 3A §17.3.2) states the CPU clears TF + // when delivering a TF-induced #DB, but QEMU may + // not always honour this. Clearing explicitly + // prevents an unwanted extra single-step on resume. + uctx.rflags &= !(1u64 << 8); + if let Some(_resume_sig) = + ptrace_stop_current(thr, Signo::SIGTRAP, &mut uctx) + { + break 'exc; + } + } warn!( "user exception: ip={:#x}, fault_addr={:#x}, kind={:?}, esr={:#x}, \ ec={:#x}, iss={:#x}, info={:?}", diff --git a/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/CMakeLists.txt new file mode 100644 index 0000000000..3cac231efc --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.20) +project(test-gdb-native-batch C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-gdb-native-batch-target src/main.c) +target_compile_options(test-gdb-native-batch-target PRIVATE -Wall -Wextra -Werror -O0 -g) + +install(TARGETS test-gdb-native-batch-target RUNTIME DESTINATION usr/bin) +install(FILES gdb-native-batch.gdb DESTINATION usr/bin) +install(PROGRAMS run-gdb-native-batch.sh DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/gdb-native-batch.gdb b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/gdb-native-batch.gdb new file mode 100644 index 0000000000..57dcfb28e9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/gdb-native-batch.gdb @@ -0,0 +1,10 @@ +set pagination off +set confirm off +set debuginfod enabled off +break native_marker +run +bt +info registers +continue +echo GDB_NATIVE_BATCH_DONE\n +quit diff --git a/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/run-gdb-native-batch.sh b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/run-gdb-native-batch.sh new file mode 100644 index 0000000000..33124ace86 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/run-gdb-native-batch.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +/usr/bin/gdb -q -batch -x /usr/bin/gdb-native-batch.gdb /usr/bin/test-gdb-native-batch-target diff --git a/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/src/main.c new file mode 100644 index 0000000000..5f0806e1bf --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/c/src/main.c @@ -0,0 +1,13 @@ +#include + +__attribute__((noinline)) static int native_marker(int value) +{ + return value + 1; +} + +int main(void) +{ + int value = native_marker(41); + printf("gdb-native-batch-target value=%d\n", value); + return value == 42 ? 0 : 1; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/qemu-x86_64.toml new file mode 100644 index 0000000000..dc15bc830f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-gdb-native-batch/qemu-x86_64.toml @@ -0,0 +1,23 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "apk add gdb && /usr/bin/run-gdb-native-batch.sh" +success_regex = ["GDB_NATIVE_BATCH_DONE"] +fail_regex = [ + '(?i)\\bpanic(?:ked)?\\b', + '(?m)FAIL', + 'gdb: not found', + 'Python Exception', + 'Python initialization failed', + 'Cannot insert breakpoint', + 'ptrace: Function not implemented', + 'No stack', +] +timeout = 300 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-exec-stop/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-exec-stop/qemu-x86_64.toml new file mode 100644 index 0000000000..517bdbf7e2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-exec-stop/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-exec-stop" +success_regex = ["DONE: 3 pass, 0 fail"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/CMakeLists.txt new file mode 100644 index 0000000000..52983e2687 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ptrace-x86-breakpoint-reinsert C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-ptrace-x86-breakpoint-reinsert src/main.c) +target_compile_options(test-ptrace-x86-breakpoint-reinsert PRIVATE -Wall -Wextra -Werror -O0 -g) + +install(TARGETS test-ptrace-x86-breakpoint-reinsert RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/src/main.c new file mode 100644 index 0000000000..9b560f5cda --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/c/src/main.c @@ -0,0 +1,264 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_SETREGSET +#define PTRACE_SETREGSET 0x4205 +#endif +#ifndef PTRACE_TRACEME +#define PTRACE_TRACEME 0 +#endif +#ifndef PTRACE_SINGLESTEP +#define PTRACE_SINGLESTEP 9 +#endif +#ifndef PTRACE_CONT +#define PTRACE_CONT 7 +#endif +#ifndef PTRACE_PEEKDATA +#define PTRACE_PEEKDATA 2 +#endif +#ifndef PTRACE_POKEDATA +#define PTRACE_POKEDATA 5 +#endif +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif + +struct x86_64_user_regs { + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t rbp; + uint64_t rbx; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rsi; + uint64_t rdi; + uint64_t orig_rax; + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + uint64_t fs_base; + uint64_t gs_base; + uint64_t ds; + uint64_t es; + uint64_t fs; + uint64_t gs; +}; + +__attribute__((noinline)) static int native_marker(int value) +{ + return value + 1; +} + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static long ptrace_call(int request, pid_t pid, void *addr, void *data) +{ +#ifdef __APPLE__ + return ptrace(request, pid, (caddr_t)addr, (int)(intptr_t)data); +#else + return ptrace(request, pid, addr, data); +#endif +} + +static int wait_stopped(pid_t pid, int *status, int expected_sig) +{ + if (waitpid(pid, status, WUNTRACED) != pid) { + return fail("waitpid"); + } + if (!WIFSTOPPED(*status) || WSTOPSIG(*status) != expected_sig) { + printf("FAIL: expected stop signal %d, status=%#x\n", expected_sig, *status); + return 1; + } + return 0; +} + +static int getregset(pid_t pid, struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_GETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + if ((size_t)iov.iov_len != sizeof(*regs)) { + errno = EIO; + return -1; + } + return 0; +} + +static int setregset(pid_t pid, const struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = (void *)regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_SETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + return 0; +} + +static int test_breakpoint_reinsert(void) +{ + printf("test 1: x86_64 breakpoint restore + singlestep + reinsert\n"); + + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + + if (pid == 0) { + if (ptrace_call(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(100); + } + raise(SIGSTOP); + int value = native_marker(40); + value = native_marker(value); + _exit(value == 42 ? 42 : 1); + } + + int status = 0; + if (wait_stopped(pid, &status, SIGSTOP) != 0) { + return 1; + } + printf(" ok: child stopped with SIGSTOP\n"); + + uintptr_t bp_addr = (uintptr_t)native_marker; + errno = 0; + long orig_word = ptrace_call(PTRACE_PEEKDATA, pid, (void *)bp_addr, NULL); + if (orig_word == -1 && errno != 0) { + return fail("PTRACE_PEEKDATA original instruction"); + } + unsigned long bp_word = ((unsigned long)orig_word & ~0xffUL) | 0xccUL; + if (ptrace_call(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)bp_word) != 0) { + return fail("PTRACE_POKEDATA install breakpoint"); + } + printf(" ok: installed int3 at native_marker\n"); + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT first call"); + } + if (wait_stopped(pid, &status, SIGTRAP) != 0) { + return 1; + } + printf(" ok: first breakpoint hit\n"); + + struct x86_64_user_regs regs; + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset at first breakpoint"); + } + if (regs.rip != bp_addr + 1 && regs.rip != bp_addr) { + printf("FAIL: RIP=%#llx not at breakpoint %#llx\n", + (unsigned long long)regs.rip, (unsigned long long)bp_addr); + return 1; + } + + if (ptrace_call(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)orig_word) != 0) { + return fail("PTRACE_POKEDATA restore original instruction"); + } + regs.rip = bp_addr; + if (setregset(pid, ®s) != 0) { + return fail("setregset rewind RIP to breakpoint"); + } + printf(" ok: restored original byte and rewound RIP\n"); + + if (ptrace_call(PTRACE_SINGLESTEP, pid, NULL, NULL) != 0) { + return fail("PTRACE_SINGLESTEP over original instruction"); + } + if (wait_stopped(pid, &status, SIGTRAP) != 0) { + return 1; + } + printf(" ok: single-step completed original instruction\n"); + + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset after singlestep"); + } + if (regs.rip == bp_addr) { + printf("FAIL: RIP did not advance after single-step: %#llx\n", + (unsigned long long)regs.rip); + return 1; + } + + if (ptrace_call(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)bp_word) != 0) { + return fail("PTRACE_POKEDATA reinsert breakpoint"); + } + printf(" ok: reinserted breakpoint at native_marker\n"); + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT second call"); + } + if (wait_stopped(pid, &status, SIGTRAP) != 0) { + return 1; + } + printf(" ok: second breakpoint hit after reinsertion\n"); + + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset at second breakpoint"); + } + if (regs.rip != bp_addr + 1 && regs.rip != bp_addr) { + printf("FAIL: second-hit RIP=%#llx not at breakpoint %#llx\n", + (unsigned long long)regs.rip, (unsigned long long)bp_addr); + return 1; + } + + if (ptrace_call(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)orig_word) != 0) { + return fail("PTRACE_POKEDATA restore after second hit"); + } + regs.rip = bp_addr; + if (setregset(pid, ®s) != 0) { + return fail("setregset rewind RIP after second hit"); + } + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT final exit"); + } + if (waitpid(pid, &status, 0) != pid) { + return fail("waitpid final exit"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 42) { + printf("FAIL: expected exit 42, status=%#x\n", status); + return 1; + } + printf(" ok: child exited with 42 after breakpoint reinsertion flow\n"); + return 0; +} + +int main(void) +{ + int pass = 0; + int fail_count = 0; + + if (test_breakpoint_reinsert() == 0) { + pass++; + } else { + fail_count++; + } + + printf("DONE: %d pass, %d fail\n", pass, fail_count); + return fail_count > 0 ? 1 : 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/qemu-x86_64.toml new file mode 100644 index 0000000000..3320ce3293 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint-reinsert/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-x86-breakpoint-reinsert" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/CMakeLists.txt new file mode 100644 index 0000000000..4b8c403ecd --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.5) +project(test-ptrace-x86-breakpoint C) +set(CMAKE_C_STANDARD 11) +add_executable(test-ptrace-x86-breakpoint src/main.c) +install(TARGETS test-ptrace-x86-breakpoint RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/src/main.c new file mode 100644 index 0000000000..39aad2ad8e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/c/src/main.c @@ -0,0 +1,389 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_SETREGSET +#define PTRACE_SETREGSET 0x4205 +#endif +#ifndef PTRACE_SINGLESTEP +#define PTRACE_SINGLESTEP 9 +#endif +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif + +struct x86_64_user_regs { + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t rbp; + uint64_t rbx; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rsi; + uint64_t rdi; + uint64_t orig_rax; + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + uint64_t fs_base; + uint64_t gs_base; + uint64_t ds; + uint64_t es; + uint64_t fs; + uint64_t gs; +}; + +__attribute__((noinline)) static int target_function(void) +{ + return 42; +} + + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static int getregset(pid_t pid, struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace(PTRACE_GETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + if ((size_t)iov.iov_len != sizeof(*regs)) { + errno = EIO; + return -1; + } + return 0; +} + +static int setregset(pid_t pid, const struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = (void *)regs, .iov_len = sizeof(*regs)}; + if (ptrace(PTRACE_SETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + return 0; +} + +/* Test 1: minimum closed-loop software breakpoint. + * + * tracer writes 0xCC; child hits breakpoint; tracer restores original + * byte and continues from the pre-int3 address. This validates the + * basic breakpoint stop and register read-back but does NOT exercise + * PTRACE_SINGLESTEP. */ +static int test_breakpoint(void) +{ + printf("test 1: x86_64 software breakpoint (int3)\n"); + + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + + if (pid == 0) { + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(100); + } + /* Trigger initial ptrace-stop via int3 instead of raise(SIGSTOP). + * The #BP exception path preserves the CPU exception frame with + * valid segment selectors (CS/SS) and stack pointer. */ + __asm__ volatile ("int3" ::: "memory"); + target_function(); + _exit(42); + } + + int status = 0; + if (waitpid(pid, &status, WUNTRACED) != pid) { + return fail("waitpid initial stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { + printf(" note: unexpected initial stop signal=%d status=%#x\n", + WSTOPSIG(status), status); + } + printf(" ok: child stopped (signal=%d)\n", WSTOPSIG(status)); + + struct x86_64_user_regs regs; + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset at initial stop"); + } + printf(" ok: initial RIP=%#llx RSP=%#llx\n", + (unsigned long long)regs.rip, (unsigned long long)regs.rsp); + + if (regs.rsp == 0 || regs.rsp == 1) { + printf("FAIL: bogus RSP=%#llx (expected valid stack pointer)\n", + (unsigned long long)regs.rsp); + return 1; + } + + /* x86 int3 is 1 byte (0xCC). CPU pushes RIP of next instruction, so + * the saved RIP in the exception frame points to int3_addr + 1. + * StarryOS does not adjust RIP backward for ptrace breakpoint stops, + * so we observe RIP = int3 + 1 here. Save the pre-int3 RIP as the + * "continue point" for later. */ + uintptr_t continue_rip = regs.rip; + + uintptr_t bp_addr = (uintptr_t)target_function; + printf(" target_function at %p\n", (void *)bp_addr); + + errno = 0; + long orig_word = ptrace(PTRACE_PEEKDATA, pid, (void *)bp_addr, NULL); + if (orig_word == -1 && errno != 0) { + return fail("PEEKDATA target_function"); + } + + unsigned long bp_word = ((unsigned long)orig_word & ~0xffUL) | 0xccUL; + if (ptrace(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)bp_word) != 0) { + return fail("POKEDATA write int3"); + } + printf(" ok: wrote int3 (0xCC) at target_function\n"); + + regs.rip = bp_addr; + if (setregset(pid, ®s) != 0) { + return fail("setregset redirect RIP to target_function"); + } + printf(" ok: redirected RIP=%#llx -> %#llx\n", + (unsigned long long)continue_rip, (unsigned long long)bp_addr); + + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT to breakpoint"); + } + + if (waitpid(pid, &status, WUNTRACED) != pid) { + return fail("waitpid breakpoint"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected SIGTRAP after breakpoint, status=%#x\n", status); + return 1; + } + printf(" ok: child stopped with SIGTRAP\n"); + + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset at breakpoint"); + } + + /* After int3, RIP = bp_addr + 1. Verify it's near the breakpoint. */ + if (regs.rip != bp_addr && regs.rip != bp_addr + 1) { + printf("FAIL: RIP=%#llx not near breakpoint addr %#llx\n", + (unsigned long long)regs.rip, (unsigned long long)bp_addr); + return 1; + } + printf(" ok: RIP=%#llx at breakpoint (offset +%lld)\n", + (unsigned long long)regs.rip, (unsigned long long)bp_addr, + (long long)(regs.rip - bp_addr)); + + if (ptrace(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)orig_word) != 0) { + return fail("POKEDATA restore original byte"); + } + printf(" ok: restored original instruction byte\n"); + + /* Rewind RIP to the original continue point (after the initial int3). + * From there the child will call target_function() normally via the + * call instruction, then _exit(42). */ + regs.rip = continue_rip; + if (setregset(pid, ®s) != 0) { + return fail("setregset rewind RIP"); + } + printf(" ok: rewound RIP to continue point %#llx\n", + (unsigned long long)continue_rip); + + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT after breakpoint restore"); + } + + if (waitpid(pid, &status, 0) != pid) { + return fail("waitpid exit after restore"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 42) { + printf("FAIL: expected exit code 42, status=%#x\n", status); + return 1; + } + printf(" ok: child exited with 42\n"); + + return 0; +} + +/* Test 2: x86_64 PTRACE_SINGLESTEP validation. + * + * Uses the initial `int3` in the child as the breakpoint target: + * + * 1. child hits initial int3, stops with SIGTRAP + * 2. tracer replaces int3 (0xCC) with NOP (0x90) at that address + * 3. tracer rewinds RIP to the int3 address + * 4. tracer issues PTRACE_SINGLESTEP + * 5. child executes the NOP and stops with SIGTRAP (#DB) + * 6. tracer verifies RIP advanced past the int3 site + * 7. tracer restores int3, then CONT + * 8. child continues to completion (target_function → _exit(42)) + * + * This validates the x86_64 Trap-flag-based single-step mechanism. */ +static int test_breakpoint_singlestep_restore(void) +{ + printf("test 2: x86_64 PTRACE_SINGLESTEP\n"); + + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + + if (pid == 0) { + if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(100); + } + /* Initial stop via int3 — also serves as the breakpoint + * the tracer will single-step over. */ + __asm__ volatile ( + ".global test_bp_addr\n" + "test_bp_addr:\n" + " int3\n" + ::: "memory" + ); + target_function(); + _exit(42); + } + + int status = 0; + if (waitpid(pid, &status, WUNTRACED) != pid) { + return fail("waitpid initial int3 stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected SIGTRAP, status=%#x\n", status); + return 1; + } + printf(" ok: initial int3 SIGTRAP stop\n"); + + struct x86_64_user_regs regs; + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset at initial stop"); + } + + extern char test_bp_addr; + uintptr_t bp_addr = (uintptr_t)&test_bp_addr; + /* int3 is 1 byte; after #BP the saved RIP = bp_addr + 1. */ + if (regs.rip != bp_addr + 1) { + printf("FAIL: expected RIP=%#llx (bp+1), got %#llx\n", + (unsigned long long)(bp_addr + 1), (unsigned long long)regs.rip); + return 1; + } + printf(" ok: RIP=%#llx (bp+1)\n", (unsigned long long)regs.rip); + + /* Step 1: replace int3 (0xCC) with NOP (0x90) at the breakpoint site, + * preserving the surrounding bytes (POKEDATA is word-sized). */ + errno = 0; + long orig_word = ptrace(PTRACE_PEEKDATA, pid, (void *)bp_addr, NULL); + if (orig_word == -1 && errno != 0) { + return fail("PEEKDATA at bp"); + } + unsigned long nop_word = ((unsigned long)orig_word & ~0xffUL) | 0x90UL; + if (ptrace(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)nop_word) != 0) { + return fail("POKEDATA write NOP"); + } + printf(" ok: replaced int3 with NOP at %#llx\n", (unsigned long long)bp_addr); + + /* Step 2: rewind RIP to the int3/NOP address. */ + regs.rip = bp_addr; + if (setregset(pid, ®s) != 0) { + return fail("setregset rewind RIP"); + } + + /* Step 3: PTRACE_SINGLESTEP — the CPU executes the NOP (1 byte) + * and then fires #DB because TF was set in RFLAGS. */ + if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) != 0) { + return fail("PTRACE_SINGLESTEP"); + } + printf(" ok: PTRACE_SINGLESTEP issued\n"); + + /* Step 4: wait for the #DB stop. */ + if (waitpid(pid, &status, WUNTRACED) != pid) { + return fail("waitpid singlestep stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) { + printf("FAIL: expected SIGTRAP after singlestep, status=%#x signal=%d\n", + status, WSTOPSIG(status)); + return 1; + } + printf(" ok: singlestep SIGTRAP stop\n"); + + /* Step 5: verify RIP advanced past the breakpoint (NOP is 1 byte). */ + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset after singlestep"); + } + if (regs.rip != bp_addr + 1) { + printf("FAIL: RIP=%#llx, expected %#llx (bp+1)\n", + (unsigned long long)regs.rip, (unsigned long long)(bp_addr + 1)); + return 1; + } + printf(" ok: RIP=%#llx advanced past breakpoint (offset +1)\n", + (unsigned long long)regs.rip); + + /* Step 6: restore int3 (for correctness; doesn't affect the test + * since execution has already passed this point). */ + unsigned long cc_word = ((unsigned long)orig_word & ~0xffUL) | 0xccUL; + if (ptrace(PTRACE_POKEDATA, pid, (void *)bp_addr, (void *)cc_word) != 0) { + return fail("POKEDATA re-insert int3"); + } + + /* Step 7: CONT — child continues through target_function → _exit(42). */ + if (ptrace(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("PTRACE_CONT"); + } + + /* Step 8: child exits normally. */ + if (waitpid(pid, &status, 0) != pid) { + return fail("waitpid exit"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 42) { + printf("FAIL: expected exit code 42, status=%#x\n", status); + return 1; + } + printf(" ok: child exited with 42\n"); + + return 0; +} + +int main(void) +{ + int pass = 0; + int fail_count = 0; + + if (test_breakpoint() == 0) { + pass++; + } else { + fail_count++; + } + + if (test_breakpoint_singlestep_restore() == 0) { + pass++; + } else { + fail_count++; + } + + printf("DONE: %d pass, %d fail\n", pass, fail_count); + return fail_count > 0 ? 1 : 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/qemu-x86_64.toml new file mode 100644 index 0000000000..90b4ff17d2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-breakpoint/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-x86-breakpoint" +success_regex = ["DONE: 2 pass, 0 fail"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/CMakeLists.txt new file mode 100644 index 0000000000..365b18d693 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ptrace-x86-regs C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-ptrace-x86-regs src/main.c) +target_compile_options(test-ptrace-x86-regs PRIVATE + -Wall + -Wextra + -Werror +) + +install(TARGETS test-ptrace-x86-regs RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/src/main.c new file mode 100644 index 0000000000..a35cb84857 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/c/src/main.c @@ -0,0 +1,269 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PTRACE_GETREGS +#define PTRACE_GETREGS 12 +#endif +#ifndef PTRACE_TRACEME +#define PTRACE_TRACEME 0 +#endif +#ifndef PTRACE_SETREGS +#define PTRACE_SETREGS 13 +#endif +#ifndef PTRACE_CONT +#define PTRACE_CONT 7 +#endif +#ifndef PTRACE_GETSIGINFO +#define PTRACE_GETSIGINFO 0x4202 +#endif +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_SETREGSET +#define PTRACE_SETREGSET 0x4205 +#endif +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif + +struct x86_64_user_regs { + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t rbp; + uint64_t rbx; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rsi; + uint64_t rdi; + uint64_t orig_rax; + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + uint64_t fs_base; + uint64_t gs_base; + uint64_t ds; + uint64_t es; + uint64_t fs; + uint64_t gs; +}; + +static long ptrace_call(int request, pid_t pid, void *addr, void *data) +{ +#ifdef __APPLE__ + return ptrace(request, pid, (caddr_t)addr, (int)(intptr_t)data); +#else + return ptrace(request, pid, addr, data); +#endif +} + +static int fail_errno(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static int fail_msg(const char *msg) +{ + printf("FAIL: %s\n", msg); + return 1; +} + +static int getregs(pid_t pid, struct x86_64_user_regs *regs) +{ + if (ptrace_call(PTRACE_GETREGS, pid, NULL, regs) != 0) { + return -1; + } + return 0; +} + +static int setregs(pid_t pid, const struct x86_64_user_regs *regs) +{ + if (ptrace_call(PTRACE_SETREGS, pid, NULL, (void *)regs) != 0) { + return -1; + } + return 0; +} + +static int getregset(pid_t pid, struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &iov) != 0) { + return -1; + } + if (iov.iov_len != (long)sizeof(*regs)) { + errno = EIO; + return -1; + } + return 0; +} + +static int setregset(pid_t pid, const struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = (void *)regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_SETREGSET, pid, (void *)NT_PRSTATUS, &iov) != 0) { + return -1; + } + return 0; +} + +static int test_ptrace_x86_regs(void) +{ + printf("test 1: x86_64 ptrace regs/siginfo\n"); + + pid_t pid = fork(); + if (pid < 0) { + return fail_errno("fork"); + } + + if (pid == 0) { + if (ptrace_call(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(100); + } + raise(SIGSTOP); + _exit(0); + } + + int status = 0; + if (waitpid(pid, &status, WUNTRACED) != pid) { + return fail_errno("waitpid stop"); + } + if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { + printf("FAIL: expected SIGSTOP stop, status=%#x\n", status); + return 1; + } + printf(" ok: child stopped with SIGSTOP\n"); + + siginfo_t si; + memset(&si, 0, sizeof(si)); + if (ptrace_call(PTRACE_GETSIGINFO, pid, NULL, &si) != 0) { + return fail_errno("PTRACE_GETSIGINFO"); + } + if (si.si_signo != SIGSTOP) { + printf("FAIL: si_signo=%d, expected %d\n", si.si_signo, SIGSTOP); + return 1; + } + printf(" ok: GETSIGINFO reports SIGSTOP\n"); + + struct x86_64_user_regs regs_getregs; + memset(®s_getregs, 0, sizeof(regs_getregs)); + if (getregs(pid, ®s_getregs) != 0) { + return fail_errno("PTRACE_GETREGS"); + } + + struct x86_64_user_regs regs_getregset; + memset(®s_getregset, 0, sizeof(regs_getregset)); + if (getregset(pid, ®s_getregset) != 0) { + return fail_errno("PTRACE_GETREGSET"); + } + + if (regs_getregs.rip == 0 || regs_getregs.rsp == 0) { + return fail_msg("GETREGS returned zero RIP/RSP"); + } + if (regs_getregs.rip != regs_getregset.rip || + regs_getregs.rsp != regs_getregset.rsp || + regs_getregs.rax != regs_getregset.rax || + regs_getregs.r15 != regs_getregset.r15) { + printf("FAIL: GETREGS/GETREGSET mismatch: rip=%#llx/%#llx rsp=%#llx/%#llx rax=%#llx/%#llx r15=%#llx/%#llx\n", + (unsigned long long)regs_getregs.rip, + (unsigned long long)regs_getregset.rip, + (unsigned long long)regs_getregs.rsp, + (unsigned long long)regs_getregset.rsp, + (unsigned long long)regs_getregs.rax, + (unsigned long long)regs_getregset.rax, + (unsigned long long)regs_getregs.r15, + (unsigned long long)regs_getregset.r15); + return 1; + } + printf(" ok: GETREGS and GETREGSET agree on core registers\n"); + + struct x86_64_user_regs modified = regs_getregset; + modified.r15 ^= 0x13579bdfULL; + if (setregset(pid, &modified) != 0) { + return fail_errno("PTRACE_SETREGSET"); + } + + struct x86_64_user_regs check_after_setregset; + memset(&check_after_setregset, 0, sizeof(check_after_setregset)); + if (getregs(pid, &check_after_setregset) != 0) { + return fail_errno("GETREGS after SETREGSET"); + } + if (check_after_setregset.r15 != modified.r15) { + printf("FAIL: SETREGSET did not update r15 (%#llx != %#llx)\n", + (unsigned long long)check_after_setregset.r15, + (unsigned long long)modified.r15); + return 1; + } + printf(" ok: SETREGSET updated r15\n"); + + modified = check_after_setregset; + modified.r14 ^= 0x2468ace0ULL; + if (setregs(pid, &modified) != 0) { + return fail_errno("PTRACE_SETREGS"); + } + + struct x86_64_user_regs check_after_setregs; + memset(&check_after_setregs, 0, sizeof(check_after_setregs)); + if (getregset(pid, &check_after_setregs) != 0) { + return fail_errno("GETREGSET after SETREGS"); + } + if (check_after_setregs.r14 != modified.r14) { + printf("FAIL: SETREGS did not update r14 (%#llx != %#llx)\n", + (unsigned long long)check_after_setregs.r14, + (unsigned long long)modified.r14); + return 1; + } + printf(" ok: SETREGS updated r14\n"); + + if (setregs(pid, ®s_getregs) != 0) { + return fail_errno("restore original regs"); + } + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail_errno("PTRACE_CONT"); + } + + if (waitpid(pid, &status, 0) != pid) { + return fail_errno("waitpid exit"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + printf("FAIL: expected child exit 0, status=%#x\n", status); + return 1; + } + printf(" ok: child continued and exited cleanly\n"); + return 0; +} + +int main(void) +{ + int pass = 0; + int fail = 0; + + if (test_ptrace_x86_regs() == 0) { + pass++; + } else { + fail++; + } + + printf("DONE: %d pass, %d fail\n", pass, fail); + return fail == 0 ? 0 : 1; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/qemu-x86_64.toml new file mode 100644 index 0000000000..76573850a6 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-regs/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-x86-regs" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/CMakeLists.txt new file mode 100644 index 0000000000..cdfdc154b4 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) +project(test-ptrace-x86-singlestep C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-ptrace-x86-singlestep src/main.c) +target_compile_options(test-ptrace-x86-singlestep PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-ptrace-x86-singlestep RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/src/main.c new file mode 100644 index 0000000000..efa74b9aa2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/c/src/main.c @@ -0,0 +1,223 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PTRACE_GETREGSET +#define PTRACE_GETREGSET 0x4204 +#endif +#ifndef PTRACE_SETREGSET +#define PTRACE_SETREGSET 0x4205 +#endif +#ifndef PTRACE_TRACEME +#define PTRACE_TRACEME 0 +#endif +#ifndef PTRACE_SINGLESTEP +#define PTRACE_SINGLESTEP 9 +#endif +#ifndef PTRACE_CONT +#define PTRACE_CONT 7 +#endif +#ifndef NT_PRSTATUS +#define NT_PRSTATUS 1 +#endif + +struct x86_64_user_regs { + uint64_t r15; + uint64_t r14; + uint64_t r13; + uint64_t r12; + uint64_t rbp; + uint64_t rbx; + uint64_t r11; + uint64_t r10; + uint64_t r9; + uint64_t r8; + uint64_t rax; + uint64_t rcx; + uint64_t rdx; + uint64_t rsi; + uint64_t rdi; + uint64_t orig_rax; + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + uint64_t fs_base; + uint64_t gs_base; + uint64_t ds; + uint64_t es; + uint64_t fs; + uint64_t gs; +}; + +__attribute__((naked, noinline, aligned(1))) static void singlestep_target(void) +{ + __asm__ volatile( + "mov $123, %rax\n" + "int3\n" + "jmp singlestep_landing\n"); +} + +__attribute__((naked, noinline, aligned(1), used)) static void singlestep_landing(void) +{ + __asm__ volatile( + "mov $42, %rdi\n" + "mov $60, %rax\n" + "syscall\n"); +} + +static int fail(const char *msg) +{ + printf("FAIL: %s: errno=%d (%s)\n", msg, errno, strerror(errno)); + return 1; +} + +static long ptrace_call(int request, pid_t pid, void *addr, void *data) +{ +#ifdef __APPLE__ + return ptrace(request, pid, (caddr_t)addr, (int)(intptr_t)data); +#else + return ptrace(request, pid, addr, data); +#endif +} + +static int wait_stopped(pid_t pid, int *status, int expected_sig) +{ + if (waitpid(pid, status, WUNTRACED) != pid) { + return fail("waitpid"); + } + if (!WIFSTOPPED(*status) || WSTOPSIG(*status) != expected_sig) { + printf("FAIL: expected stop signal %d, status=%#x\n", expected_sig, *status); + return 1; + } + return 0; +} + +static int getregset(pid_t pid, struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_GETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + if ((size_t)iov.iov_len != sizeof(*regs)) { + errno = EIO; + return -1; + } + return 0; +} + +static int setregset(pid_t pid, const struct x86_64_user_regs *regs) +{ + struct iovec iov = {.iov_base = (void *)regs, .iov_len = sizeof(*regs)}; + if (ptrace_call(PTRACE_SETREGSET, pid, (void *)(long)NT_PRSTATUS, &iov) != 0) { + return -1; + } + return 0; +} + +static int test_singlestep(void) +{ + printf("test 1: x86_64 PTRACE_SINGLESTEP\n"); + + pid_t pid = fork(); + if (pid < 0) { + return fail("fork"); + } + + if (pid == 0) { + if (ptrace_call(PTRACE_TRACEME, 0, NULL, NULL) != 0) { + _exit(100); + } + raise(SIGSTOP); + _exit(99); + } + + int status = 0; + if (wait_stopped(pid, &status, SIGSTOP) != 0) { + return 1; + } + printf(" ok: child stopped with SIGSTOP\n"); + + struct x86_64_user_regs regs; + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset initial"); + } + + regs.rip = (uintptr_t)singlestep_target; + regs.rax = 0; + if (setregset(pid, ®s) != 0) { + return fail("setregset target rip"); + } + + if (ptrace_call(PTRACE_SINGLESTEP, pid, NULL, NULL) != 0) { + return fail("ptrace singlestep"); + } + if (wait_stopped(pid, &status, SIGTRAP) != 0) { + return 1; + } + printf(" ok: single-step trap delivered\n"); + + memset(®s, 0, sizeof(regs)); + if (getregset(pid, ®s) != 0) { + return fail("getregset after singlestep"); + } + if (regs.rax != 123) { + printf("FAIL: expected RAX=123 after single-step, got %#llx\n", + (unsigned long long)regs.rax); + return 1; + } + if (regs.rip == (uintptr_t)singlestep_target) { + printf("FAIL: RIP did not advance after single-step: %#llx\n", + (unsigned long long)regs.rip); + return 1; + } + printf(" ok: single-step executed one instruction (RAX=%#llx RIP=%#llx)\n", + (unsigned long long)regs.rax, (unsigned long long)regs.rip); + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("ptrace cont to int3"); + } + if (wait_stopped(pid, &status, SIGTRAP) != 0) { + return 1; + } + printf(" ok: child reached the following int3\n"); + + if (ptrace_call(PTRACE_CONT, pid, NULL, NULL) != 0) { + return fail("ptrace cont to exit"); + } + if (waitpid(pid, &status, 0) != pid) { + return fail("waitpid exit"); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 42) { + printf("FAIL: expected exit 42, status=%#x\n", status); + return 1; + } + printf(" ok: child exited with 42\n"); + return 0; +} + +int main(void) +{ + int pass = 0; + int fail_count = 0; + + if (test_singlestep() == 0) { + pass++; + } else { + fail_count++; + } + + printf("DONE: %d pass, %d fail\n", pass, fail_count); + return fail_count > 0 ? 1 : 0; +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/qemu-x86_64.toml new file mode 100644 index 0000000000..b14aaff8be --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-ptrace-x86-singlestep/qemu-x86_64.toml @@ -0,0 +1,14 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-ptrace-x86-singlestep" +success_regex = ["DONE: 1 pass, 0 fail"] +fail_regex = ['(?i)\\bpanic(?:ked)?\\b', '(?m)FAIL'] +timeout = 120