Skip to content
Merged
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
229 changes: 143 additions & 86 deletions src/syscall/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,130 @@ int64_t sys_dup3(int oldfd, int newfd, int linux_flags)
return newfd;
}

/* Translate a Linux struct flock (aarch64) at `arg` to macOS layout, run
* fcntl(host_fd, mac_cmd, ...), and for a GETLK command write the result
* back translated to Linux layout. Shared by the traditional (F_GETLK/
* F_SETLK/F_SETLKW) and OFD (F_OFD_GETLK/F_OFD_SETLK/F_OFD_SETLKW) lock
* commands, which differ only in the macOS cmd values and in how l_pid is
* reported back for a GETLK conflict.
*
* Linux aarch64 layout: {short l_type, short l_whence,
* long l_start, long l_len, int l_pid, pad[4]}
* macOS layout: {off_t l_start, off_t l_len, pid_t l_pid,
* short l_type, short l_whence}
* Use guest_read/guest_write (not guest_ptr) to safely handle structs that
* span 2MiB page table block boundaries.
*/
static int64_t fcntl_flock_op(guest_t *g,
host_fd_ref_t *host_ref,
uint64_t arg,
int mac_cmd,
bool is_getlk,
bool is_ofd)
{
uint8_t lflock[32]; /* Linux struct flock is 32 bytes on aarch64 */
if (guest_read_small(g, arg, lflock, sizeof(lflock)) < 0)
return -LINUX_EFAULT;

int16_t l_type, l_whence;
int64_t l_start, l_len;
int32_t l_pid;
memcpy(&l_type, lflock + 0, 2);
memcpy(&l_whence, lflock + 2, 2);
memcpy(&l_start, lflock + 8, 8); /* offset 8 due to padding */
memcpy(&l_len, lflock + 16, 8);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
memcpy(&l_pid, lflock + 24, 4);

/* Linux rejects F_OFD_GETLK/SETLK/SETLKW requests with a nonzero l_pid:
* OFD locks are owned by the open file description, not a process, so
* the field is reserved on input (fs/locks.c fcntl_getlk/fcntl_setlk
* both `return -EINVAL` on a nonzero request l_pid for these commands).
*/
if (is_ofd && l_pid != 0)
return -LINUX_EINVAL;

/* l_type constants differ between Linux and macOS/BSD:
* Linux: F_RDLCK=0, F_WRLCK=1, F_UNLCK=2
* macOS: F_RDLCK=1, F_UNLCK=2, F_WRLCK=3
* Passing the Linux value straight through makes a Linux F_RDLCK (0) an
* invalid type on macOS, which fcntl() rejects with EINVAL. This is the
* lock POSIX databases (e.g. SQLite) take first, so it must map.
*/
short mac_type;
switch (l_type) {
case 0: /* LINUX_F_RDLCK */
mac_type = F_RDLCK;
break;
case 1: /* LINUX_F_WRLCK */
Comment on lines +1034 to +1074

@doanbaotrung doanbaotrung Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Traditional F_GETLK Leaks Host PID to the Guest**

  • Location: fcntl_flock_op inside src/syscall/fs.c

  • Problem:
    For OFD locks (F_OFD_GETLK), the implementation correctly overrides the conflict PID with -1 (per Linux specification) to avoid leaking host PIDs.

    However, for traditional locks (F_GETLK), it reports:

    rp = mac_fl.l_pid;

    This passes the macOS host process ID directly to the guest. Because elfuse translates and virtualizes guest PIDs, the guest program (e.g., SQLite or other locking monitors) receives a raw host PID (e.g., 40548) instead of the child's guest PID (e.g., 2).

    If the guest tries to manage or check that PID (for example, via kill(conflict_pid, 0)), the PID translation gate in sc_kill looks for the host PID in the guest table, fails to resolve it, and returns ESRCH, even though the process is alive.

  • Proposed Fix:
    Add a proc_host_to_guest_pid helper to map the host PID back to its corresponding guest PID via the child process table and cross-process registry.

mac_type = F_WRLCK;
break;
case 2: /* LINUX_F_UNLCK */
mac_type = F_UNLCK;
break;
default:
return -LINUX_EINVAL;
}

struct flock mac_fl = {
.l_start = l_start,
.l_len = l_len,
.l_pid = 0,
.l_type = mac_type,
.l_whence = l_whence, /* SEEK_SET=0, SEEK_CUR=1, SEEK_END=2 same */
};

if (fcntl(host_ref->fd, mac_cmd, &mac_fl) < 0)
return linux_errno();

if (!is_getlk)
return 0;

/* Map macOS l_type back to Linux constants (see above). */
int16_t rt;
switch (mac_fl.l_type) {
case F_RDLCK:
rt = 0; /* LINUX_F_RDLCK */
break;
case F_WRLCK:
rt = 1; /* LINUX_F_WRLCK */
break;
default:
rt = 2; /* LINUX_F_UNLCK */
break;
}
int16_t rw = mac_fl.l_whence;
int64_t rs = mac_fl.l_start, rl = mac_fl.l_len;
int32_t rp;
if (is_ofd) {
/* OFD locks are owned by the open file description, not a single
* process, so Linux always reports l_pid=-1 on a conflicting
* F_OFD_GETLK lock instead of leaking a host PID to the guest.
*/
rp = (rt == 2) ? 0 : -1;
} else if (rt == 2) {
rp = (int32_t) mac_fl.l_pid; /* F_UNLCK: no conflict to translate */
} else {
/* mac_fl.l_pid is a raw host PID, meaningless to guest code that
* treats it as a real PID (e.g. a liveness check via kill(pid, 0)).
* Translate it to the conflicting process's guest PID when it is
* part of this guest's fork family; fall back to the host PID only
* when the lock holder cannot be resolved (e.g. an unrelated host
* process), since no guest identity exists for it to report.
*/
int64_t gpid = proc_host_to_guest_pid((pid_t) mac_fl.l_pid);
rp = (gpid > 0) ? (int32_t) gpid : (int32_t) mac_fl.l_pid;
}
memset(lflock, 0, sizeof(lflock));
memcpy(lflock + 0, &rt, 2);
memcpy(lflock + 2, &rw, 2);
memcpy(lflock + 8, &rs, 8);
memcpy(lflock + 16, &rl, 8);
memcpy(lflock + 24, &rp, 4);
if (guest_write_small(g, arg, lflock, sizeof(lflock)) < 0)
return -LINUX_EFAULT;
return 0;
}

int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
{
if (!RANGE_CHECK(fd, 0, FD_TABLE_SIZE))
Expand Down Expand Up @@ -1196,95 +1320,28 @@ int64_t sys_fcntl(guest_t *g, int fd, int cmd, uint64_t arg)
host_fd_ref_t host_ref;
if (host_fd_ref_open(fd, &host_ref) < 0)
return -LINUX_EBADF;
/* Translate Linux struct flock (aarch64) to macOS struct flock. Linux
* aarch64 layout: {short l_type, short l_whence,
* long l_start, long l_len, int l_pid, pad[4]}
* macOS layout: {off_t l_start, off_t l_len, pid_t l_pid,
* short l_type, short l_whence}
* Use guest_read/guest_write (not guest_ptr) to safely handle structs
* that span 2MiB page table block boundaries.
*/
uint8_t lflock[32]; /* Linux struct flock is 32 bytes on aarch64 */
if (guest_read_small(g, arg, lflock, sizeof(lflock)) < 0)
return -LINUX_EFAULT;

/* Read Linux flock fields */
int16_t l_type, l_whence;
int64_t l_start, l_len;
memcpy(&l_type, lflock + 0, 2);
memcpy(&l_whence, lflock + 2, 2);
memcpy(&l_start, lflock + 8, 8); /* offset 8 due to padding */
memcpy(&l_len, lflock + 16, 8);

/* l_type constants differ between Linux and macOS/BSD:
* Linux: F_RDLCK=0, F_WRLCK=1, F_UNLCK=2
* macOS: F_RDLCK=1, F_UNLCK=2, F_WRLCK=3
* Passing the Linux value straight through makes a Linux F_RDLCK (0) an
* invalid type on macOS, which fcntl() rejects with EINVAL. This is the
* lock POSIX databases (e.g. SQLite) take first, so it must map.
*/
short mac_type;
switch (l_type) {
case 0: /* LINUX_F_RDLCK */
mac_type = F_RDLCK;
break;
case 1: /* LINUX_F_WRLCK */
mac_type = F_WRLCK;
break;
case 2: /* LINUX_F_UNLCK */
mac_type = F_UNLCK;
break;
default:
host_fd_ref_close(&host_ref);
return -LINUX_EINVAL;
}

struct flock mac_fl = {
.l_start = l_start,
.l_len = l_len,
.l_pid = 0,
.l_type = mac_type,
.l_whence = l_whence, /* SEEK_SET=0, SEEK_CUR=1, SEEK_END=2 same */
};

int mac_cmd = (cmd == 5) ? F_GETLK : (cmd == 6) ? F_SETLK : F_SETLKW;
if (fcntl(host_ref.fd, mac_cmd, &mac_fl) < 0) {
host_fd_ref_close(&host_ref);
return linux_errno();
}

/* For F_GETLK, write back the result */
if (cmd == 5) {
/* Map macOS l_type back to Linux constants (see above). */
int16_t rt;
switch (mac_fl.l_type) {
case F_RDLCK:
rt = 0; /* LINUX_F_RDLCK */
break;
case F_WRLCK:
rt = 1; /* LINUX_F_WRLCK */
break;
default:
rt = 2; /* LINUX_F_UNLCK */
break;
}
int16_t rw = mac_fl.l_whence;
int64_t rs = mac_fl.l_start, rl = mac_fl.l_len;
int32_t rp = mac_fl.l_pid;
memset(lflock, 0, sizeof(lflock));
memcpy(lflock + 0, &rt, 2);
memcpy(lflock + 2, &rw, 2);
memcpy(lflock + 8, &rs, 8);
memcpy(lflock + 16, &rl, 8);
memcpy(lflock + 24, &rp, 4);
if (guest_write_small(g, arg, lflock, sizeof(lflock)) < 0) {
host_fd_ref_close(&host_ref);
return -LINUX_EFAULT;
}
}
int64_t rc =
fcntl_flock_op(g, &host_ref, arg, mac_cmd, cmd == 5, false);
host_fd_ref_close(&host_ref);
return 0;
return rc;
}
#if defined(F_OFD_GETLK) && defined(F_OFD_SETLK) && defined(F_OFD_SETLKW)
case 36: /* F_OFD_GETLK */
case 37: /* F_OFD_SETLK */
case 38: { /* F_OFD_SETLKW */
host_fd_ref_t host_ref;
if (host_fd_ref_open(fd, &host_ref) < 0)
return -LINUX_EBADF;
int mac_cmd = (cmd == 36) ? F_OFD_GETLK
: (cmd == 37) ? F_OFD_SETLK
: F_OFD_SETLKW;
int64_t rc =
fcntl_flock_op(g, &host_ref, arg, mac_cmd, cmd == 36, true);
host_fd_ref_close(&host_ref);
return rc;
}
#endif
case 8: { /* F_SETOWN */
/* SIGIO/SIGURG delivery owner. The arg is a signed value passed by
* value: pid > 0 targets a process, pid < 0 targets a process group,
Expand Down
69 changes: 69 additions & 0 deletions src/syscall/proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,35 @@ static void registry_parse_cb(char *rec, void *vctx)
c->entries[idx].pgid = (int64_t) pg;
}

typedef struct {
pid_t target;
int64_t guest_pid;
bool found;
} registry_find_ctx_t;

/* Locate @target's guest pid without registry_parse_cb's per-record
* kill(2) liveness probe: that check exists to build a filtered live-
* membership list for group-signal delivery, but a host_pid ->guest_pid
* lookup is only ever done for a pid the caller just observed to be alive
* (e.g. it holds a conflicting file lock right now), so it is redundant
* here. proc_host_to_guest_pid still verifies the match via proc_pidpath
* to guard against the pid having been recycled.
*/
static void registry_find_by_host_cb(char *rec, void *vctx)
{
registry_find_ctx_t *c = vctx;
long hp;
long long gp, pg;
if (sscanf(rec, "%ld %lld %lld", &hp, &gp, &pg) != 3)
return;
if (hp <= 0 || hp > INT_MAX || pg < 0 || pg > INT_MAX)
return;
if ((pid_t) hp != c->target)
return;
c->guest_pid = (int64_t) gp;
c->found = true;
}

/* Parse the whole registry from @fd (caller holds an flock) into @entries,
* keeping one record per live host pid.
*
Expand Down Expand Up @@ -805,6 +834,46 @@ int proc_get_namespace_targets(proc_signal_target_t *out,
return count;
}

int64_t proc_host_to_guest_pid(pid_t host_pid)
{
pthread_mutex_lock(&pid_lock);
proc_entry_t *entry = proc_find_host_entry(host_pid);
int64_t result = entry ? entry->guest_pid : -1;
pthread_mutex_unlock(&pid_lock);
if (result != -1)
return result;

char path[PATH_MAX];
if (!process_registry_path(path, sizeof(path)))
return -1;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);

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 first lock-conflict PID lookup in a recycled namespace-owner process can read a stale process registry and return the wrong guest PID. This new registry reader should mirror the existing registry readers by calling proc_registry_reset_if_owner(path) before opening the file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/proc.c, line 849:

<comment>A first lock-conflict PID lookup in a recycled namespace-owner process can read a stale process registry and return the wrong guest PID. This new registry reader should mirror the existing registry readers by calling `proc_registry_reset_if_owner(path)` before opening the file.</comment>

<file context>
@@ -805,6 +834,46 @@ int proc_get_namespace_targets(proc_signal_target_t *out,
+    char path[PATH_MAX];
+    if (!process_registry_path(path, sizeof(path)))
+        return -1;
+    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+    if (fd < 0)
+        return -1;
</file context>
Suggested change
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
proc_registry_reset_if_owner(path);
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);

if (fd < 0)
return -1;
if (flock_retry(fd, LOCK_SH) != 0) {
close(fd);
return -1;
}
registry_find_ctx_t ctx = {.target = host_pid};
for_each_record(fd, registry_find_by_host_cb, &ctx);
flock_retry(fd, LOCK_UN);
close(fd);
if (!ctx.found)
return -1;

/* Guard against host pid reuse: only trust the hit if the pid still
* runs this elfuse binary, same check as proc_get_namespace_targets.
*/
char our_path[PROC_PIDPATHINFO_MAXSIZE];
int our_len = proc_pidpath(getpid(), our_path, sizeof(our_path));
if (our_len <= 0)
return -1;
char ppath[PROC_PIDPATHINFO_MAXSIZE];
int plen = proc_pidpath(host_pid, ppath, sizeof(ppath));
if (plen != our_len || memcmp(ppath, our_path, (size_t) our_len))
return -1;
return ctx.guest_pid;
}

int proc_get_child_pids(pid_t *out, int max_pids)
{
/* Seed with direct children from the process table */
Expand Down
8 changes: 8 additions & 0 deletions src/syscall/proc.h
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,14 @@ int proc_set_child_pgid(int64_t guest_pid, int64_t pgid);
*/
pid_t proc_guest_to_host_pid(int64_t gpid);

/* Look up a host PID in the child process table, falling back to the
* cross-process registry so the rest of the fork family (grandchildren,
* siblings' descendants) resolves too. Returns the guest PID if @host_pid is
* a live member of this guest's fork family, or -1 otherwise (e.g. the lock
* holder is an unrelated host process).
*/
int64_t proc_host_to_guest_pid(pid_t host_pid);

/* Queue a Linux guest signal in a fork-child elfuse process. target_guest_pid
* tags the transport record so the receiver drops it if its host pid was
* recycled onto a different guest.
Expand Down
Loading
Loading