Files
raven/pkg/syscall/handler.go
T

3648 lines
98 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build guest_linux
package syscall
import (
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
"github.com/mirkobrombin/Raven/pkg/arch"
"github.com/mirkobrombin/Raven/pkg/mmu"
"github.com/mirkobrombin/Raven/pkg/signal"
"github.com/mirkobrombin/Raven/pkg/thread"
"github.com/mirkobrombin/Raven/pkg/vfs"
ravelog "github.com/mirkobrombin/Raven/pkg/log"
)
// Linux RISC-V syscall numbers (from asm-generic/unistd.h)
const (
SysGetcwd = 17
SysDup = 23
SysDup3 = 24
SysFcntl = 25
SysIoctl = 29
SysMkdirat = 34
SysUnlinkat = 35
SysSymlinkat = 36
SysLinkat = 37
SysFaccessat = 48
SysChdir = 49
SysFchmod = 52
SysFchmodat = 53
SysFchownat = 54
SysFchown = 55
SysOpenat = 56
SysClose = 57
SysPipe2 = 59
SysGetdents64 = 61
SysLseek = 62
SysRead = 63
SysWrite = 64
SysReadv = 65
SysWritev = 66
SysPread64 = 67
SysPwrite64 = 68
SysPselect6 = 72
SysPpoll = 73
sysPoll = 10025 // internal: x64 poll(fds, nfds, timeout_ms)
SysReadlinkat = 78
SysFstatat = 79
SysFstat = 80
SysFsync = 82
SysFdatasync = 83
SysFtruncate = 46
SysFallocate = 47
SysUtimensat = 88
SysExit = 93
SysExitGroup = 94
SysSetTidAddress = 96
SysFutex = 98
SysSetRobustList = 99
SysNanosleep = 101
SysGetitimer = 102
SysSetitimer = 103
SysClockGettime = 113
SysClockGetres = 114
SysClockNanosleep = 115
SysSyslog = 116
SysSchedSetaffinity = 122
SysSchedGetaffinity = 123
SysSchedYield = 124
SysKill = 129
SysTkill = 130
SysTgkill = 131
SysSigaltstack = 132
SysRtSigaction = 134
SysRtSigprocmask = 135
SysRtSigreturn = 139
SysTimes = 153
SysUname = 160
SysSync = 162
SysUmask = 166
SysPrctl = 167
SysGetrusage = 165
SysGetpid = 172
SysGetppid = 173
SysGetuid = 174
SysGeteuid = 175
SysGetgid = 176
SysGetegid = 177
SysGetresuid = 10095
SysGetresgid = 10096
SysMincore = 10097
SysGettid = 178
SysSysinfo = 179
SysSocket = 198
SysBind = 200
SysListen = 201
SysAccept = 202
SysConnect = 203
SysSendto = 206
SysRecvfrom = 207
SysSetsockopt = 208
SysGetsockopt = 209
SysShutdown = 210
SysSendmsg = 211
SysRecvmsg = 212
SysBrk = 214
SysMunmap = 215
SysMremap = 216
SysClone = 220
SysExecve = 221
SysMmap = 222
SysFadvise64 = 223
SysSwapon = 224
SysMprotect = 226
SysMsync = 227
SysMlock = 228
SysMunlock = 229
SysMadvise = 233
SysAccept4 = 242
SysWait4 = 260
SysPrlimit64 = 261
SysRenameat2 = 276
SysGetrandom = 278
SysMembarrier = 283
SysStatx = 291
SysRseq = 293
SysEpollCreate1 = 20
SysEpollCtl = 21
SysEpollPwait = 22
SysEventfd2 = 19
SysTimerfdCreate = 85
SysTimerfdSettime = 86
SysTimerfdGettime = 87
SysSocketpair = 199
SysGetsockname = 204
SysGetpeername = 205
SysMemfdCreate = 279
SysKcmp = 10091 // internal, mapped from x86-64 kcmp (SYS 253)
SysRiscvHwprobe = 258
SysArchPrctl = 259 // internal, mapped from x86-64 arch_prctl
SysSendfile = 262 // internal, mapped from x86-64 sendfile
SysClone3 = 10090 // internal, mapped from x86-64 clone3 (SYS 435)
// Legacy x64 syscalls that need arg remapping to their *at variants.
// These use high numbers to avoid collisions with RISC-V generic numbers.
sysX64Open = 10002
sysX64Stat = 10004
sysX64Lstat = 10006
sysX64Access = 10021
sysX64Rename = 10082
sysX64Mkdir = 10083
sysX64Unlink = 10087
sysX64Symlink = 10088
sysX64Readlink = 10089
)
// Handler holds the synthetic kernel state.
type Handler struct {
Mem *mmu.MMU
FS *vfs.VFS
ThreadM *thread.Manager
Signals *signal.SignalState
Umask uint32 // current umask
Trace bool // trace syscalls to stderr
Arch arch.Arch // guest architecture
debugX11 bool // trace all syscalls (for X11 debugging)
clearChildTidMu sync.Mutex
clearChildTid map[int]uint64 // TID → clear_child_tid address
// temporary debug: count syscalls by number
// vfork / posix_spawn subprocess support.
// When the guest calls clone(CLONE_VFORK) or clone3(CLONE_VFORK),
// we record a pending vfork state. The child goroutine runs until it
// calls execve (which launches a real host process) or exit, then
// unblocks the parent via vforkDone.
vforkMu sync.Mutex
vforkPending bool // true while a vfork child is running
vforkDone chan int // child sends guest PID after execve/exit
vforkSnapshot vfs.FDSnapshot // parent FD table saved before vfork child runs
// host child processes spawned via execve-in-vfork-child, keyed by guest PID.
childMu sync.Mutex
childProcs map[int]*os.Process // guest pid → host *os.Process
nextGPID int // monotonic guest PID counter (starts at 2)
}
// NewHandler creates a syscall handler wired to the given MMU and VFS.
func NewHandler(mem *mmu.MMU, fs *vfs.VFS) *Handler {
h := &Handler{
Mem: mem,
FS: fs,
Signals: signal.New(),
Umask: 0022,
childProcs: make(map[int]*os.Process),
nextGPID: 2,
}
return h
}
// Handle is the SyscallHandler callback for the CPU.
func (h *Handler) Handle(c arch.CPU) {
num := c.SyscallNum()
if c.GetArch() == arch.ArchAMD64 {
num = x64SyscallToRV(num)
}
args := c.SyscallArgs()
a0, a1, a2, a3, a4, a5 := args[0], args[1], args[2], args[3], args[4], args[5]
if h.debugX11 {
ravelog.Logf("[x11-trace] syscall %d (a0=0x%x a1=0x%x a2=0x%x)\n", num, a0, a1, a2)
}
if h.Trace {
ravelog.Logf("[trace] syscall %d (a0=0x%x a1=0x%x a2=0x%x a3=0x%x) instret=%d\n", num, a0, a1, a2, a3, c.GetInstret())
}
// DEBUG: heap integrity check before/after each syscall
var heapSigBefore uint64
if h.Trace {
heapSigBefore = h.heapSignature()
if heapSigBefore != 0 {
ravelog.Logf("[heap-sig] before syscall %d: 0x%x\n", num, heapSigBefore)
}
}
defer func() {
if h.Trace {
after := h.heapSignature()
if after != 0 {
ravelog.Logf("[heap-sig] after syscall %d: 0x%x\n", num, after)
}
if heapSigBefore != 0 && after != 0 && heapSigBefore != after {
ravelog.Logf("[HEAP-CORRUPT] syscall %d (a0=0x%x a1=0x%x a2=0x%x) changed heap sig 0x%x→0x%x instret=%d\n",
num, a0, a1, a2, heapSigBefore, after, c.GetInstret())
}
}
}()
switch num {
// --- I/O ---
case SysWrite:
h.sysWrite(c, a0, a1, a2)
case SysWritev:
h.sysWritev(c, a0, a1, a2)
case SysReadv:
h.sysReadv(c, a0, a1, a2)
case SysRead:
h.sysRead(c, a0, a1, a2)
case SysPread64:
h.sysPread64(c, a0, a1, a2, a3)
case SysPwrite64:
h.sysPwrite64(c, a0, a1, a2, a3)
// --- Process lifecycle ---
case SysExit, SysExitGroup:
c.Stop()
c.SetSyscallReturn(a0)
// --- Memory management ---
case SysBrk:
result := h.Mem.Brk(a0)
if h.Trace {
ravelog.Logf("[brk] req=0x%x → 0x%x\n", a0, result)
if a0 != 0 && result == a0 {
// Dump first 128 bytes of the heap region
heapBase := h.Mem.GetBrkBase()
if data, err := h.Mem.ReadBytes(heapBase, 128); err == nil {
ravelog.Logf("[brk] heap@0x%x: %x\n", heapBase, data)
}
}
}
c.SetSyscallReturn(result)
case SysMmap:
h.sysMmap(c)
case SysMunmap:
if h.ThreadM != nil && h.ThreadM.IsThreadStack(a0, a1) {
// The parent is trying to munmap a live thread's stack (common in
// Rust/glibc clone3 patterns where the parent frees the stack
// allocation immediately after clone3). Skip the unmap so the child
// goroutine can execute; the stack is unregistered when the thread exits.
c.SetSyscallReturn(0)
break
}
h.Mem.Munmap(a0, a1)
c.SetSyscallReturn(0)
case SysMprotect:
h.sysMprotect(c, a0, a1, a2)
case SysMremap:
h.sysMremap(c, a0, a1, a2, a3, a4)
case SysMadvise:
h.sysMadvise(c, a0, a1, a2)
case SysMsync, SysMlock, SysMunlock:
c.SetSyscallReturn(0)
// --- Filesystem ---
case SysOpenat:
h.sysOpenat(c, a0, a1, a2, a3)
case sysX64Open: // x64 open(pathname, flags, mode) → openat(AT_FDCWD, ...)
h.sysOpenat(c, atFDCWD, a0, a1, a2)
case SysClose:
if err := h.FS.Close(int(a0)); err != nil {
c.SetSyscallReturn(negErrno(EBADF))
} else {
c.SetSyscallReturn(0)
}
case SysFstat:
h.sysFstat(c, a0, a1)
case SysFstatat:
h.sysFstatat(c, a0, a1, a2)
case sysX64Stat: // x64 stat(pathname, statbuf) → fstatat(AT_FDCWD, ...)
h.sysFstatat(c, atFDCWD, a0, a1)
case sysX64Lstat: // x64 lstat(pathname, statbuf) → fstatat(AT_FDCWD, ...)
h.sysFstatat(c, atFDCWD, a0, a1)
case SysStatx:
h.sysStatx(c, a0, a1, a2, a3, a4)
case SysLseek:
h.sysLseek(c, a0, a1, a2)
case SysDup:
h.sysDup(c, a0)
case SysDup3:
h.sysDup3(c, a0, a1)
case SysPipe2:
h.sysPipe2(c, a0, a1)
case SysFcntl:
h.sysFcntl(c, a0, a1, a2)
case SysIoctl:
h.sysIoctl(c, a0, a1)
case SysGetdents64:
h.sysGetdents64(c, a0, a1, a2)
case SysReadlinkat:
h.sysReadlinkat(c, a0, a1, a2, a3)
case sysX64Readlink: // x64 readlink(pathname, buf, bufsiz) → readlinkat(AT_FDCWD, ...)
h.sysReadlinkat(c, atFDCWD, a0, a1, a2)
case SysFaccessat:
h.sysFaccessat(c, a0, a1)
case sysX64Access: // x64 access(pathname, mode) → faccessat(AT_FDCWD, ...)
h.sysFaccessat(c, atFDCWD, a0)
case SysGetcwd:
h.sysGetcwd(c, a0, a1)
case SysMkdirat:
h.sysMkdirat(c, a0, a1, a2)
case sysX64Mkdir: // x64 mkdir(pathname, mode) → mkdirat(AT_FDCWD, ...)
h.sysMkdirat(c, atFDCWD, a0, a1)
case SysUnlinkat:
h.sysUnlinkat(c, a0, a1, a2)
case sysX64Unlink: // x64 unlink(pathname) → unlinkat(AT_FDCWD, pathname, 0)
h.sysUnlinkat(c, atFDCWD, a0, 0)
case SysChdir:
h.sysChdir(c, a0)
case SysFchmod, SysFchmodat, SysFchownat, SysFchown:
c.SetSyscallReturn(0)
case SysFsync, SysFdatasync, SysFadvise64:
c.SetSyscallReturn(0)
case SysSync:
c.SetSyscallReturn(0)
case SysFtruncate:
h.sysFtruncate(c, a0, a1)
case SysFallocate:
h.sysFallocate(c, a0, a1, a2, a3)
case SysUtimensat:
c.SetSyscallReturn(0)
case SysSymlinkat:
h.sysSymlinkat(c, a0, a1, a2)
case sysX64Symlink: // x64 symlink(target, linkpath) → symlinkat(target, AT_FDCWD, linkpath)
h.sysSymlinkat(c, a0, atFDCWD, a1)
case SysLinkat:
c.SetSyscallReturn(negErrno(ENOSYS))
case SysRenameat2:
h.sysRenameat(c, a0, a1, a2, a3)
case sysX64Rename: // x64 rename(oldpath, newpath) → renameat(AT_FDCWD, old, AT_FDCWD, new)
h.sysRenameat(c, atFDCWD, a0, atFDCWD, a1)
// --- Process/Thread info ---
case SysGetuid, SysGeteuid, SysGetgid, SysGetegid:
c.SetSyscallReturn(1000)
case SysGetresuid:
// getresuid(ruid, euid, suid) - stub: all UIDs = 1000
if a0 != 0 {
h.Mem.Write32(a0, 1000)
}
if a1 != 0 {
h.Mem.Write32(a1, 1000)
}
if a2 != 0 {
h.Mem.Write32(a2, 1000)
}
c.SetSyscallReturn(0)
case SysGetresgid:
// getresgid(rgid, egid, sgid) - stub: all GIDs = 1000
if a0 != 0 {
h.Mem.Write32(a0, 1000)
}
if a1 != 0 {
h.Mem.Write32(a1, 1000)
}
if a2 != 0 {
h.Mem.Write32(a2, 1000)
}
c.SetSyscallReturn(0)
case SysGetpid, SysGetppid:
c.SetSyscallReturn(1)
case SysGettid:
tid := h.getTID(c)
c.SetSyscallReturn(uint64(tid))
case SysSetTidAddress:
tid := h.getTID(c)
h.clearChildTidMu.Lock()
if h.clearChildTid == nil {
h.clearChildTid = make(map[int]uint64)
}
h.clearChildTid[tid] = a0
h.clearChildTidMu.Unlock()
c.SetSyscallReturn(uint64(tid))
case SysUmask:
old := h.Umask
h.Umask = uint32(a0) & 0777
c.SetSyscallReturn(uint64(old))
// --- Signals ---
case SysRtSigaction:
h.sysRtSigaction(c, a0, a1, a2, a3)
case SysRtSigprocmask:
h.sysRtSigprocmask(c, a0, a1, a2)
case SysSigaltstack:
h.sysSigaltstack(c, a0, a1)
case SysRtSigreturn:
c.SetSyscallReturn(0) // TODO: restore saved signal frame
case SysKill, SysTkill:
h.Signals.Raise(int(a1))
c.SetSyscallReturn(0)
case SysTgkill:
h.Signals.Raise(int(a2))
c.SetSyscallReturn(0)
// --- Time ---
case SysClockGettime:
h.sysClockGettime(c, a0, a1)
case SysClockGetres:
h.sysClockGetres(c, a0, a1)
case SysClockNanosleep:
h.sysClockNanosleep(c, a0, a1, a2, a3)
case SysNanosleep:
h.sysNanosleep(c, a1)
// --- Resource limits ---
case SysPrlimit64:
h.sysPrlimit64(c, a0, a1, a2, a3)
case SysGetrusage:
h.sysGetrusage(c, a0, a1)
// --- Random ---
case SysGetrandom:
h.sysGetrandom(c, a0, a1)
// --- System info ---
case SysUname:
h.sysUname(c, a0)
case SysSysinfo:
h.sysSysinfo(c, a0)
// --- Threading ---
case SysFutex:
h.sysFutex(c, a0, a1, a2, a3)
case SysSetRobustList:
c.SetSyscallReturn(0)
case SysClone:
h.sysClone(c, a0, a1, a2, a3, a4)
case SysClone3:
h.sysClone3(c, a0, a1)
// --- Scheduling ---
case SysSchedYield:
c.SetSyscallReturn(0)
case SysSchedGetaffinity:
if a2 >= 8 {
h.Mem.Write64(a1, 1)
c.SetSyscallReturn(0)
} else {
c.SetSyscallReturn(negErrno(EINVAL))
}
case SysSchedSetaffinity:
c.SetSyscallReturn(0)
// --- Misc ---
case SysPrctl:
h.sysPrctl(c, a0, a1)
case SysArchPrctl:
h.sysArchPrctl(c, a0, a1)
case SysSyslog:
c.SetSyscallReturn(negErrno(EPERM))
case SysMembarrier:
c.SetSyscallReturn(0)
case SysKcmp:
c.SetSyscallReturn(negErrno(ENOSYS))
case SysMincore:
// mincore(addr, length, vec) - report pages in memory.
// Linux returns -ENOMEM if any page in [addr, addr+length) is not mapped.
pageSize := uint64(4096)
nPages := (a1 + pageSize - 1) / pageSize
for i := uint64(0); i < nPages; i++ {
pageAddr := a0 + i*pageSize
if _, _, err := h.Mem.LookupRead(pageAddr, mmu.PermRead); err != nil {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
h.Mem.Write8(a2+i, 1) // bit 0 set = page in RAM
}
c.SetSyscallReturn(0)
case SysRseq:
c.SetSyscallReturn(negErrno(ENOSYS))
case SysPpoll:
h.sysPpoll(c, a0, a1, a2, a3)
case sysPoll:
h.sysPollInt(c, a0, a1, int32(a2))
case SysPselect6:
c.SetSyscallReturn(0)
case SysTimes:
c.SetSyscallReturn(0)
case SysSendfile:
h.sysSendfile(c, a0, a1, a2, a3)
// --- Networking ---
case SysSocket:
h.sysSocket(c, a0, a1, a2)
case SysBind:
h.sysBind(c, a0, a1, a2)
case SysListen:
h.sysListen(c, a0, a1)
case SysAccept, SysAccept4:
h.sysAccept4(c, a0, a1, a2, a3)
case SysConnect:
h.sysConnect(c, a0, a1, a2)
case SysSendto:
h.sysSendto(c, a0, a1, a2, a3, a4, a5)
case SysRecvfrom:
h.sysRecvfrom(c, a0, a1, a2, a3, a4, a5)
case SysSendmsg:
h.sysSendmsg(c, a0, a1, a2)
case SysRecvmsg:
h.sysRecvmsg(c, a0, a1, a2)
case SysSetsockopt:
h.sysSetsockopt(c, a0, a1, a2, a3, a4)
case SysGetsockopt:
h.sysGetsockopt(c, a0, a1, a2, a3, a4)
case SysShutdown:
h.sysShutdown(c, a0, a1)
case SysSocketpair:
h.sysSocketpair(c, a0, a1, a2, a3)
case SysGetsockname:
h.sysGetsockname(c, a0, a1, a2)
case SysGetpeername:
h.sysGetpeername(c, a0, a1, a2)
// --- Epoll ---
case SysEpollCreate1:
h.sysEpollCreate1(c, a0)
case SysEpollCtl:
h.sysEpollCtl(c, a0, a1, a2, a3)
case SysEpollPwait:
h.sysEpollPwait(c, a0, a1, a2, a3)
case SysEventfd2:
h.sysEventfd2(c, a0, a1)
case SysTimerfdCreate:
h.sysTimerfdCreate(c, a0, a1)
case SysTimerfdSettime:
h.sysTimerfdSettime(c, a0, a1, a2, a3)
case SysTimerfdGettime:
h.sysTimerfdGettime(c, a0, a1)
// --- memfd ---
case SysMemfdCreate:
h.sysMemfdCreate(c, a0, a1)
// --- Wait ---
case SysWait4:
h.sysWait4(c, a0, a1, a2, a3)
// --- Exec ---
case SysExecve:
h.sysExecve(c, a0, a1, a2)
// --- Timers ---
case SysGetitimer:
h.sysGetitimer(c, a0, a1)
case SysSetitimer:
h.sysSetitimer(c, a0, a1, a2)
// --- RISC-V specific ---
case SysRiscvHwprobe:
// Return success; guest memory already zeroed → reports no extensions.
c.SetSyscallReturn(0)
default:
ravelog.Logf("[govm] unhandled syscall %d (a0=0x%x a1=0x%x a2=0x%x)\n", num, a0, a1, a2)
c.SetSyscallReturn(negErrno(ENOSYS))
}
}
// allocGuestPID returns a monotonically increasing guest PID for a new child process.
func (h *Handler) allocGuestPID() int {
h.childMu.Lock()
defer h.childMu.Unlock()
pid := h.nextGPID
h.nextGPID++
return pid
}
// sysExecve: if we're inside a vfork-child goroutine, launch the real host
// binary and unblock the parent. Otherwise return ENOSYS (we don't support
// exec-as-replace-image for the main guest process).
func (h *Handler) sysExecve(c arch.CPU, pathPtr, argvPtr, envpPtr uint64) {
path, err := h.readGuestString(pathPtr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
// Build argv
argv := h.readGuestStringArray(argvPtr)
// Build envp
envp := h.readGuestStringArray(envpPtr)
ravelog.Logf("[govm] execve(%q, %v)\n", path, argv)
h.vforkMu.Lock()
pending := h.vforkPending
ch := h.vforkDone
snap := h.vforkSnapshot
if pending {
h.vforkPending = false
}
h.vforkMu.Unlock()
if !pending {
// Not inside a vfork child - can't replace main guest image.
ravelog.Logf("[govm] execve outside vfork → ENOSYS\n")
c.SetSyscallReturn(negErrno(ENOSYS))
return
}
// Capture the child's current host FDs for 0/1/2 BEFORE restoring the
// parent's FD table. The child may have dup2'd a pipe write-end onto fd 1.
childHFD := [3]int{-1, -1, -1}
for i := 0; i < 3; i++ {
childHFD[i] = h.FS.GetHostFDNum(i)
}
// Restore the parent's FD table immediately - this undoes any dup2/close
// file actions the child performed on the shared FD table.
h.FS.RestoreFDs(snap)
// Launch the real host process using dup'd copies of the child's stdio fds.
dupOrFallback := func(hfd int, fallback *os.File) (*os.File, bool) {
if hfd >= 0 {
dupFD, err := syscall.Dup(hfd)
if err == nil {
return os.NewFile(uintptr(dupFD), fmt.Sprintf("hfd%d-dup", hfd)), true
}
}
return fallback, false
}
stdinF, stdinDup := dupOrFallback(childHFD[0], os.Stdin)
stdoutF, stdoutDup := dupOrFallback(childHFD[1], os.Stdout)
stderrF, stderrDup := dupOrFallback(childHFD[2], os.Stderr)
cmd := exec.Command(path, argv[1:]...)
cmd.Env = envp
cmd.Stdin = stdinF
cmd.Stdout = stdoutF
cmd.Stderr = stderrF
if err := cmd.Start(); err != nil {
ravelog.Logf("[govm] execve host start error: %v\n", err)
ch <- -1
c.Stop()
return
}
// Close our dup'd copies now that the child process owns them.
// This is critical for any pipe write-end: once we close our copy,
// the reader sees EOF when the child process exits.
if stdinDup {
stdinF.Close()
}
if stdoutDup {
stdoutF.Close()
}
if stderrDup {
stderrF.Close()
}
// Also close the original child host fds that are now owned by the subprocess.
// These were in the child's FD table (e.g. dup2'd pipe write-end on fd 1)
// and must not stay open in our process.
for i := 0; i < 3; i++ {
hfd := childHFD[i]
if hfd < 0 {
continue
}
// Don't close if it's the same as the parent's restored fd for that slot
// (i.e. child didn't change it) - GetHostFDNum after RestoreFDs reflects parent.
parentHFD := h.FS.GetHostFDNum(i)
if hfd != parentHFD {
syscall.Close(hfd)
}
}
gpid := h.allocGuestPID()
h.childMu.Lock()
h.childProcs[gpid] = cmd.Process
h.childMu.Unlock()
ravelog.Logf("[govm] execve launched pid=%d (guest=%d)\n", cmd.Process.Pid, gpid)
ch <- gpid
// Stop this child CPU - it has been replaced by the host process.
c.Stop()
}
// sysWait4 waits for a child host process, populating the wstatus word.
func (h *Handler) sysWait4(c arch.CPU, gpidArg, wstatusPtr, options, rusagePtr uint64) {
gpid := int(int32(gpidArg)) // signed: -1 = any child
h.childMu.Lock()
if len(h.childProcs) == 0 {
h.childMu.Unlock()
ravelog.Logf("[govm] wait4(pid=%d) → ECHILD (no children)\n", gpid)
c.SetSyscallReturn(negErrno(ECHILD))
return
}
// Find matching pid
var proc *os.Process
var foundPID int
for pid, p := range h.childProcs {
if gpid == -1 || gpid == pid {
proc = p
foundPID = pid
break
}
}
if proc == nil {
h.childMu.Unlock()
ravelog.Logf("[govm] wait4(pid=%d) → ECHILD (not found)\n", gpid)
c.SetSyscallReturn(negErrno(ECHILD))
return
}
h.childMu.Unlock()
const WNOHANG = 1
ravelog.Logf("[govm] wait4(guest=%d host=%d options=0x%x)\n", gpid, proc.Pid, options)
if options&WNOHANG != 0 {
// Non-blocking: use unix.Wait4 with WNOHANG.
var wstatus unix.WaitStatus
wpid, err := unix.Wait4(proc.Pid, &wstatus, unix.WNOHANG, nil)
if err != nil || wpid == 0 {
// Child not yet done.
c.SetSyscallReturn(0)
return
}
h.childMu.Lock()
delete(h.childProcs, foundPID)
h.childMu.Unlock()
ws := uint32(wstatus)
if wstatusPtr != 0 {
h.Mem.Write32(wstatusPtr, ws)
}
ravelog.Logf("[govm] wait4 WNOHANG → pid=%d wstatus=0x%x\n", foundPID, ws)
c.SetSyscallReturn(uint64(foundPID))
return
}
// Blocking wait.
var wstatus unix.WaitStatus
_, err := unix.Wait4(proc.Pid, &wstatus, 0, nil)
h.childMu.Lock()
delete(h.childProcs, foundPID)
h.childMu.Unlock()
if err != nil {
ravelog.Logf("[govm] wait4 error: %v\n", err)
c.SetSyscallReturn(negErrno(ECHILD))
return
}
ws := uint32(wstatus)
if wstatusPtr != 0 {
h.Mem.Write32(wstatusPtr, ws)
}
ravelog.Logf("[govm] wait4 → pid=%d wstatus=0x%x exitCode=%d\n", foundPID, ws, wstatus.ExitStatus())
c.SetSyscallReturn(uint64(foundPID))
}
// readGuestStringArray reads a NULL-terminated array of guest string pointers.
func (h *Handler) readGuestStringArray(ptr uint64) []string {
if ptr == 0 {
return nil
}
var result []string
for i := uint64(0); ; i++ {
addrBytes, err := h.Mem.ReadBytes(ptr+i*8, 8)
if err != nil {
break
}
strPtr := binary.LittleEndian.Uint64(addrBytes)
if strPtr == 0 {
break
}
s, err := h.readGuestString(strPtr)
if err != nil {
break
}
result = append(result, s)
}
return result
}
// --- Errno constants ---
const (
EPERM = 1
ENOENT = 2
ESRCH = 3
EINTR = 4
EIO = 5
ENXIO = 6
EBADF = 9
ECHILD = 10
EAGAIN = 11
ENOMEM = 12
EACCES = 13
EFAULT = 14
EEXIST = 17
ENODEV = 19
ENOTDIR = 20
EISDIR = 21
EINVAL = 22
ENFILE = 23
EMFILE = 24
ENOTTY = 25
ENOSPC = 28
ESPIPE = 29
EPIPE = 32
ERANGE = 34
ENOSYS = 38
ENOTEMPTY = 39
ENOTSOCK = 88
EMSGSIZE = 90
EPROTOTYPE = 91
ENOPROTOOPT = 92
EPROTONOSUPPORT = 93
EOPNOTSUPP = 95
EAFNOSUPPORT = 97
EADDRINUSE = 98
EADDRNOTAVAIL = 99
ENETUNREACH = 101
ECONNABORTED = 103
ECONNRESET = 104
ENOBUFS = 105
EISCONN = 106
ENOTCONN = 107
ETIMEDOUT = 110
ECONNREFUSED = 111
EINPROGRESS = 115
)
// AT_FDCWD is the "current working directory" sentinel for *at() syscalls.
const atFDCWD = uint64(0xFFFFFFFFFFFFFF9C) // -100 as uint64
func negErrno(e uint64) uint64 {
return ^(e - 1) // -e in two's complement
}
// getTID returns the guest thread ID for the calling CPU.
func (h *Handler) getTID(c arch.CPU) int {
if h.ThreadM != nil {
return h.ThreadM.GetTID(c)
}
return 1
}
// --- Helpers ---
// readBytes is a convenience wrapper for Mem.ReadBytes that discards the error.
func (h *Handler) readBytes(addr, n uint64) []byte {
data, _ := h.Mem.ReadBytes(addr, int(n))
return data
}
func (h *Handler) readGuestString(addr uint64) (string, error) {
if addr == 0 {
return "", syscall.EFAULT
}
var buf []byte
for {
b, err := h.Mem.Read8(addr)
if err != nil || b == 0 {
break
}
buf = append(buf, b)
addr++
}
return string(buf), nil
}
// --- Syscall implementations ---
// heapSignature computes a fast hash of the glibc heap chunk headers.
// We walk the chunk chain from aligned heap base and hash the size fields.
func (h *Handler) heapSignature() uint64 {
heapBase := h.Mem.GetBrkBase()
if heapBase == 0 {
return 0
}
// Aligned heap start (glibc aligns to 16)
aligned := (heapBase + 15) &^ 15
var sig uint64
addr := aligned
brkCur := h.Mem.Brk(0)
for i := 0; i < 64 && addr < brkCur; i++ {
// Read size field at addr+8
sz, err := h.Mem.Read64(addr + 8)
if err != nil {
break
}
chunkSz := sz &^ 7 // mask out flags
if chunkSz == 0 || chunkSz > (brkCur-heapBase) {
break // top chunk or invalid
}
sig = sig*31 + sz // include flags in sig
addr += chunkSz
}
return sig
}
func (h *Handler) sysOpenat(c arch.CPU, dirfd, pathAddr, flags, mode uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if path == "" {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
fd, err := h.FS.OpenAt(int(int32(dirfd)), path, int(flags), uint32(mode))
if err != nil {
if h.Trace {
ravelog.Logf("[govm] openat(%q) → ENOENT\n", path)
}
c.SetSyscallReturn(negErrno(ENOENT))
return
}
if h.Trace {
ravelog.Logf("[govm] openat(%q) → fd=%d\n", path, fd)
}
c.SetSyscallReturn(uint64(fd))
}
func (h *Handler) sysMprotect(c arch.CPU, addr, length, prot uint64) {
var perms uint8
if prot&0x1 != 0 {
perms |= mmu.PermRead
}
if prot&0x2 != 0 {
perms |= mmu.PermWrite
}
if prot&0x4 != 0 {
perms |= mmu.PermExecute
}
length = (length + mmu.PageSize - 1) & mmu.PageMask
addr = addr & mmu.PageMask
// Verify all pages are already mapped
for pg := addr; pg < addr+length; pg += mmu.PageSize {
if !h.Mem.IsMapped(pg) {
if h.Trace {
ravelog.Logf("[mprotect] ENOMEM: addr=0x%x len=0x%x prot=%d unmapped_page=0x%x\n", addr, length, prot, pg)
}
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
}
// Change permissions
if h.Trace {
ravelog.Logf("[mprotect] OK: addr=0x%x len=0x%x end=0x%x prot=%d perms=0x%x pages=%d\n", addr, length, addr+length, prot, perms, length/mmu.PageSize)
brkBase := h.Mem.GetBrkBase()
brkCur := h.Mem.Brk(0)
if brkBase != 0 && addr+length > brkBase {
ravelog.Logf("[mprotect] WARNING: overlaps heap! heap=0x%x-0x%x mprotect=0x%x-0x%x prot=%d\n",
brkBase, brkCur, addr, addr+length, prot)
}
}
for pg := addr; pg < addr+length; pg += mmu.PageSize {
h.Mem.MapPage(pg, perms)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysIoctl(c arch.CPU, fd, cmd uint64) {
if h.Trace {
ravelog.Logf("[govm] ioctl(fd=%d, cmd=0x%x)\n", fd, cmd)
}
const FIONREAD = 0x541B
const FIONBIO = 0x5421
switch cmd {
case 0x5401: // TCGETS
if fd <= 2 {
h.dumpGuestEnviron()
}
c.SetSyscallReturn(negErrno(ENOTTY))
return
case 0x5413: // TIOCGWINSZ
c.SetSyscallReturn(negErrno(ENOTTY))
return
case FIONREAD:
arg := c.SyscallArgs()[2]
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
avail, err := unix.IoctlRetInt(hostFD, FIONREAD)
if err == nil {
h.Mem.Write32(arg, uint32(avail))
if h.Trace {
ravelog.Logf("[govm] ioctl FIONREAD fd=%d→hfd=%d avail=%d\n", fd, hostFD, avail)
}
c.SetSyscallReturn(0)
return
}
}
h.Mem.Write32(arg, 0)
c.SetSyscallReturn(0)
return
case FIONBIO:
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
arg := c.SyscallArgs()[2]
val, _ := h.Mem.Read32(arg)
if val != 0 {
unix.SetNonblock(hostFD, true)
} else {
unix.SetNonblock(hostFD, false)
}
}
c.SetSyscallReturn(0)
return
}
if fd <= 2 {
c.SetSyscallReturn(negErrno(ENOTTY))
} else {
c.SetSyscallReturn(negErrno(ENOTTY))
}
}
// dumpGuestEnviron reads the guest's environ pointer and dumps each entry.
// This is a temporary debug helper.
func (h *Handler) dumpGuestEnviron() {
// Scan memory for the environ pointer pattern.
// The environ global is typically near the BSS of libc.
// As an approximation, we search for a sequence of guest pointers
// that form consecutive nul-terminated KEY=VALUE strings.
// More reliably: the env strings are at strArea = 0x6ffffffff000
// and the environ array is pointed to by glibc's __environ.
// We can compute the environ array address from the stack layout:
// - argc at sp
// - argv[0..n] at sp+8 .. sp+8*(argc+1)
// - env[0..m] at sp+8*(argc+2) ..
// Read sp from the initial stack pointer minus a bit.
// For now, use a known layout: stackTop=0x700000000000, strArea=stackTop-4096.
const stackTop = uint64(0x700000000000)
const strArea = stackTop - 4096
// Read argc first - it's at sp which we approximate from strArea.
// The actual sp is below strArea by nWords*8. We can scan from strArea upward
// to find the argc, but that's complex. Instead, just dump the strings
// in strArea directly.
if data, err := h.Mem.ReadBytes(strArea, 512); err == nil {
ravelog.Logf("[ENVIRON DUMP] strArea=0x%x:\n", strArea)
// Print as individual nul-terminated strings
start := 0
for i, b := range data {
if b == 0 {
if i > start {
fmt.Fprintf(os.Stderr, " [%d..%d] %q\n", start, i, data[start:i])
}
start = i + 1
}
}
}
}
func (h *Handler) sysFstat(c arch.CPU, fd, statAddr uint64) {
node := h.FS.Stat(int(fd))
if node == nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
h.writeStatBuf(statAddr, node)
c.SetSyscallReturn(0)
}
func (h *Handler) sysFstatat(c arch.CPU, dirfd, pathAddr, statAddr uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if path == "" {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
node, err := h.FS.StatPath(int(int32(dirfd)), path)
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
h.writeStatBuf(statAddr, node)
c.SetSyscallReturn(0)
}
func (h *Handler) sysStatx(c arch.CPU, dirfd, pathAddr, flags, mask, statxAddr uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if path == "" && flags&0x1000 == 0 { // AT_EMPTY_PATH = 0x1000
c.SetSyscallReturn(negErrno(ENOENT))
return
}
node, err := h.FS.StatPath(int(int32(dirfd)), path)
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
// struct statx = 256 bytes
buf := make([]byte, 256)
binary.LittleEndian.PutUint32(buf[0:], 0x7FF) // stx_mask
binary.LittleEndian.PutUint32(buf[4:], 4096) // stx_blksize
binary.LittleEndian.PutUint64(buf[8:], 0) // stx_attributes
binary.LittleEndian.PutUint32(buf[16:], node.Nlink) // stx_nlink
binary.LittleEndian.PutUint32(buf[20:], node.Uid) // stx_uid
binary.LittleEndian.PutUint32(buf[24:], node.Gid) // stx_gid
binary.LittleEndian.PutUint16(buf[28:], uint16(node.FileMode)) // stx_mode
binary.LittleEndian.PutUint64(buf[32:], node.Ino) // stx_ino
binary.LittleEndian.PutUint64(buf[40:], uint64(node.Size)) // stx_size
h.Mem.WriteBytes(statxAddr, buf)
c.SetSyscallReturn(0)
}
// writeStatBuf writes a struct stat for the current guest architecture.
func (h *Handler) writeStatBuf(addr uint64, node *vfs.VNode) {
h.writeStatBufArch(addr, node, h.Arch)
}
func (h *Handler) writeStatBufArch(addr uint64, node *vfs.VNode, a arch.Arch) {
if a == arch.ArchAMD64 {
// x86_64 struct stat: 144 bytes
buf := make([]byte, 144)
binary.LittleEndian.PutUint64(buf[0:], 1) // st_dev
binary.LittleEndian.PutUint64(buf[8:], node.Ino) // st_ino
binary.LittleEndian.PutUint64(buf[16:], uint64(node.Nlink)) // st_nlink (8 bytes)
binary.LittleEndian.PutUint32(buf[24:], node.FileMode) // st_mode
binary.LittleEndian.PutUint32(buf[28:], node.Uid) // st_uid
binary.LittleEndian.PutUint32(buf[32:], node.Gid) // st_gid
binary.LittleEndian.PutUint64(buf[48:], uint64(node.Size)) // st_size
binary.LittleEndian.PutUint64(buf[56:], 4096) // st_blksize (8 bytes)
binary.LittleEndian.PutUint64(buf[72:], uint64(node.Atime.Unix()))
binary.LittleEndian.PutUint64(buf[88:], uint64(node.Mtime.Unix()))
binary.LittleEndian.PutUint64(buf[104:], uint64(node.Ctime.Unix()))
h.Mem.WriteBytes(addr, buf)
} else {
// RISC-V struct stat: 128 bytes
buf := make([]byte, 128)
binary.LittleEndian.PutUint64(buf[0:], 1) // st_dev
binary.LittleEndian.PutUint64(buf[8:], node.Ino) // st_ino
binary.LittleEndian.PutUint32(buf[16:], node.FileMode) // st_mode
binary.LittleEndian.PutUint32(buf[20:], node.Nlink) // st_nlink
binary.LittleEndian.PutUint32(buf[24:], node.Uid) // st_uid
binary.LittleEndian.PutUint32(buf[28:], node.Gid) // st_gid
binary.LittleEndian.PutUint64(buf[48:], uint64(node.Size)) // st_size
binary.LittleEndian.PutUint32(buf[56:], 4096) // st_blksize
binary.LittleEndian.PutUint64(buf[72:], uint64(node.Atime.Unix()))
binary.LittleEndian.PutUint64(buf[88:], uint64(node.Mtime.Unix()))
binary.LittleEndian.PutUint64(buf[104:], uint64(node.Ctime.Unix()))
h.Mem.WriteBytes(addr, buf)
}
}
func (h *Handler) sysLseek(c arch.CPU, fd uint64, offset uint64, whence uint64) {
pos, err := h.FS.Lseek(int(fd), int64(offset), int(whence))
if err != nil {
c.SetSyscallReturn(negErrno(ESPIPE))
return
}
c.SetSyscallReturn(uint64(pos))
}
func (h *Handler) sysDup(c arch.CPU, oldfd uint64) {
newfd, err := h.FS.Dup(int(oldfd))
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
c.SetSyscallReturn(uint64(newfd))
}
func (h *Handler) sysDup3(c arch.CPU, oldfd, newfd uint64) {
err := h.FS.Dup3(int(oldfd), int(newfd))
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
c.SetSyscallReturn(newfd)
}
func (h *Handler) sysPipe2(c arch.CPU, pipefdAddr, flags uint64) {
// Strip O_NONBLOCK from the host pipe: Go's os.File doesn't use the
// network poller for pipes, so a non-blocking pipe causes EAGAIN on read
// instead of blocking. The guest's O_NONBLOCK semantics are honoured at
// the syscall level (our sysRead returns EAGAIN when appropriate).
hostFlags := int(flags) &^ unix.O_NONBLOCK
hostFDs := make([]int, 2)
if err := unix.Pipe2(hostFDs, hostFlags); err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
rf := os.NewFile(uintptr(hostFDs[0]), "pipe:r")
wf := os.NewFile(uintptr(hostFDs[1]), "pipe:w")
rfd := h.FS.AllocHostFile(rf)
wfd := h.FS.AllocHostFile(wf)
h.Mem.Write32(pipefdAddr, uint32(rfd))
h.Mem.Write32(pipefdAddr+4, uint32(wfd))
if h.Trace {
ravelog.Logf("[govm] pipe2 → rfd=%d wfd=%d\n", rfd, wfd)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetdents64(c arch.CPU, fd, buf, count uint64) {
// For host-backed fds (real directories on the host filesystem) call the
// kernel directly and copy the result into guest memory.
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
hostBuf := make([]byte, count)
n, err := unix.Getdents(hostFD, hostBuf)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
if n > 0 {
h.Mem.WriteBytes(buf, hostBuf[:n])
}
c.SetSyscallReturn(uint64(n))
return
}
entries := h.FS.Getdents64(int(fd))
if entries == nil {
c.SetSyscallReturn(0) // EOF
return
}
// Each dirent64: d_ino(8) + d_off(8) + d_reclen(2) + d_type(1) + name + padding
offset := uint64(0)
for _, e := range entries {
nameLen := len(e.Name) + 1 // +1 for null terminator
recLen := uint16(((8 + 8 + 2 + 1 + nameLen) + 7) & ^7) // 8-byte aligned
if offset+uint64(recLen) > count {
break
}
h.Mem.Write64(buf+offset, e.Ino)
h.Mem.Write64(buf+offset+8, offset+uint64(recLen))
h.Mem.Write16(buf+offset+16, recLen)
dtype := uint8(8) // DT_REG
if e.IsDir {
dtype = 4 // DT_DIR
}
h.Mem.Write8(buf+offset+18, dtype)
h.Mem.WriteBytes(buf+offset+19, append([]byte(e.Name), 0))
offset += uint64(recLen)
}
c.SetSyscallReturn(offset)
}
func (h *Handler) sysReadlinkat(c arch.CPU, dirfd, pathAddr, buf, bufsiz uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
target, err := h.FS.Readlinkat(int(int32(dirfd)), path)
if err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
data := []byte(target)
if uint64(len(data)) > bufsiz {
data = data[:bufsiz]
}
if h.Trace {
ravelog.Logf("[govm] readlinkat(%q) → %q (len=%d)\n", path, data, len(data))
}
h.Mem.WriteBytes(buf, data)
c.SetSyscallReturn(uint64(len(data)))
}
func (h *Handler) sysFaccessat(c arch.CPU, dirfd, pathAddr uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
_, err = h.FS.StatPath(int(int32(dirfd)), path)
if strings.Contains(path, "Xauth") || strings.Contains(path, "xauth") || strings.Contains(path, ".X") || strings.Contains(path, "mutter") {
if h.Trace {
ravelog.Logf("[govm] access(%q) → err=%v\n", path, err)
}
}
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetcwd(c arch.CPU, buf, size uint64) {
cwd := h.FS.Getcwd()
data := append([]byte(cwd), 0)
if uint64(len(data)) > size {
c.SetSyscallReturn(negErrno(ERANGE))
return
}
h.Mem.WriteBytes(buf, data)
c.SetSyscallReturn(uint64(len(data)))
}
func (h *Handler) sysMkdirat(c arch.CPU, dirfd, pathAddr, mode uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if path == "" {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
absPath := h.FS.ResolvePath(int(int32(dirfd)), path)
// Check if already exists
if _, err := h.FS.Resolve(absPath); err == nil {
c.SetSyscallReturn(negErrno(EEXIST))
return
}
// Check if parent exists
dir, _ := vfs.SplitPath(absPath)
if _, err := h.FS.Resolve(dir); err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
h.FS.MkdirAll(absPath)
c.SetSyscallReturn(0)
}
func (h *Handler) sysClockGettime(c arch.CPU, clockID, tsAddr uint64) {
if tsAddr != 0 {
now := time.Now()
h.Mem.Write64(tsAddr, uint64(now.Unix()))
h.Mem.Write64(tsAddr+8, uint64(now.Nanosecond()))
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysClockGetres(c arch.CPU, clockID, resAddr uint64) {
if resAddr != 0 {
h.Mem.Write64(resAddr, 0) // seconds
h.Mem.Write64(resAddr+8, 1) // nanoseconds (1ns resolution)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysNanosleep(c arch.CPU, reqAddr uint64) {
if reqAddr != 0 {
sec, _ := h.Mem.Read64(reqAddr)
nsec, _ := h.Mem.Read64(reqAddr + 8)
d := time.Duration(sec)*time.Second + time.Duration(nsec)*time.Nanosecond
if d > 0 && d < 10*time.Second {
time.Sleep(d)
}
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysFcntl(c arch.CPU, fd, cmd, arg uint64) {
switch cmd {
case 0: // F_DUPFD
newfd, err := h.FS.Dup(int(fd))
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
} else {
c.SetSyscallReturn(uint64(newfd))
}
case 1: // F_GETFD
c.SetSyscallReturn(0)
case 2: // F_SETFD
c.SetSyscallReturn(0)
case 3: // F_GETFL
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
flags, err := unix.FcntlInt(uintptr(hostFD), 3, 0)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(uint64(flags))
return
}
f := h.FS.GetFD(int(fd))
if f == nil {
c.SetSyscallReturn(negErrno(EBADF))
} else {
c.SetSyscallReturn(uint64(f.Flags))
}
case 4: // F_SETFL
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
_, err := unix.FcntlInt(uintptr(hostFD), 4, int(arg))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(0)
return
}
c.SetSyscallReturn(0)
case 1024: // F_DUPFD_CLOEXEC
newfd, err := h.FS.Dup(int(fd))
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
} else {
c.SetSyscallReturn(uint64(newfd))
}
default:
c.SetSyscallReturn(negErrno(EINVAL))
}
}
func (h *Handler) sysPrlimit64(c arch.CPU, pid, resource, newLimit, oldLimit uint64) {
if oldLimit != 0 {
h.Mem.Write64(oldLimit, 0x7FFFFFFFFFFFFFFF)
h.Mem.Write64(oldLimit+8, 0x7FFFFFFFFFFFFFFF)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetrandom(c arch.CPU, buf, count uint64) {
data := make([]byte, count)
rand.Read(data)
h.Mem.WriteBytes(buf, data)
c.SetSyscallReturn(count)
}
func (h *Handler) sysUname(c arch.CPU, buf uint64) {
const fieldLen = 65
machine := "riscv64"
if c.GetArch() == arch.ArchAMD64 {
machine = "x86_64"
}
fields := []string{
"Linux", // sysname
"Raven", // nodename
"1.0.0", // release
"#1 SMP", // version
machine, // machine
}
for i, s := range fields {
offset := uint64(i) * fieldLen
b := make([]byte, fieldLen)
copy(b, s)
h.Mem.WriteBytes(buf+offset, b)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysSysinfo(c arch.CPU, buf uint64) {
// struct sysinfo is 112 bytes on RISC-V
data := make([]byte, 112)
binary.LittleEndian.PutUint64(data[0:], uint64(time.Now().Unix())) // uptime
binary.LittleEndian.PutUint64(data[32:], 1024*1024*1024) // totalram (1GB)
binary.LittleEndian.PutUint64(data[40:], 512*1024*1024) // freeram
binary.LittleEndian.PutUint64(data[48:], 0) // sharedram
binary.LittleEndian.PutUint64(data[56:], 0) // bufferram
binary.LittleEndian.PutUint64(data[64:], 0) // totalswap
binary.LittleEndian.PutUint64(data[72:], 0) // freeswap
binary.LittleEndian.PutUint16(data[80:], 1) // procs
binary.LittleEndian.PutUint32(data[104:], 1) // mem_unit
h.Mem.WriteBytes(buf, data)
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetrusage(c arch.CPU, who, usage uint64) {
// struct rusage = 144 bytes, zero-fill
data := make([]byte, 144)
h.Mem.WriteBytes(usage, data)
c.SetSyscallReturn(0)
}
// --- Signal syscalls ---
func (h *Handler) sysRtSigaction(c arch.CPU, signum, actAddr, oldAddr, sigsetsize uint64) {
sig := int(signum)
if sig < 1 || sig > signal.NSIG {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
// Write old action if requested
if oldAddr != 0 {
old := h.Signals.GetAction(sig)
if old != nil {
h.Mem.Write64(oldAddr, old.Handler)
h.Mem.Write64(oldAddr+8, old.Flags)
h.Mem.Write64(oldAddr+16, old.Restorer)
h.Mem.Write64(oldAddr+24, old.Mask)
}
}
// Set new action if provided
if actAddr != 0 {
handler, _ := h.Mem.Read64(actAddr)
flags, _ := h.Mem.Read64(actAddr + 8)
restorer, _ := h.Mem.Read64(actAddr + 16)
mask, _ := h.Mem.Read64(actAddr + 24)
act := &signal.SigAction{
Handler: handler,
Flags: flags,
Restorer: restorer,
Mask: mask,
}
h.Signals.SetAction(sig, act)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysRtSigprocmask(c arch.CPU, how, setAddr, oldsetAddr uint64) {
var newmask uint64
if setAddr != 0 {
newmask, _ = h.Mem.Read64(setAddr)
}
old := h.Signals.SetMask(int(how), newmask)
if oldsetAddr != 0 {
h.Mem.Write64(oldsetAddr, old)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysSigaltstack(c arch.CPU, ssAddr, oldssAddr uint64) {
if oldssAddr != 0 {
oldSP, oldFlags, oldSize := h.Signals.SetAltStack(0, 0, 0)
h.Mem.Write64(oldssAddr, oldSP)
h.Mem.Write32(oldssAddr+8, uint32(oldFlags))
h.Mem.Write64(oldssAddr+16, oldSize)
// Restore since we just read
h.Signals.SetAltStack(oldSP, oldFlags, oldSize)
}
if ssAddr != 0 {
sp, _ := h.Mem.Read64(ssAddr)
flags, _ := h.Mem.Read32(ssAddr + 8)
size, _ := h.Mem.Read64(ssAddr + 16)
h.Signals.SetAltStack(sp, uint64(flags), size)
}
c.SetSyscallReturn(0)
}
// --- Filesystem operations ---
func (h *Handler) sysUnlinkat(c arch.CPU, dirfd, pathAddr, flags uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
err = h.FS.Unlinkat(int(int32(dirfd)), path, int(flags))
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysChdir(c arch.CPU, pathAddr uint64) {
path, err := h.readGuestString(pathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
err = h.FS.Chdir(path)
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysFtruncate(c arch.CPU, fd, length uint64) {
// If backed by a host file (memfd), truncate the real file
if hf := h.FS.GetHostFile(int(fd)); hf != nil {
if err := hf.Truncate(int64(length)); err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
c.SetSyscallReturn(0)
return
}
err := h.FS.Ftruncate(int(fd), int64(length))
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysFallocate(c arch.CPU, fd, mode, offset, length uint64) {
// Simplified: just extend the file if needed
total := int64(offset + length)
err := h.FS.Ftruncate(int(fd), total)
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysSymlinkat(c arch.CPU, targetAddr, dirfd, linkpathAddr uint64) {
target, err := h.readGuestString(targetAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
linkpath, err := h.readGuestString(linkpathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
err = h.FS.Symlinkat(target, int(int32(dirfd)), linkpath)
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysRenameat(c arch.CPU, olddirfd, oldpathAddr, newdirfd, newpathAddr uint64) {
oldpath, err := h.readGuestString(oldpathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
newpath, err := h.readGuestString(newpathAddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
err = h.FS.Renameat(int(int32(olddirfd)), oldpath, int(int32(newdirfd)), newpath)
if err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
// --- Time syscalls ---
func (h *Handler) sysClockNanosleep(c arch.CPU, clockID, flags, reqAddr, remAddr uint64) {
if reqAddr == 0 {
c.SetSyscallReturn(0)
return
}
sec, _ := h.Mem.Read64(reqAddr)
nsec, _ := h.Mem.Read64(reqAddr + 8)
if flags&1 != 0 { // TIMER_ABSTIME
target := time.Unix(int64(sec), int64(nsec))
d := time.Until(target)
if d > 0 && d < 60*time.Second {
time.Sleep(d)
}
} else {
d := time.Duration(sec)*time.Second + time.Duration(nsec)*time.Nanosecond
if d > 0 && d < 60*time.Second {
time.Sleep(d)
}
}
// Write zero to remaining time
if remAddr != 0 {
h.Mem.Write64(remAddr, 0)
h.Mem.Write64(remAddr+8, 0)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetitimer(c arch.CPU, which, currValue uint64) {
if currValue != 0 {
// struct itimerval = 2 × struct timeval = 2 × (8+8) = 32 bytes
data := make([]byte, 32)
h.Mem.WriteBytes(currValue, data) // zero-filled = timer disabled
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysSetitimer(c arch.CPU, which, newValue, oldValue uint64) {
if oldValue != 0 {
data := make([]byte, 32)
h.Mem.WriteBytes(oldValue, data) // return old as zero
}
c.SetSyscallReturn(0)
}
// --- ppoll ---
func (h *Handler) sysPpoll(c arch.CPU, fdsAddr, nfds, timeoutAddr, sigmask uint64) {
if nfds == 0 {
c.SetSyscallReturn(0)
return
}
// Build host pollfd array by translating guest fds → host fds.
// struct pollfd: fd(4) + events(2) + revents(2) = 8 bytes
const pollfdSize = 8
hostFDs := make([]unix.PollFd, 0, nfds)
guestFDIdx := make([]int, 0, nfds) // guest fd number for each hostFD entry
for i := uint64(0); i < nfds; i++ {
off := fdsAddr + i*pollfdSize
gfd32, _ := h.Mem.Read32(off)
events, _ := h.Mem.Read16(off + 4)
gfd := int(int32(gfd32))
hfd := h.FS.GetHostFDNum(gfd)
if hfd < 0 {
// Non-host fd (VFS file) - skip from real poll, mark no events
continue
}
hostFDs = append(hostFDs, unix.PollFd{Fd: int32(hfd), Events: int16(events)})
guestFDIdx = append(guestFDIdx, int(i))
}
// Build timeout
var timeoutMs int
if timeoutAddr == 0 {
timeoutMs = -1 // block indefinitely
} else {
sec, _ := h.Mem.Read64(timeoutAddr)
nsec, _ := h.Mem.Read64(timeoutAddr + 8)
ms := int64(sec)*1000 + int64(nsec)/1_000_000
if ms < 0 {
ms = 0
}
timeoutMs = int(ms)
}
var n int
var err error
if len(hostFDs) > 0 {
n, err = unix.Poll(hostFDs, timeoutMs)
} else {
// No host fds to poll - sleep for timeout and return 0
if timeoutMs > 0 {
time.Sleep(time.Duration(timeoutMs) * time.Millisecond)
}
c.SetSyscallReturn(0)
return
}
if err != nil {
if err == syscall.EINTR {
c.SetSyscallReturn(negErrno(EINTR))
} else {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
}
return
}
// Write revents back into guest pollfd structs
for j, idx := range guestFDIdx {
off := fdsAddr + uint64(idx)*pollfdSize
h.Mem.Write16(off+6, uint16(hostFDs[j].Revents))
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysPrctl(c arch.CPU, option, arg uint64) {
switch option {
case 15: // PR_SET_NAME
c.SetSyscallReturn(0)
case 16: // PR_GET_NAME
h.Mem.WriteBytes(arg, append([]byte("govm"), 0))
c.SetSyscallReturn(0)
default:
c.SetSyscallReturn(0)
}
}
func (h *Handler) sysArchPrctl(c arch.CPU, code, addr uint64) {
switch code {
case 0x1001: // ARCH_SET_GS
c.SetSyscallReturn(0)
case 0x1002: // ARCH_SET_FS
c.SetTLS(addr)
c.SetSyscallReturn(0)
case 0x1003: // ARCH_GET_FS
h.Mem.Write64(addr, c.GetTLS())
c.SetSyscallReturn(0)
case 0x1004: // ARCH_GET_GS
h.Mem.Write64(addr, 0)
c.SetSyscallReturn(0)
default:
c.SetSyscallReturn(negErrno(EINVAL))
}
}
func (h *Handler) sysFutex(c arch.CPU, uaddr, op, val, timeout uint64) {
futexOp := op & 0x7F // mask out FUTEX_PRIVATE_FLAG
switch futexOp {
case 0, 9: // FUTEX_WAIT / FUTEX_WAIT_BITSET
// Read current value at uaddr
cur, err := h.Mem.Read32(uaddr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if cur != uint32(val) {
c.SetSyscallReturn(negErrno(EAGAIN))
return
}
// For single-threaded, we'd block forever. Use thread manager if available.
if h.ThreadM != nil {
h.ThreadM.FutexWait(uaddr, uint32(val))
}
c.SetSyscallReturn(0)
case 1, 10: // FUTEX_WAKE / FUTEX_WAKE_BITSET
woken := 0
if h.ThreadM != nil {
woken = h.ThreadM.FutexWake(uaddr, int(val))
}
c.SetSyscallReturn(uint64(woken))
default:
c.SetSyscallReturn(0)
}
}
// sysPollInt handles x64's poll(2) where timeout is int milliseconds (not *timespec).
func (h *Handler) sysPollInt(c arch.CPU, fdsAddr, nfds uint64, timeoutMs int32) {
if nfds == 0 {
if timeoutMs > 0 {
time.Sleep(time.Duration(timeoutMs) * time.Millisecond)
}
c.SetSyscallReturn(0)
return
}
const pollfdSize = 8
hostFDs := make([]unix.PollFd, 0, nfds)
guestFDIdx := make([]int, 0, nfds)
for i := uint64(0); i < nfds; i++ {
off := fdsAddr + i*pollfdSize
gfd32, _ := h.Mem.Read32(off)
events, _ := h.Mem.Read16(off + 4)
gfd := int(int32(gfd32))
hfd := h.FS.GetHostFDNum(gfd)
if hfd < 0 {
continue
}
hostFDs = append(hostFDs, unix.PollFd{Fd: int32(hfd), Events: int16(events)})
guestFDIdx = append(guestFDIdx, int(i))
}
var n int
var err error
if len(hostFDs) > 0 {
n, err = unix.Poll(hostFDs, int(timeoutMs))
} else {
// All polled FDs are emulated (no host backing). Sleep to avoid spinning.
switch {
case timeoutMs == 0:
// non-blocking poll: just return 0
case timeoutMs > 0:
time.Sleep(time.Duration(timeoutMs) * time.Millisecond)
default: // -1 (infinite)
time.Sleep(10 * time.Millisecond)
}
c.SetSyscallReturn(0)
return
}
if err != nil {
if err == syscall.EINTR {
c.SetSyscallReturn(negErrno(EINTR))
return
}
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
// Write revents back
for j, idx := range guestFDIdx {
off := fdsAddr + uint64(idx)*pollfdSize
h.Mem.Write16(off+6, uint16(hostFDs[j].Revents))
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysClone(c arch.CPU, flags, childStack, parentTid, a3, a4 uint64) {
tls := uint64(0)
childTid := a4
if c.GetArch() == arch.ArchAMD64 {
// x86_64: flags, stack, parent_tid, child_tid, tls
childTid = a3
tls = a4
}
// If this is a thread creation (CLONE_THREAD is the definitive flag for pthreads).
// posix_spawn and vfork use CLONE_VM|CLONE_VFORK but NOT CLONE_THREAD; those
// should fall through to the fork-like path and fail gracefully.
if h.ThreadM != nil && flags&0x10000 != 0 { // CLONE_THREAD
childPC := c.SyscallReturnPC()
if childStack == 0 {
childStack = c.GetSP()
}
childTLS := uint64(0)
if flags&0x00080000 != 0 { // CLONE_SETTLS
childTLS = tls
}
tid, err := h.ThreadM.Clone(c, childPC, childStack, childTLS, 0, 0)
if err != nil {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
// Write TID to child_tidptr if CLONE_CHILD_SETTID (0x01000000)
if flags&0x01000000 != 0 && childTid != 0 {
h.Mem.Write32(childTid, uint32(tid))
}
// Write TID to parent_tidptr if CLONE_PARENT_SETTID (0x00100000)
if flags&0x00100000 != 0 && parentTid != 0 {
h.Mem.Write32(parentTid, uint32(tid))
}
c.SetSyscallReturn(uint64(tid))
} else {
// fork-like (posix_spawn, vfork): if CLONE_VFORK, run the child as a
// goroutine that can call execve to launch a real host process, then
// unblocks the parent. Without CLONE_VFORK just fail immediately.
const CLONE_VFORK = 0x4000
if flags&CLONE_VFORK != 0 {
gpid := h.allocGuestPID()
ch := make(chan int, 1)
snap := h.FS.SnapshotFDs() // save parent FD table before child modifies it
h.vforkMu.Lock()
h.vforkPending = true
h.vforkDone = ch
h.vforkSnapshot = snap
h.vforkMu.Unlock()
// Child goroutine: clone the CPU state and run until execve/exit.
childCPU := c.CloneForThread()
childCPU.SetSyscallReturn(0) // child sees pid 0
if childStack != 0 {
childCPU.SetSP(childStack)
}
childCPU.SetSyscallHandler(h.Handle)
go func() {
if err2 := childCPU.Run(); err2 != nil {
ravelog.Logf("[govm] vfork-child exited: %v\n", err2)
}
// If child exited without execve, restore FD table and unblock parent.
h.vforkMu.Lock()
pending := h.vforkPending
h.vforkPending = false
savedSnap := h.vforkSnapshot
h.vforkMu.Unlock()
if pending {
h.FS.RestoreFDs(savedSnap)
ch <- gpid
}
}()
// Parent blocks until child calls execve (or exits).
guestPID := <-ch
c.SetSyscallReturn(uint64(guestPID))
} else {
ravelog.Logf("[govm] clone fork-like flags=0x%x → ENOSYS\n", flags)
c.SetSyscallReturn(negErrno(ENOSYS))
}
}
}
// sysClone3 handles the clone3(2) syscall (x86-64 SYS 435).
// Unlike clone(2), clone3 passes a pointer to struct clone_args rather than
// individual register arguments.
//
// struct clone_args layout (CLONE_ARGS_SIZE_VER0 = 64 bytes):
//
// u64 flags, pidfd, child_tid, parent_tid, exit_signal, stack, stack_size, tls
func (h *Handler) sysClone3(c arch.CPU, uargsPtr, size uint64) {
if size < 64 {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
// Read struct clone_args from guest memory.
buf, err := h.Mem.ReadBytes(uargsPtr, 64)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
flags := binary.LittleEndian.Uint64(buf[0:8])
pidfdPtr := binary.LittleEndian.Uint64(buf[8:16])
childTid := binary.LittleEndian.Uint64(buf[16:24])
parentTid := binary.LittleEndian.Uint64(buf[24:32])
// exit_signal at buf[32:40] - not used beyond SIGCHLD detection
stack := binary.LittleEndian.Uint64(buf[40:48])
stackSize := binary.LittleEndian.Uint64(buf[48:56])
tls := binary.LittleEndian.Uint64(buf[56:64])
if h.ThreadM != nil && flags&0x10000 != 0 { // CLONE_THREAD → pthread creation
childPC := c.SyscallReturnPC()
childStack := stack + stackSize // stack grows down: top = base + size
if childStack == 0 {
childStack = c.GetSP()
}
childTLS := uint64(0)
if flags&0x00080000 != 0 { // CLONE_SETTLS
childTLS = tls
}
tid, err := h.ThreadM.Clone(c, childPC, childStack, childTLS, stack, stackSize)
if err != nil {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
if flags&0x01000000 != 0 && childTid != 0 { // CLONE_CHILD_SETTID
h.Mem.Write32(childTid, uint32(tid))
}
if flags&0x00100000 != 0 && parentTid != 0 { // CLONE_PARENT_SETTID
h.Mem.Write32(parentTid, uint32(tid))
}
if flags&0x1000 != 0 && pidfdPtr != 0 { // CLONE_PIDFD
// Write a fake pidfd (-1) so callers that require a valid fd get
// a safe sentinel. The real pidfd model isn't implemented.
h.Mem.Write32(pidfdPtr, ^uint32(0))
}
c.SetSyscallReturn(uint64(tid))
} else {
// Process-creation (fork-like, posix_spawn, vfork).
const CLONE_VFORK = 0x4000
if flags&CLONE_VFORK != 0 {
gpid := h.allocGuestPID()
ch := make(chan int, 1)
snap := h.FS.SnapshotFDs()
h.vforkMu.Lock()
h.vforkPending = true
h.vforkDone = ch
h.vforkSnapshot = snap
h.vforkMu.Unlock()
childCPU := c.CloneForThread()
childCPU.SetSyscallReturn(0)
if stack != 0 {
childCPU.SetSP(stack + stackSize)
}
childCPU.SetSyscallHandler(h.Handle)
if flags&0x00080000 != 0 && tls != 0 { // CLONE_SETTLS
childCPU.SetTLS(tls)
}
go func() {
if err2 := childCPU.Run(); err2 != nil {
ravelog.Logf("[govm] vfork-child exited: %v\n", err2)
}
h.vforkMu.Lock()
pending := h.vforkPending
h.vforkPending = false
savedSnap := h.vforkSnapshot
h.vforkMu.Unlock()
if pending {
h.FS.RestoreFDs(savedSnap)
ch <- gpid
}
}()
guestPID := <-ch
if flags&0x1000 != 0 && pidfdPtr != 0 { // CLONE_PIDFD
h.Mem.Write32(pidfdPtr, ^uint32(0))
}
c.SetSyscallReturn(uint64(guestPID))
} else {
ravelog.Logf("[govm] clone3 fork-like flags=0x%x → ENOSYS\n", flags)
c.SetSyscallReturn(negErrno(ENOSYS))
}
}
}
func (h *Handler) sysWrite(c arch.CPU, fdNum, bufAddr, count uint64) {
data, err := h.Mem.ReadBytes(bufAddr, int(count))
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
n, err := h.FS.Write(int(fdNum), data)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysWritev(c arch.CPU, fdNum, iovAddr, iovcnt uint64) {
// Debug: dump heap before error output
if h.Trace && fdNum == 2 {
heapBase := h.Mem.GetBrkBase()
if heapBase != 0 {
// Dump first 2048 bytes for better visibility
if data, err := h.Mem.ReadBytes(heapBase, 2048); err == nil {
ravelog.Logf("[heap-dump] base=0x%x\n", heapBase)
// Print as 8-byte words for easier reading
for off := 0; off+8 <= len(data); off += 8 {
val := binary.LittleEndian.Uint64(data[off:])
if val != 0 || off < 64 || off > len(data)-64 {
fmt.Fprintf(os.Stderr, " +0x%04x: 0x%016x\n", off, val)
}
}
}
}
}
var total uint64
for i := uint64(0); i < iovcnt; i++ {
base, err := h.Mem.Read64(iovAddr + i*16)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
length, err := h.Mem.Read64(iovAddr + i*16 + 8)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
data, err := h.Mem.ReadBytes(base, int(length))
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if h.Trace {
if hfd := h.FS.GetHostFDNum(int(fdNum)); hfd >= 0 {
disp := data
if len(disp) > 24 {
disp = disp[:24]
}
ravelog.Logf("[govm] writev(fd=%d→hfd=%d, buf=%d, base=0x%x): %02x\n", fdNum, hfd, length, base, disp)
}
}
n, err := h.FS.Write(int(fdNum), data)
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
total += uint64(n)
}
c.SetSyscallReturn(total)
}
func (h *Handler) sysRead(c arch.CPU, fdNum, bufAddr, count uint64) {
buf := make([]byte, count)
n, err := h.FS.Read(int(fdNum), buf)
if err != nil && err != io.EOF {
hfd := h.FS.GetHostFDNum(int(fdNum))
ravelog.Logf("[govm] read(fd=%d→hfd=%d) err=%v\n", fdNum, hfd, err)
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
if n > 0 {
if h.Trace {
if hfd := h.FS.GetHostFDNum(int(fdNum)); hfd >= 0 {
ravelog.Logf("[govm] read(fd=%d→hfd=%d, n=%d): %02x\n", fdNum, hfd, n, buf[:min(8, n)])
}
}
if err := h.Mem.WriteBytes(bufAddr, buf[:n]); err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysPread64(c arch.CPU, fdNum, bufAddr, count, offset uint64) {
buf := make([]byte, count)
n, err := h.FS.Pread(int(fdNum), buf, int64(offset))
if err != nil && err != io.EOF {
c.SetSyscallReturn(negErrno(EBADF))
return
}
if n > 0 {
if err := h.Mem.WriteBytes(bufAddr, buf[:n]); err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysPwrite64(c arch.CPU, fdNum, bufAddr, count, offset uint64) {
data, err := h.Mem.ReadBytes(bufAddr, int(count))
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
n, err := h.FS.Pwrite(int(fdNum), data, int64(offset))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysSendfile(c arch.CPU, outFd, inFd, offsetAddr, count uint64) {
buf := make([]byte, count)
n, err := h.FS.Read(int(inFd), buf)
if err != nil || n == 0 {
c.SetSyscallReturn(negErrno(EBADF))
return
}
wn, err := h.FS.Write(int(outFd), buf[:n])
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
if offsetAddr != 0 {
off, _ := h.Mem.Read64(offsetAddr)
off += uint64(n)
h.Mem.Write64(offsetAddr, off)
}
c.SetSyscallReturn(uint64(wn))
}
func (h *Handler) sysReadv(c arch.CPU, fdNum, iovAddr, iovcnt uint64) {
var total uint64
for i := uint64(0); i < iovcnt; i++ {
base, err := h.Mem.Read64(iovAddr + i*16)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
length, err := h.Mem.Read64(iovAddr + i*16 + 8)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
buf := make([]byte, length)
n, err := h.FS.Read(int(fdNum), buf)
if err != nil && err != io.EOF {
if total > 0 {
break
}
c.SetSyscallReturn(negErrno(EBADF))
return
}
if n > 0 {
if err := h.Mem.WriteBytes(base, buf[:n]); err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
}
total += uint64(n)
if n < int(length) || err == io.EOF {
break
}
}
c.SetSyscallReturn(total)
}
// ===================================================================
// Socket syscalls - AF_UNIX uses in-memory VFS sockets; AF_INET and
// AF_INET6 pass through to real host sockets.
// ===================================================================
const (
AF_INET = 2
AF_INET6 = 10
)
func (h *Handler) sysSocket(c arch.CPU, domain, sockType, protocol uint64) {
stype := int(sockType & 0xff) // strip SOCK_CLOEXEC/SOCK_NONBLOCK
if h.Trace {
ravelog.Logf("[govm] socket(domain=%d, type=%d)\n", domain, stype)
}
if domain == vfs.AF_UNIX {
// Use a real host socket so the guest can connect to host AF_UNIX sockets
// (Wayland, D-Bus, X11, etc.). Preserve SOCK_CLOEXEC/SOCK_NONBLOCK flags.
hostFlags := int(sockType) | syscall.SOCK_CLOEXEC
hostFD, err := syscall.Socket(syscall.AF_UNIX, hostFlags, 0)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
gfd := h.FS.WrapHostFD(hostFD, fmt.Sprintf("socket(AF_UNIX,%d)", stype))
c.SetSyscallReturn(uint64(gfd))
return
}
// AF_INET / AF_INET6 - create real host socket
if domain != AF_INET && domain != AF_INET6 {
c.SetSyscallReturn(negErrno(EAFNOSUPPORT))
return
}
hostFD, err := syscall.Socket(int(domain), stype, int(protocol))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
gfd := h.FS.WrapHostFD(hostFD, fmt.Sprintf("socket(%d,%d)", domain, stype))
c.SetSyscallReturn(uint64(gfd))
}
func (h *Handler) sysConnect(c arch.CPU, fd, addrPtr, addrLen uint64) {
// Host socket?
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
sa := h.parseSockaddr(addrPtr, addrLen)
if sa == nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
err := syscall.Connect(hostFD, sa)
if err != nil {
ravelog.Logf("[govm] connect ERROR: %v\n", err)
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(0)
return
}
// AF_UNIX in-memory socket
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
addrBuf := h.readBytes(addrPtr, addrLen)
if len(addrBuf) < 3 {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
path := extractUnixPath(addrBuf)
if err := sock.Connect(path); err != nil {
c.SetSyscallReturn(negErrno(ECONNREFUSED))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysBind(c arch.CPU, fd, addrPtr, addrLen uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
sa := h.parseSockaddr(addrPtr, addrLen)
if sa == nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
err := syscall.Bind(hostFD, sa)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(0)
return
}
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
addrBuf := h.readBytes(addrPtr, addrLen)
if len(addrBuf) < 3 {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
path := extractUnixPath(addrBuf)
if err := sock.Bind(path); err != nil {
c.SetSyscallReturn(negErrno(EADDRINUSE))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysListen(c arch.CPU, fd, backlog uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
err := syscall.Listen(hostFD, int(backlog))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(0)
return
}
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
if err := sock.Listen(int(backlog)); err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysAccept4(c arch.CPU, fd, addrPtr, addrLenPtr, flags uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
nfd, sa, err := syscall.Accept(hostFD)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
// Write peer address back if requested
if addrPtr != 0 && addrLenPtr != 0 {
h.writeSockaddr(addrPtr, addrLenPtr, sa)
}
gfd := h.FS.WrapHostFD(nfd, "accepted")
c.SetSyscallReturn(uint64(gfd))
return
}
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
newSock, err := sock.Accept()
if err != nil {
c.SetSyscallReturn(negErrno(EAGAIN))
return
}
h.FS.Lock()
gfd := h.FS.AllocSocketUnlocked(newSock)
h.FS.Unlock()
c.SetSyscallReturn(uint64(gfd))
}
func (h *Handler) sysSendto(c arch.CPU, fd, bufAddr, bufLen, flags, destAddr, destLen uint64) {
// Try host FD first (covers both host-backed sockets and HostFile FDs)
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
buf := h.readBytes(bufAddr, bufLen)
n, err := syscall.Write(hostFD, buf)
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(uint64(n))
return
}
sock := h.FS.GetSocket(int(fd))
if sock != nil {
buf := h.readBytes(bufAddr, bufLen)
n, err := sock.Write(buf)
if err != nil {
c.SetSyscallReturn(negErrno(ECONNRESET))
return
}
c.SetSyscallReturn(uint64(n))
return
}
// Fallback to VFS write
buf := h.readBytes(bufAddr, bufLen)
n, err := h.FS.Write(int(fd), buf)
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysRecvfrom(c arch.CPU, fd, bufAddr, bufLen, flags, srcAddr, srcLenPtr uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
buf := make([]byte, bufLen)
n, err := syscall.Read(hostFD, buf)
if err != nil {
if h.Trace {
ravelog.Logf("[govm] recvfrom(fd=%d→hfd=%d, len=%d) err=%v\n", fd, hostFD, bufLen, err)
}
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
if n > 0 {
if h.Trace {
ravelog.Logf("[govm] recvfrom(fd=%d→hfd=%d, n=%d): %02x\n", fd, hostFD, n, buf[:min(64, n)])
}
h.Mem.WriteBytes(bufAddr, buf[:n])
}
c.SetSyscallReturn(uint64(n))
return
}
sock := h.FS.GetSocket(int(fd))
if sock != nil {
buf := make([]byte, bufLen)
n, err := sock.Read(buf)
if err != nil {
c.SetSyscallReturn(negErrno(ECONNRESET))
return
}
if n > 0 {
h.Mem.WriteBytes(bufAddr, buf[:n])
}
c.SetSyscallReturn(uint64(n))
return
}
// Fallback to VFS read
buf := make([]byte, bufLen)
n, err := h.FS.Read(int(fd), buf)
if err != nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
if n > 0 {
h.Mem.WriteBytes(bufAddr, buf[:n])
}
c.SetSyscallReturn(uint64(n))
}
func (h *Handler) sysShutdown(c arch.CPU, fd, how uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
err := syscall.Shutdown(hostFD, int(how))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
c.SetSyscallReturn(0)
return
}
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
sock.Close()
c.SetSyscallReturn(0)
}
func (h *Handler) sysSocketpair(c arch.CPU, domain, sockType, protocol, sv uint64) {
fds, err := syscall.Socketpair(int(domain), int(sockType), int(protocol))
if err != nil {
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
gfd0 := h.FS.WrapHostFD(fds[0], "socketpair")
gfd1 := h.FS.WrapHostFD(fds[1], "socketpair")
var buf [8]byte
binary.LittleEndian.PutUint32(buf[0:4], uint32(gfd0))
binary.LittleEndian.PutUint32(buf[4:8], uint32(gfd1))
h.Mem.WriteBytes(sv, buf[:])
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetsockopt(c arch.CPU, fd, level, optname, optval, optlen uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
if optval != 0 && optlen != 0 {
lenBuf := h.readBytes(optlen, 4)
if len(lenBuf) >= 4 {
olen := int(binary.LittleEndian.Uint32(lenBuf))
val, err := syscall.GetsockoptInt(hostFD, int(level), int(optname))
if err != nil {
// fallback: return 0
var buf [4]byte
h.Mem.WriteBytes(optval, buf[:])
} else if olen >= 4 {
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], uint32(val))
h.Mem.WriteBytes(optval, buf[:])
}
_ = val
}
}
c.SetSyscallReturn(0)
return
}
// Stub for VFS sockets
if optval != 0 && optlen != 0 {
lenBuf := h.readBytes(optlen, 4)
if len(lenBuf) >= 4 {
olen := binary.LittleEndian.Uint32(lenBuf)
if olen >= 4 {
var buf [4]byte
h.Mem.WriteBytes(optval, buf[:])
}
}
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysSetsockopt(c arch.CPU, fd, level, optname, optval, optlen uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 && optval != 0 && optlen >= 4 {
valBuf := h.readBytes(optval, 4)
val := int(binary.LittleEndian.Uint32(valBuf))
syscall.SetsockoptInt(hostFD, int(level), int(optname), val)
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetsockname(c arch.CPU, fd, addrPtr, addrLenPtr uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
sa, err := syscall.Getsockname(hostFD)
if err == nil && addrPtr != 0 && addrLenPtr != 0 {
h.writeSockaddr(addrPtr, addrLenPtr, sa)
}
c.SetSyscallReturn(0)
return
}
// Minimal stub: return AF_UNIX + empty path
if addrPtr != 0 && addrLenPtr != 0 {
var buf [2]byte
binary.LittleEndian.PutUint16(buf[:], uint16(vfs.AF_UNIX))
h.Mem.WriteBytes(addrPtr, buf[:])
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], 2)
h.Mem.WriteBytes(addrLenPtr, lenBuf[:])
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysGetpeername(c arch.CPU, fd, addrPtr, addrLenPtr uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD >= 0 {
sa, err := syscall.Getpeername(hostFD)
if err == nil && addrPtr != 0 && addrLenPtr != 0 {
h.writeSockaddr(addrPtr, addrLenPtr, sa)
}
c.SetSyscallReturn(0)
return
}
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
if addrPtr != 0 && addrLenPtr != 0 {
var buf [2]byte
binary.LittleEndian.PutUint16(buf[:], uint16(vfs.AF_UNIX))
h.Mem.WriteBytes(addrPtr, buf[:])
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], 2)
h.Mem.WriteBytes(addrLenPtr, lenBuf[:])
}
c.SetSyscallReturn(0)
}
// parseSockaddr reads a Linux sockaddr from guest memory and returns a Go Sockaddr.
func (h *Handler) parseSockaddr(addrPtr, addrLen uint64) syscall.Sockaddr {
buf := h.readBytes(addrPtr, addrLen)
if len(buf) < 2 {
return nil
}
family := binary.LittleEndian.Uint16(buf)
switch family {
case AF_INET:
if len(buf) < 8 {
return nil
}
port := int(buf[2])<<8 | int(buf[3])
sa := &syscall.SockaddrInet4{Port: port}
copy(sa.Addr[:], buf[4:8])
return sa
case AF_INET6:
if len(buf) < 28 {
return nil
}
port := int(buf[2])<<8 | int(buf[3])
sa := &syscall.SockaddrInet6{Port: port}
// flowinfo at [4:8], skip
copy(sa.Addr[:], buf[8:24])
// scope_id at [24:28]
sa.ZoneId = binary.LittleEndian.Uint32(buf[24:28])
return sa
case vfs.AF_UNIX:
path := extractUnixPath(buf)
return &syscall.SockaddrUnix{Name: path}
}
return nil
}
// writeSockaddr writes a Go Sockaddr back to guest memory.
func (h *Handler) writeSockaddr(addrPtr, addrLenPtr uint64, sa syscall.Sockaddr) {
switch a := sa.(type) {
case *syscall.SockaddrInet4:
var buf [16]byte
binary.LittleEndian.PutUint16(buf[0:], AF_INET)
buf[2] = byte(a.Port >> 8)
buf[3] = byte(a.Port)
copy(buf[4:8], a.Addr[:])
h.Mem.WriteBytes(addrPtr, buf[:])
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], 16)
h.Mem.WriteBytes(addrLenPtr, lenBuf[:])
case *syscall.SockaddrInet6:
var buf [28]byte
binary.LittleEndian.PutUint16(buf[0:], AF_INET6)
buf[2] = byte(a.Port >> 8)
buf[3] = byte(a.Port)
copy(buf[8:24], a.Addr[:])
binary.LittleEndian.PutUint32(buf[24:], a.ZoneId)
h.Mem.WriteBytes(addrPtr, buf[:])
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], 28)
h.Mem.WriteBytes(addrLenPtr, lenBuf[:])
case *syscall.SockaddrUnix:
var buf [110]byte
binary.LittleEndian.PutUint16(buf[0:], uint16(vfs.AF_UNIX))
copy(buf[2:], a.Name)
sz := uint32(2 + len(a.Name) + 1)
h.Mem.WriteBytes(addrPtr, buf[:sz])
var lenBuf [4]byte
binary.LittleEndian.PutUint32(lenBuf[:], sz)
h.Mem.WriteBytes(addrLenPtr, lenBuf[:])
}
}
// extractUnixPath extracts the path from a sockaddr_un buffer.
// Format: family(2 bytes LE) + path (up to 108 bytes).
// Handles both concrete paths (null-terminated) and Linux abstract sockets
// (first path byte is \0; the rest is the abstract name up to end of buf).
func extractUnixPath(buf []byte) string {
if len(buf) <= 2 {
return ""
}
pathBytes := buf[2:]
if len(pathBytes) == 0 {
return ""
}
if pathBytes[0] == 0 {
// Abstract socket: leading null is NOT a terminator.
// The name spans from buf[2] to end of the trimmed buffer.
return string(pathBytes)
}
for i, b := range pathBytes {
if b == 0 {
return string(pathBytes[:i])
}
}
return string(pathBytes)
}
// ===================================================================
// sendmsg / recvmsg with SCM_RIGHTS FD translation
// ===================================================================
// RISC-V 64-bit struct msghdr layout:
// msg_name 8 bytes (ptr)
// msg_namelen 4 bytes (uint32) + 4 pad
// msg_iov 8 bytes (ptr)
// msg_iovlen 8 bytes (size_t)
// msg_control 8 bytes (ptr)
// msg_controllen 8 bytes (size_t)
// msg_flags 4 bytes (int) + 4 pad
// Total: 56 bytes
func (h *Handler) sysSendmsg(c arch.CPU, fd, msgPtr, flags uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD < 0 {
// Try socket
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
hostFD = sock.HostFD()
if hostFD < 0 {
c.SetSyscallReturn(negErrno(ENOTCONN))
return
}
}
// Parse msghdr from guest memory
hdr := h.readBytes(msgPtr, 56)
if len(hdr) < 56 {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
iovPtr := binary.LittleEndian.Uint64(hdr[16:])
iovLen := binary.LittleEndian.Uint64(hdr[24:])
controlPtr := binary.LittleEndian.Uint64(hdr[32:])
controlLen := binary.LittleEndian.Uint64(hdr[40:])
// Gather iov data
var payload []byte
for i := uint64(0); i < iovLen; i++ {
iovEntry := h.readBytes(iovPtr+i*16, 16)
base := binary.LittleEndian.Uint64(iovEntry[0:])
blen := binary.LittleEndian.Uint64(iovEntry[8:])
if blen > 0 {
payload = append(payload, h.readBytes(base, blen)...)
}
}
// Process control messages - translate guest FDs to host FDs
var oob []byte
if controlLen > 0 && controlPtr != 0 {
guestCtrl := h.readBytes(controlPtr, controlLen)
cmsgs := ParseCmsgs(guestCtrl)
for _, cm := range cmsgs {
if cm.Level == 1 && cm.Type == vfs.SCM_RIGHTS { // SOL_SOCKET=1
// Translate guest FDs → host FDs
guestFDs := ExtractSCMRightsFDs(cm.Data)
hostFDs := make([]int, len(guestFDs))
for j, gfd := range guestFDs {
hfd := h.FS.GetHostFDNum(int(gfd))
if hfd < 0 {
c.SetSyscallReturn(negErrno(EBADF))
return
}
hostFDs[j] = hfd
}
oob = syscall.UnixRights(hostFDs...)
} else {
// Pass through non-SCM_RIGHTS control messages
oob = append(oob, BuildCmsg(cm.Level, cm.Type, cm.Data)...)
}
}
}
err := syscall.Sendmsg(hostFD, payload, oob, nil, int(flags))
if err != nil {
ravelog.Logf("[govm] sendmsg(fd=%d→hfd=%d, len=%d) err=%v\n", fd, hostFD, len(payload), err)
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
ravelog.Logf("[govm] sendmsg(fd=%d→hfd=%d, len=%d): %02x\n", fd, hostFD, len(payload), payload[:min(8, len(payload))])
c.SetSyscallReturn(uint64(len(payload)))
}
func (h *Handler) sysRecvmsg(c arch.CPU, fd, msgPtr, flags uint64) {
hostFD := h.FS.GetHostFDNum(int(fd))
if hostFD < 0 {
sock := h.FS.GetSocket(int(fd))
if sock == nil {
c.SetSyscallReturn(negErrno(ENOTSOCK))
return
}
hostFD = sock.HostFD()
if hostFD < 0 {
c.SetSyscallReturn(negErrno(ENOTCONN))
return
}
}
// Parse msghdr from guest memory
hdr := h.readBytes(msgPtr, 56)
if len(hdr) < 56 {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
iovPtr := binary.LittleEndian.Uint64(hdr[16:])
iovLen := binary.LittleEndian.Uint64(hdr[24:])
controlPtr := binary.LittleEndian.Uint64(hdr[32:])
controlLen := binary.LittleEndian.Uint64(hdr[40:])
// Calculate total iov capacity
var totalCap uint64
iovEntries := make([]struct{ base, blen uint64 }, iovLen)
for i := uint64(0); i < iovLen; i++ {
iovEntry := h.readBytes(iovPtr+i*16, 16)
iovEntries[i].base = binary.LittleEndian.Uint64(iovEntry[0:])
iovEntries[i].blen = binary.LittleEndian.Uint64(iovEntry[8:])
totalCap += iovEntries[i].blen
}
buf := make([]byte, totalCap)
oob := make([]byte, controlLen+256) // extra space for FDs
n, oobn, _, _, err := syscall.Recvmsg(hostFD, buf, oob, int(flags))
if err != nil {
if h.debugX11 {
ravelog.Logf("[govm] recvmsg(fd=%d→hfd=%d, len=%d) err=%v\n", fd, hostFD, totalCap, err)
}
c.SetSyscallReturn(negErrno(errnoFromErr(err)))
return
}
if h.debugX11 && n > 0 {
disp := buf[:n]
if len(disp) > 64 {
disp = disp[:64]
}
ravelog.Logf("[govm] recvmsg(fd=%d→hfd=%d, n=%d, iovs=%d): %x\n", fd, hostFD, n, iovLen, disp)
for i, iov := range iovEntries {
ravelog.Logf("[govm] iov[%d]: base=0x%x len=%d\n", i, iov.base, iov.blen)
}
}
// Scatter received data into guest iov
off := 0
for _, iov := range iovEntries {
if off >= n {
break
}
chunk := n - off
if uint64(chunk) > iov.blen {
chunk = int(iov.blen)
}
if h.debugX11 {
ravelog.Logf("[govm] scatter %d bytes at 0x%x: %x\n", chunk, iov.base, buf[off:off+chunk])
}
h.Mem.WriteBytes(iov.base, buf[off:off+chunk])
off += chunk
}
// Process received control messages - reverse-translate host FDs to guest FDs
if oobn > 0 && controlPtr != 0 && controlLen > 0 {
cmsgs := ParseCmsgs(oob[:oobn])
var guestCtrl []byte
for _, cm := range cmsgs {
if cm.Level == 1 && cm.Type == vfs.SCM_RIGHTS { // SOL_SOCKET=1
hostFDs := ExtractSCMRightsFDs(cm.Data)
guestFDs := make([]int32, len(hostFDs))
for j, hfd := range hostFDs {
// Wrap each received host FD as a VFS FD
gfd := h.FS.WrapHostFD(int(hfd), fmt.Sprintf("scm_fd_%d", hfd))
guestFDs[j] = int32(gfd)
}
guestCtrl = append(guestCtrl, BuildCmsg(cm.Level, cm.Type, PackSCMRightsFDs(guestFDs))...)
} else {
guestCtrl = append(guestCtrl, BuildCmsg(cm.Level, cm.Type, cm.Data)...)
}
}
if len(guestCtrl) > int(controlLen) {
guestCtrl = guestCtrl[:controlLen]
}
h.Mem.WriteBytes(controlPtr, guestCtrl)
// Update msg_controllen
binary.LittleEndian.PutUint64(hdr[40:], uint64(len(guestCtrl)))
h.Mem.WriteBytes(msgPtr+40, hdr[40:48])
}
c.SetSyscallReturn(uint64(n))
}
// errnoFromErr extracts a numeric errno from a Go error.
func errnoFromErr(err error) uint64 {
if err == nil {
return 0
}
if errno, ok := err.(syscall.Errno); ok {
return uint64(errno)
}
// os.File wraps syscall errors in *os.PathError or *os.LinkError
if pe, ok := err.(*os.PathError); ok {
return errnoFromErr(pe.Err)
}
if le, ok := err.(*os.LinkError); ok {
return errnoFromErr(le.Err)
}
return EINVAL
}
// ===================================================================
// Epoll syscalls
// ===================================================================
func (h *Handler) sysEpollCreate1(c arch.CPU, flags uint64) {
epfd := h.FS.AllocEpoll()
c.SetSyscallReturn(uint64(epfd))
}
func (h *Handler) sysEpollCtl(c arch.CPU, epfd, op, fd, eventPtr uint64) {
ep := h.FS.GetEpoll(int(epfd))
if ep == nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
var events uint32
var data uint64
if op != uint64(vfs.EPOLL_CTL_DEL) && eventPtr != 0 {
if c.GetArch() == arch.ArchAMD64 {
// On x86_64, struct epoll_event is packed (12 bytes)
evBuf := h.readBytes(eventPtr, 12)
if len(evBuf) >= 12 {
events = binary.LittleEndian.Uint32(evBuf[0:])
data = binary.LittleEndian.Uint64(evBuf[4:])
}
} else {
// On others (like riscv64), it's 16 bytes
evBuf := h.readBytes(eventPtr, 16)
if len(evBuf) >= 16 {
events = binary.LittleEndian.Uint32(evBuf[0:])
data = binary.LittleEndian.Uint64(evBuf[8:])
}
}
}
if err := ep.Ctl(int(op), int(fd), events, data); err != nil {
c.SetSyscallReturn(negErrno(ENOENT))
return
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysEpollPwait(c arch.CPU, epfd, eventsPtr, maxevents, timeout uint64) {
ep := h.FS.GetEpoll(int(epfd))
if ep == nil {
c.SetSyscallReturn(negErrno(EBADF))
return
}
entries := ep.Entries()
if len(entries) == 0 {
// No FDs monitored - respect timeout
if timeout > 0 && timeout != ^uint64(0) {
time.Sleep(time.Duration(timeout) * time.Millisecond)
}
c.SetSyscallReturn(0)
return
}
// Check eventfds and pipes first (VFS-only FDs without host backing)
nReady := uint64(0)
for _, e := range entries {
if nReady >= maxevents {
break
}
efd := h.FS.GetEventFD(e.GuestFD)
if efd != nil && efd.Ready() {
var events uint32
if e.Events&vfs.EPOLLIN != 0 {
events |= vfs.EPOLLIN
}
// eventfd is always writable
if e.Events&vfs.EPOLLOUT != 0 {
events |= vfs.EPOLLOUT
}
if events != 0 {
if c.GetArch() == arch.ArchAMD64 {
var evBuf [12]byte
binary.LittleEndian.PutUint32(evBuf[0:], events)
binary.LittleEndian.PutUint64(evBuf[4:], e.Data)
h.Mem.WriteBytes(eventsPtr+nReady*12, evBuf[:])
} else {
var evBuf [16]byte
binary.LittleEndian.PutUint32(evBuf[0:], events)
binary.LittleEndian.PutUint64(evBuf[8:], e.Data)
h.Mem.WriteBytes(eventsPtr+nReady*16, evBuf[:])
}
nReady++
}
}
}
if nReady > 0 {
c.SetSyscallReturn(nReady)
return
}
// Build poll set from monitored entries that have host FDs
type pollEntry struct {
entry *vfs.EpollEntry
hostFD int
}
var polls []pollEntry
var pollFDs []unix.PollFd
for _, e := range entries {
hfd := h.FS.GetHostFDNum(e.GuestFD)
if hfd >= 0 {
var pEvents int16
if e.Events&vfs.EPOLLIN != 0 {
pEvents |= unix.POLLIN
}
if e.Events&vfs.EPOLLOUT != 0 {
pEvents |= unix.POLLOUT
}
if e.Events&vfs.EPOLLRDHUP != 0 {
pEvents |= unix.POLLRDHUP
}
pollFDs = append(pollFDs, unix.PollFd{
Fd: int32(hfd),
Events: pEvents,
})
polls = append(polls, pollEntry{entry: e, hostFD: hfd})
}
}
if len(pollFDs) == 0 {
if timeout > 0 && timeout != ^uint64(0) {
time.Sleep(time.Duration(timeout) * time.Millisecond)
}
c.SetSyscallReturn(0)
return
}
// Call host poll
timeoutMs := int(timeout)
if timeout == ^uint64(0) {
timeoutMs = -1
}
_, _ = unix.Poll(pollFDs, timeoutMs)
// Translate results back into guest epoll_event structs
for i, pfd := range pollFDs {
if pfd.Revents == 0 || nReady >= maxevents {
continue
}
var events uint32
if pfd.Revents&unix.POLLIN != 0 {
events |= vfs.EPOLLIN
}
if pfd.Revents&unix.POLLOUT != 0 {
events |= vfs.EPOLLOUT
}
if pfd.Revents&unix.POLLERR != 0 {
events |= vfs.EPOLLERR
}
if pfd.Revents&unix.POLLHUP != 0 {
events |= vfs.EPOLLHUP
}
if pfd.Revents&unix.POLLRDHUP != 0 {
events |= vfs.EPOLLRDHUP
}
// struct epoll_event layout is arch-dependent:
// x86-64: packed { uint32 events; uint64 data } = 12 bytes, no padding
// others: { uint32 events; pad[4]; uint64 data } = 16 bytes
if c.GetArch() == arch.ArchAMD64 {
var evBuf [12]byte
binary.LittleEndian.PutUint32(evBuf[0:], events)
binary.LittleEndian.PutUint64(evBuf[4:], polls[i].entry.Data)
h.Mem.WriteBytes(eventsPtr+nReady*12, evBuf[:])
} else {
var evBuf [16]byte
binary.LittleEndian.PutUint32(evBuf[0:], events)
binary.LittleEndian.PutUint64(evBuf[8:], polls[i].entry.Data)
h.Mem.WriteBytes(eventsPtr+nReady*16, evBuf[:])
}
nReady++
}
c.SetSyscallReturn(nReady)
}
// ===================================================================
// eventfd2
// ===================================================================
func (h *Handler) sysEventfd2(c arch.CPU, initval, flags uint64) {
hostFD, err := unix.Eventfd(uint(initval), int(flags))
if err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
f := os.NewFile(uintptr(hostFD), "eventfd")
gfd := h.FS.AllocHostFile(f)
c.SetSyscallReturn(uint64(gfd))
}
// ===================================================================
// timerfd
// ===================================================================
func (h *Handler) sysTimerfdCreate(c arch.CPU, clockid, flags uint64) {
hostFD, err := unix.TimerfdCreate(int(clockid), int(flags))
if err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
f := os.NewFile(uintptr(hostFD), "timerfd")
gfd := h.FS.AllocHostFile(f)
c.SetSyscallReturn(uint64(gfd))
}
func (h *Handler) sysTimerfdSettime(c arch.CPU, fdNum, flags, newValue, oldValue uint64) {
hostFD := h.FS.GetHostFDNum(int(fdNum))
if hostFD < 0 {
c.SetSyscallReturn(negErrno(EBADF))
return
}
// Read itimerspec from guest (two timespec = 32 bytes)
buf := h.readBytes(newValue, 32)
if len(buf) < 32 {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
var newSpec unix.ItimerSpec
newSpec.Interval.Sec = int64(binary.LittleEndian.Uint64(buf[0:]))
newSpec.Interval.Nsec = int64(binary.LittleEndian.Uint64(buf[8:]))
newSpec.Value.Sec = int64(binary.LittleEndian.Uint64(buf[16:]))
newSpec.Value.Nsec = int64(binary.LittleEndian.Uint64(buf[24:]))
var oldSpec unix.ItimerSpec
err := unix.TimerfdSettime(hostFD, int(flags), &newSpec, &oldSpec)
if err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
if oldValue != 0 {
var out [32]byte
binary.LittleEndian.PutUint64(out[0:], uint64(oldSpec.Interval.Sec))
binary.LittleEndian.PutUint64(out[8:], uint64(oldSpec.Interval.Nsec))
binary.LittleEndian.PutUint64(out[16:], uint64(oldSpec.Value.Sec))
binary.LittleEndian.PutUint64(out[24:], uint64(oldSpec.Value.Nsec))
h.Mem.WriteBytes(oldValue, out[:])
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysTimerfdGettime(c arch.CPU, fdNum, currValue uint64) {
hostFD := h.FS.GetHostFDNum(int(fdNum))
if hostFD < 0 {
c.SetSyscallReturn(negErrno(EBADF))
return
}
var spec unix.ItimerSpec
err := unix.TimerfdGettime(hostFD, &spec)
if err != nil {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
var out [32]byte
binary.LittleEndian.PutUint64(out[0:], uint64(spec.Interval.Sec))
binary.LittleEndian.PutUint64(out[8:], uint64(spec.Interval.Nsec))
binary.LittleEndian.PutUint64(out[16:], uint64(spec.Value.Sec))
binary.LittleEndian.PutUint64(out[24:], uint64(spec.Value.Nsec))
h.Mem.WriteBytes(currValue, out[:])
c.SetSyscallReturn(0)
}
// ===================================================================
// memfd_create + fd-backed mmap
// ===================================================================
func (h *Handler) sysMemfdCreate(c arch.CPU, namePtr, flags uint64) {
name, err := h.readGuestString(namePtr)
if err != nil {
c.SetSyscallReturn(negErrno(EFAULT))
return
}
if name == "" {
name = "memfd"
}
hostFD, err := unix.MemfdCreate(name, int(flags))
if err != nil {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
f := os.NewFile(uintptr(hostFD), name)
gfd := h.FS.AllocHostFile(f)
c.SetSyscallReturn(uint64(gfd))
}
// Ensure unused imports are silenced
var _ = unsafe.Sizeof(0)
// sysMmap handles mmap with support for fd-backed mappings.
func (h *Handler) sysMmap(c arch.CPU) {
args := c.SyscallArgs()
addr := args[0]
length := args[1]
prot := args[2]
flags := args[3]
fd := args[4]
offset := args[5]
if length == 0 {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
// Reject unreasonably large mappings that would cause integer overflow.
// Real Linux imposes a per-process memory limit; we conservatively cap at 1TB.
// The value 0x8000000000001000 is a known bad size from ld.so reservation arithmetic.
if length > 0x10000000000 { // > 1TB
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
origAddr := addr
logMmap := func(result uint64) {
if h.Trace {
ravelog.Logf("[mmap] req=0x%x len=0x%x prot=%d flags=0x%x fd=%d off=0x%x → 0x%x\n",
origAddr, length, prot, flags, int64(fd), offset, result)
}
}
var perms uint8
if prot&0x1 != 0 {
perms |= mmu.PermRead
}
if prot&0x2 != 0 {
perms |= mmu.PermWrite
}
if prot&0x4 != 0 {
perms |= mmu.PermExecute
}
mapShared := flags&0x01 != 0 // MAP_SHARED
mapAnon := flags&0x20 != 0 // MAP_ANONYMOUS
mapFixed := flags&0x10 != 0 // MAP_FIXED
if mapFixed && addr&mmu.PageMask != addr {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
// If MAP_ANONYMOUS or fd == -1, use normal anonymous mmap
if mapAnon || fd == ^uint64(0) || int64(fd) == -1 {
if mapFixed && addr != 0 {
h.Mem.MapRangeZero(addr, length, perms)
logMmap(addr)
c.SetSyscallReturn(addr)
} else {
result := h.Mem.Mmap(addr, length, perms)
logMmap(result)
c.SetSyscallReturn(result)
}
return
}
// fd-backed mmap: check if the FD has a real host file
hostFile := h.FS.GetHostFile(int(fd))
if hostFile == nil {
// VFS-only file - allocate anonymous pages and copy data
if mapFixed && addr != 0 {
h.Mem.MapRangeZero(addr, length, perms)
} else {
addr = h.Mem.Mmap(addr, length, perms)
}
// Read file data into the mapped region without advancing offset
buf := make([]byte, length)
n, _ := h.FS.Pread(int(fd), buf, int64(offset))
if n > 0 {
h.Mem.WriteBytesRaw(addr, buf[:n])
}
logMmap(addr)
c.SetSyscallReturn(addr)
return
}
// Real host file (memfd or host passthrough)
_ = offset
if !mapShared {
// MAP_PRIVATE: just allocate memory and read the file. This avoids SIGBUS
// if the requested length exceeds the file size (common in ELF loaders).
if mapFixed && addr != 0 {
h.Mem.MapRangeZero(addr, length, perms)
} else {
addr = h.Mem.Mmap(addr, length, perms)
// Mmap for anonymous already creates fresh (zeroed) pages if they are new,
// but just to be absolutely sure for file-backed we could zero it.
// Actually, Mmap on an unmapped region creates zeroed pages, so it's fine.
}
// Read data at offset
buf := make([]byte, length)
n, _ := hostFile.ReadAt(buf, int64(offset))
if n > 0 {
// Bypass permission checks because the requested perms might be PROT_READ
h.Mem.WriteBytesRaw(addr, buf[:n])
}
logMmap(addr)
c.SetSyscallReturn(addr)
return
}
// MAP_SHARED (like Wayland shm or memfd): mmap for zero-copy
hostFDnum := int(hostFile.Fd())
mmapProt := 0
if prot&0x1 != 0 {
mmapProt |= syscall.PROT_READ
}
if prot&0x2 != 0 {
mmapProt |= syscall.PROT_WRITE
}
mmapFlags := syscall.MAP_SHARED
if addr != 0 && !mapFixed {
// Check if hint address is available
conflict := false
for pg := addr & mmu.PageMask; pg < addr+length; pg += mmu.PageSize {
if h.Mem.IsMapped(pg) {
conflict = true
break
}
}
if conflict {
addr = 0
}
}
hostSlice, err := syscall.Mmap(hostFDnum, int64(offset), int(length), mmapProt, mmapFlags)
if err != nil {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
// Alias the host mmap'd region into the guest MMU
guestAddr := h.Mem.MapHostSlice(addr, hostSlice, perms)
logMmap(guestAddr)
c.SetSyscallReturn(guestAddr)
}
func (h *Handler) sysMadvise(c arch.CPU, addr, length, advice uint64) {
// MADV_DONTNEED = 4
if advice == 4 {
h.Mem.MapRangeZero(addr, length, mmu.PermRead|mmu.PermWrite) // should keep current perms, but this is a stub
}
c.SetSyscallReturn(0)
}
func (h *Handler) sysMremap(c arch.CPU, oldAddr, oldLen, newLen, flags, newAddr uint64) {
// flags: 1 = MREMAP_MAYMOVE, 2 = MREMAP_FIXED, 3 = both
mayMove := flags&1 != 0
isFixed := flags&2 != 0
oldLen = (oldLen + mmu.PageSize - 1) & mmu.PageMask
newLen = (newLen + mmu.PageSize - 1) & mmu.PageMask
if newLen <= oldLen {
if oldLen > newLen {
h.Mem.Munmap(oldAddr+newLen, oldLen-newLen)
}
if isFixed {
// Move to newAddr
data, _ := h.Mem.ReadBytesRaw(oldAddr, int(newLen))
h.Mem.MapRangeZero(newAddr, newLen, mmu.PermRead|mmu.PermWrite|mmu.PermExecute) // simplified perms
h.Mem.WriteBytesRaw(newAddr, data)
if oldAddr != newAddr {
h.Mem.Munmap(oldAddr, oldLen)
}
c.SetSyscallReturn(newAddr)
return
}
c.SetSyscallReturn(oldAddr)
return
}
if isFixed {
// MREMAP_FIXED requires MREMAP_MAYMOVE
if !mayMove {
c.SetSyscallReturn(negErrno(EINVAL))
return
}
data, _ := h.Mem.ReadBytesRaw(oldAddr, int(oldLen))
h.Mem.MapRangeZero(newAddr, newLen, mmu.PermRead|mmu.PermWrite|mmu.PermExecute)
h.Mem.WriteBytesRaw(newAddr, data)
if oldAddr != newAddr {
h.Mem.Munmap(oldAddr, oldLen)
}
c.SetSyscallReturn(newAddr)
return
}
// Try expanding in place if the following pages are unmapped
canExpand := true
for pg := oldAddr + oldLen; pg < oldAddr+newLen; pg += mmu.PageSize {
if h.Mem.IsMapped(pg) {
canExpand = false
break
}
}
if canExpand {
h.Mem.MapRangeZero(oldAddr+oldLen, newLen-oldLen, mmu.PermRead|mmu.PermWrite|mmu.PermExecute)
c.SetSyscallReturn(oldAddr)
return
}
if !mayMove {
c.SetSyscallReturn(negErrno(ENOMEM))
return
}
// Move the allocation
targetAddr := h.Mem.Mmap(0, newLen, mmu.PermRead|mmu.PermWrite|mmu.PermExecute)
data, _ := h.Mem.ReadBytesRaw(oldAddr, int(oldLen))
h.Mem.WriteBytesRaw(targetAddr, data)
h.Mem.Munmap(oldAddr, oldLen)
c.SetSyscallReturn(targetAddr)
}
// x64SyscallToRV translates an x86_64 Linux syscall number to the RISC-V
// (asm-generic) number used by the handler's switch statement.
func x64SyscallToRV(num uint64) uint64 {
if n, ok := x64Map[num]; ok {
return n
}
return num | 0x80000000 // will hit default/ENOSYS
}
var x64Map = map[uint64]uint64{
0: SysRead,
1: SysWrite,
2: sysX64Open, // open → openat (needs AT_FDCWD)
3: SysClose,
4: sysX64Stat, // stat → fstatat (needs AT_FDCWD)
5: SysFstat,
6: sysX64Lstat, // lstat → fstatat (needs AT_FDCWD)
7: sysPoll, // poll(fds, nfds, timeout_ms) - timeout is int, not *timespec
8: SysLseek,
9: SysMmap,
10: SysMprotect,
11: SysMunmap,
12: SysBrk,
13: SysRtSigaction,
14: SysRtSigprocmask,
15: SysRtSigreturn,
16: SysIoctl,
17: SysPread64, // pread64
18: SysPwrite64, // pwrite64
19: SysReadv,
20: SysWritev,
21: sysX64Access, // access → faccessat (needs AT_FDCWD)
22: SysPipe2, // pipe → pipe2
23: SysPselect6, // select → pselect6
24: SysSchedYield,
25: SysMremap,
26: SysMsync,
27: SysMincore,
28: SysMadvise,
32: SysDup,
33: SysDup3, // dup2 → dup3
35: SysNanosleep,
36: SysGetitimer,
38: SysSetitimer,
39: SysGetpid,
40: SysSendfile,
41: SysSocket,
42: SysConnect,
43: SysAccept,
44: SysSendto,
45: SysRecvfrom,
46: SysSendmsg,
47: SysRecvmsg,
48: SysShutdown,
49: SysBind,
50: SysListen,
51: SysGetsockname,
52: SysGetpeername,
53: SysSocketpair,
54: SysSetsockopt,
55: SysGetsockopt,
56: SysClone,
59: SysExecve,
60: SysExit,
61: SysWait4,
62: SysKill,
63: SysUname,
72: SysFcntl,
73: SysFlock, // flock → stub
74: SysFsync,
75: SysFdatasync,
76: SysFtruncate, // truncate → stub
77: SysFtruncate,
78: SysGetdents64,
79: SysGetcwd,
80: SysChdir,
82: sysX64Rename, // rename → renameat2 (needs 2x AT_FDCWD)
83: sysX64Mkdir, // mkdir → mkdirat (needs AT_FDCWD)
87: sysX64Unlink, // unlink → unlinkat (needs AT_FDCWD)
88: sysX64Symlink, // symlink → symlinkat (needs AT_FDCWD)
89: sysX64Readlink, // readlink → readlinkat (needs AT_FDCWD)
90: SysFchmod, // chmod → fchmod
91: SysFchown, // fchown
95: SysUmask,
96: SysTimes,
97: SysGetrusage, // getrlimit → stub
98: SysGetrusage,
99: SysSysinfo,
100: SysTimes,
102: SysGetuid,
103: SysSyslog,
104: SysGetgid,
105: SysSetTidAddress, // setuid → stub
106: sysX64Stat, // stat → fstatat (needs AT_FDCWD)
107: SysGeteuid,
108: SysGetegid,
118: SysGetresuid,
120: SysGetresgid,
110: SysGetppid,
131: SysSigaltstack,
137: SysStatx, // statfs → stub
155: SysPrctl, // pivot_root → stub
157: SysPrctl,
158: SysArchPrctl,
160: SysSchedSetaffinity, // setrlimit → stub
186: SysGettid,
201: SysClockGettime, // time → clock_gettime stub
202: SysFutex,
203: SysSchedSetaffinity,
204: SysSchedGetaffinity,
206: SysStatx, // io_setup → stub
217: SysGetdents64,
218: SysSetTidAddress,
228: SysClockGettime,
229: SysClockGetres,
230: SysClockNanosleep,
231: SysExitGroup,
232: SysEpollPwait,
233: SysEpollCtl,
234: SysTgkill,
257: SysOpenat,
258: SysMkdirat, // mkdirat
260: SysFchownat,
262: SysFstatat, // newfstatat
263: SysUnlinkat,
264: SysRenameat2,
265: SysLinkat,
266: SysSymlinkat,
267: SysReadlinkat,
268: SysFchmodat,
269: SysFaccessat,
270: SysPselect6,
271: SysPpoll,
276: SysSplice, // splice → will hit default
279: SysStatx, // move_pages → stub
281: SysEpollPwait, // epoll_pwait
282: SysEpollCreate1, // via 291
283: SysTimerfdCreate, // timerfd_create (obsolete, see 322)
284: SysEventfd2, // eventfd (old), treat as eventfd2
285: SysFallocate,
286: SysTimerfdSettime,
287: SysTimerfdGettime,
288: SysAccept4,
289: SysPipe2, // signalfd4 → pipe2 stub
290: SysEventfd2,
291: SysEpollCreate1,
273: SysSetRobustList,
292: SysDup3,
293: SysPipe2, // pipe2
302: SysPrlimit64,
303: SysMemfdCreate,
310: SysSetRobustList, // process_vm_readv → stub
318: SysGetrandom,
316: SysRenameat2,
253: SysKcmp, // kcmp - compare kernel resources of two processes
319: SysMemfdCreate,
320: SysMembarrier, // kexec_file_load → stub
334: SysRseq,
322: SysTimerfdCreate, // timerfd_create (new number)
332: SysStatx, // statx
435: SysClone3, // clone3
}
// SysFlock/SysSplice: handled as unknown → ENOSYS
const SysFlock = 0xDEAD0001
const SysSplice = 0xDEAD0002