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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 72 additions & 32 deletions src/runtime/forkipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@

#include "syscall/linux-wire.h"
#include "syscall/chown-overlay.h"
#include "syscall/fs.h"
#include "syscall/internal.h"
#include "syscall/mem.h"
#include "syscall/net.h" /* absock namespace IPC state */
Expand All @@ -83,11 +84,13 @@
* sys_clone further down.
*/
#define LINUX_CLONE_VM 0x00000100
#define LINUX_CLONE_PIDFD 0x00001000
#define LINUX_CLONE_VFORK 0x00004000
#define LINUX_CLONE_THREAD 0x00010000
#define LINUX_CLONE_SETTLS 0x00080000
#define LINUX_CLONE_PARENT_SETTID 0x00100000
#define LINUX_CLONE_CHILD_CLEARTID 0x00200000
#define LINUX_CLONE_DETACHED 0x00400000
#define LINUX_CLONE_CHILD_SETTID 0x01000000
/* LINUX_SIGCHLD defined in syscall_signal.h (included above) */

Expand Down Expand Up @@ -1162,13 +1165,33 @@ static void *thread_create_and_run(void *arg)
*/
static void *vm_clone_thread_run(void *arg);

/* Publish the pidfd before the child can run. Copyout failure must cancel
* creation, as in Linux copy_process().
*/
static int clone_create_pidfd(guest_t *g,
int64_t child_pid,
pid_t host_pid,
uint64_t pidfd_gva)
{
int fd = pidfd_create(g, child_pid, host_pid);
if (fd < 0)
return fd;
int32_t value = fd;
if (guest_write_small(g, pidfd_gva, &value, sizeof(value)) < 0) {
sys_close(fd);
return -LINUX_EFAULT;
}
return fd;
}

static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu,
guest_t *g,
uint64_t flags,
uint64_t child_stack,
uint64_t stack_map_start,
uint64_t stack_map_end,
uint64_t ptid_gva,
uint64_t pidfd_gva,
uint64_t tls,
uint64_t ctid_gva,
bool verbose)
Expand Down Expand Up @@ -1233,6 +1256,17 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu,
return -LINUX_EFAULT;
}

int child_pidfd = -1;
if (flags & LINUX_CLONE_PIDFD) {
child_pidfd = clone_create_pidfd(g, child_tid, -1, pidfd_gva);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: CLONE_PIDFD with CLONE_VM now creates a pidfd that can never become readable. pidfd_create(g, child_tid, -1, ...) takes the host_pid <= 0 branch in proc-pidfd.c, which arms no exit monitor, and nothing calls proc_pidfd_notify_exit when a VM-clone child is reaped (its exit goes through thread_ptrace_wait, not any proc.c path). A guest blocking on this pidfd hangs forever, even after the child exits. Linux supports pidfds for CLONE_VM non-thread children; since the emulator cannot watch in-process children, reject CLONE_PIDFD with -EINVAL for the CLONE_VM path (like threads) or complete the pidfd when the VM-clone child is reaped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/runtime/forkipc.c, line 1261:

<comment>`CLONE_PIDFD` with `CLONE_VM` now creates a pidfd that can never become readable. `pidfd_create(g, child_tid, -1, ...)` takes the `host_pid <= 0` branch in proc-pidfd.c, which arms no exit monitor, and nothing calls `proc_pidfd_notify_exit` when a VM-clone child is reaped (its exit goes through `thread_ptrace_wait`, not any proc.c path). A guest blocking on this pidfd hangs forever, even after the child exits. Linux supports pidfds for `CLONE_VM` non-thread children; since the emulator cannot watch in-process children, reject `CLONE_PIDFD` with `-EINVAL` for the `CLONE_VM` path (like threads) or complete the pidfd when the VM-clone child is reaped.</comment>

<file context>
@@ -1235,6 +1256,17 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu,
 
+    int child_pidfd = -1;
+    if (flags & LINUX_CLONE_PIDFD) {
+        child_pidfd = clone_create_pidfd(g, child_tid, -1, pidfd_gva);
+        if (child_pidfd < 0) {
+            clone_rollback_tid_flags(g, &tid_rollback);
</file context>

if (child_pidfd < 0) {
clone_rollback_tid_flags(g, &tid_rollback);
free(tca);
thread_deactivate(t);
return child_pidfd;
}
}

/* Create the host pthread */
pthread_t host_thread;
pthread_attr_t attr;
Expand All @@ -1244,6 +1278,8 @@ static int64_t sys_clone_vm(hv_vcpu_t parent_vcpu,

if (err != 0) {
log_error("clone_vm: pthread_create failed: %s", strerror(err));
if (child_pidfd >= 0)
sys_close(child_pidfd);
free(tca);
thread_deactivate(t);

Expand Down Expand Up @@ -1516,6 +1552,7 @@ int64_t sys_clone(hv_vcpu_t vcpu,
uint64_t stack_map_start,
uint64_t stack_map_end,
uint64_t ptid_gva,
uint64_t pidfd_gva,
uint64_t tls,
uint64_t ctid_gva,
bool verbose)
Expand All @@ -1528,6 +1565,16 @@ int64_t sys_clone(hv_vcpu_t vcpu,
if ((flags & ~(uint64_t) 0xff) & LINUX_CLONE3_NS_FLAGS)
return -LINUX_EINVAL;

bool want_pidfd = (flags & LINUX_CLONE_PIDFD) != 0;
/* clone shares the parent_tid and pidfd argument; clone3 keeps them
* separate. Linux rejects aliased outputs and CLONE_DETACHED with PIDFD.
* Thread pidfds are unsupported here.
*/
if (want_pidfd &&
((flags & (LINUX_CLONE_THREAD | LINUX_CLONE_DETACHED)) ||
((flags & LINUX_CLONE_PARENT_SETTID) && pidfd_gva == ptid_gva)))
return -LINUX_EINVAL;

/* CLONE_THREAD: create a new thread in the same VM (not a new process) */
if (flags & LINUX_CLONE_THREAD) {
return sys_clone_thread(vcpu, g, flags, child_stack, stack_map_start,
Expand Down Expand Up @@ -1558,7 +1605,8 @@ int64_t sys_clone(hv_vcpu_t vcpu,
if ((flags & LINUX_CLONE_VM) && !(flags & LINUX_CLONE_THREAD) &&
!is_vfork) {
return sys_clone_vm(vcpu, g, flags, child_stack, stack_map_start,
stack_map_end, ptid_gva, tls, ctid_gva, verbose);
stack_map_end, ptid_gva, pidfd_gva, tls, ctid_gva,
verbose);
}

log_debug("clone(flags=0x%llx, vfork=%d)", (unsigned long long) flags,
Expand Down Expand Up @@ -1738,6 +1786,7 @@ int64_t sys_clone(hv_vcpu_t vcpu,
int snapshot_shm_fd = -1;
bool siblings_quiesced = false;
int64_t fail_rc = -LINUX_ENOMEM;
int clone_pidfd = -1;

/* Quiesce sibling vCPUs for snapshot consistency. In multithreaded guests,
* sibling vCPUs may be actively mutating guest memory during the fork
Expand Down Expand Up @@ -2003,6 +2052,21 @@ int64_t sys_clone(hv_vcpu_t vcpu,
log_error("clone: failed to commit child bookkeeping");
goto fail_snapshot;
}
/* Publish the parent's pidfd after the child fd snapshot, but before the
* child can run. A failed allocation or copyout cancels the held child.
*/
if (want_pidfd) {
clone_pidfd =
clone_create_pidfd(g, child_guest_pid, child_host_pid, pidfd_gva);
if (clone_pidfd < 0) {
fail_rc = clone_pidfd;
goto fail_snapshot;
}
}
if (flags & LINUX_CLONE_PARENT_SETTID) {
int32_t tid = (int32_t) child_guest_pid;
(void) guest_write_small(g, ptid_gva, &tid, sizeof(tid));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A failed CLONE_PARENT_SETTID copyout is ignored, so clone releases the child and reports success without writing its TID. Check the copyout and unwind with -LINUX_EFAULT before admitting the child.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/runtime/forkipc.c, line 2068:

<comment>A failed `CLONE_PARENT_SETTID` copyout is ignored, so clone releases the child and reports success without writing its TID. Check the copyout and unwind with `-LINUX_EFAULT` before admitting the child.</comment>

<file context>
@@ -2021,17 +2056,16 @@ int64_t sys_clone(hv_vcpu_t vcpu,
+    }
+    if (flags & LINUX_CLONE_PARENT_SETTID) {
+        int32_t tid = (int32_t) child_guest_pid;
+        (void) guest_write_small(g, ptid_gva, &tid, sizeof(tid));
     }
     uint8_t admission_ready = 1;
</file context>
Suggested change
(void) guest_write_small(g, ptid_gva, &tid, sizeof(tid));
if (guest_write_small(g, ptid_gva, &tid, sizeof(tid)) < 0) {
fail_rc = -LINUX_EFAULT;
goto fail_snapshot;
}

}
uint8_t admission_ready = 1;
if (fork_ipc_write_all(ipc_sock, &admission_ready,
sizeof(admission_ready)) < 0) {
Expand Down Expand Up @@ -2075,6 +2139,8 @@ int64_t sys_clone(hv_vcpu_t vcpu,
return child_guest_pid;

fail_snapshot:
if (clone_pidfd >= 0)
sys_close(clone_pidfd);
proc_cancel_child(child_guest_pid);
free(regions_snapshot);
if (snapshot_shm_fd >= 0)
Expand Down Expand Up @@ -2134,7 +2200,6 @@ struct linux_clone_args {
#define CLONE_ARGS_SIZE_VER0 64 /* v5.3: first 8 fields (flags..tls) */

/* Unsupported clone3-only flags: reject early rather than silently ignoring. */
#define LINUX_CLONE_PIDFD 0x00001000
#define LINUX_CLONE_INTO_CGROUP 0x200000000ULL

int64_t sys_clone3(hv_vcpu_t vcpu,
Expand Down Expand Up @@ -2204,13 +2269,9 @@ int64_t sys_clone3(hv_vcpu_t vcpu,

/* Merge exit_signal into flags for sys_clone compatibility. clone3 moved
* exit_signal out of the flags field; sys_clone expects it in the low byte.
* Safe because validation confirmed ca.flags low byte is zero. Strip
* CLONE_PIDFD before passing to sys_clone (which does not understand it).
* Pidfd creation happens after the clone returns.
* Validation confirmed ca.flags low byte is zero.
*/
bool want_pidfd = (ca.flags & LINUX_CLONE_PIDFD) != 0;
uint64_t flags =
(ca.flags & ~(uint64_t) LINUX_CLONE_PIDFD) | ca.exit_signal;
uint64_t flags = ca.flags | ca.exit_signal;

/* Compute child stack pointer. clone: child_stack is the TOP of the stack
* (SP value). clone3: stack is the BOTTOM, stack_size is the length. SP =
Expand All @@ -2231,28 +2292,7 @@ int64_t sys_clone3(hv_vcpu_t vcpu,
(unsigned long long) ca.stack, (unsigned long long) ca.stack_size,
(unsigned long long) ca.tls, (unsigned long long) cl_args_size);

int64_t ret = sys_clone(vcpu, g, flags, child_stack, ca.stack,
ca.stack + ca.stack_size, ca.parent_tid, ca.tls,
ca.child_tid, verbose);

/* If clone succeeded and CLONE_PIDFD was requested, create a pidfd for the
* child and write the guest FD number to ca.pidfd.
*/
if (ret > 0 && want_pidfd && ca.pidfd != 0) {
/* A CLONE_VM child has no host pid of its own, so it resolves to -1 and
* its pidfd carries no monitor.
*/
int pfd = pidfd_create(g, ret, proc_resolve_guest_pid(ret));
if (pfd >= 0) {
int32_t pfd32 = (int32_t) pfd;
if (guest_write_small(g, ca.pidfd, &pfd32, sizeof(pfd32)) < 0) {
/* GVA invalid; close the newly created pidfd. */
fd_entry_t snap;
if (fd_snapshot_and_close(pfd, &snap))
fd_cleanup_entry(pfd, &snap);
}
}
}

return ret;
return sys_clone(vcpu, g, flags, child_stack, ca.stack,
ca.stack + ca.stack_size, ca.parent_tid, ca.pidfd, ca.tls,
ca.child_tid, verbose);
}
1 change: 1 addition & 0 deletions src/runtime/forkipc.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ int64_t sys_clone(hv_vcpu_t vcpu,
uint64_t stack_map_start,
uint64_t stack_map_end,
uint64_t ptid_gva,
uint64_t pidfd_gva,
uint64_t tls,
uint64_t ctid_gva,
bool verbose);
Expand Down
5 changes: 5 additions & 0 deletions src/syscall/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
#define SYS_getitimer 102
#define SYS_setitimer 103
#define SYS_nanosleep 101
#define SYS_timer_create 107
#define SYS_timer_gettime 108
#define SYS_timer_getoverrun 109
#define SYS_timer_settime 110
#define SYS_timer_delete 111
#define SYS_clock_gettime 113
#define SYS_clock_getres 114
#define SYS_clock_nanosleep 115
Expand Down
5 changes: 5 additions & 0 deletions src/syscall/dispatch.tbl
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ SYS_rt_tgsigqueueinfo sc_rt_tgsigqueueinfo 1
SYS_nanosleep sc_nanosleep 1
SYS_getitimer sc_getitimer 1
SYS_setitimer sc_setitimer 1
SYS_timer_create sc_timer_create 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the five timer_* entries has a test reference under tests/, and none is in INDIRECT_COVERAGE, so scripts/check-syscall-coverage.py fails make check. A small C test covering create/settime/gettime/getoverrun/delete, TIMER_ABSTIME, a nonzero overrun and SIGEV_NONE would clear the gate. It should also reject an out-of-range signal number. The CLONE_PIDFD path and its failure rollback are untested as well.

SYS_timer_gettime sc_timer_gettime 0
SYS_timer_getoverrun sc_timer_getoverrun 0
SYS_timer_settime sc_timer_settime 1
SYS_timer_delete sc_timer_delete 0
SYS_clock_gettime sc_clock_gettime 0
SYS_clock_getres sc_clock_getres 0
SYS_clock_nanosleep sc_clock_nanosleep 1
Expand Down
22 changes: 7 additions & 15 deletions src/syscall/fd.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,12 @@ static int sfd_alloc_slot(const void *state, size_t count, size_t stride)
* timerfd return an 8-byte counter of expirations.
*/

/* Linux itimerspec for timerfd_settime/gettime */
typedef struct {
int64_t it_interval_sec, it_interval_nsec, it_value_sec, it_value_nsec;
} linux_itimerspec_t;

/* Per-timerfd state (stored alongside the FD_TIMERFD entry) */
#define TIMERFD_MAX 32
#define LINUX_TFD_NONBLOCK LINUX_O_NONBLOCK
#define LINUX_TFD_CLOEXEC LINUX_O_CLOEXEC
#define LINUX_TFD_TIMER_ABSTIME 1
#define LINUX_TFD_TIMER_CANCEL_ON_SET 2
#define LINUX_CLOCK_REALTIME 0
#define LINUX_CLOCK_MONOTONIC 1

/* Linux CLOCK_BOOTTIME counts time including suspend; macOS has no equivalent.
* timerfd_settime treats non-REALTIME slots as MONOTONIC for ABSTIME
* conversion, which matches translate_clockid() in time.c.
*/
#define LINUX_CLOCK_BOOTTIME 7

static struct {
int guest_fd; /* Guest fd (-1 if unused) */
Expand Down Expand Up @@ -1318,10 +1305,15 @@ int64_t signalfd_read(int guest_fd,
memset(&info, 0, sizeof(info));
info.ssi_signo = (uint32_t) pending[i].signum;
info.ssi_code = pending[i].si_code;
info.ssi_pid = (uint32_t) pending[i].si_pid;
info.ssi_uid = pending[i].si_uid;
info.ssi_int = pending[i].si_int;
info.ssi_ptr = pending[i].si_ptr;
if (info.ssi_code == LINUX_SI_TIMER) {
info.ssi_tid = (uint32_t) pending[i].si_pid;
info.ssi_overrun = pending[i].si_uid;
} else {
info.ssi_pid = (uint32_t) pending[i].si_pid;
info.ssi_uid = pending[i].si_uid;
}

uint64_t off = i * sizeof(linux_signalfd_siginfo_t);
if (guest_write_small(g, buf_gva + off, &info, sizeof(info)) < 0) {
Expand Down
16 changes: 16 additions & 0 deletions src/syscall/linux-wire.h
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,22 @@ typedef struct {
int64_t tv_sec, tv_nsec;
} linux_timespec_t;

/* Linux clocks and POSIX timer flags. */
#define LINUX_CLOCK_REALTIME 0
#define LINUX_CLOCK_MONOTONIC 1
#define LINUX_CLOCK_BOOTTIME 7
#define LINUX_TIMER_ABSTIME 1
#define LINUX_SIGEV_SIGNAL 0
#define LINUX_SIGEV_NONE 1
#define LINUX_SIGEV_THREAD_ID 4

/* Linux struct itimerspec, shared by POSIX timers and timerfd. */
typedef struct {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
int64_t it_interval_sec, it_interval_nsec, it_value_sec, it_value_nsec;
} linux_itimerspec_t;

_Static_assert(sizeof(linux_itimerspec_t) == 32, "Linux itimerspec size");

/* Linux struct timeval (aarch64). */
typedef struct {
int64_t tv_sec, tv_usec;
Expand Down
1 change: 1 addition & 0 deletions src/syscall/proc-pidfd.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ int pidfd_create(guest_t *g, int64_t target_pid, pid_t host_pid)
close(pfd[1]);
return -LINUX_EMFILE;
}
fd_publish_linux_flags(gfd, LINUX_O_CLOEXEC);

pthread_mutex_lock(&pidfd_lock);
pidfd_entry_t *entry = pidfd_find_free_entry();
Expand Down
Loading
Loading