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
2 changes: 2 additions & 0 deletions src/core/bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ int guest_bootstrap_prepare(guest_t *g,
free(rosetta_argv);
return -1;
}
g->start_stack = boot->stack_pointer;
startup_trace_step("build_linux_stack", t0);
/* rosetta_argv was copied into the guest stack; the host allocation is no
* longer needed. The strings themselves are constants (ROSETTA_PATH) or
Expand Down Expand Up @@ -868,6 +869,7 @@ int guest_bootstrap_rosetta_post_reset(guest_t *g,
log_error("build_linux_stack failed during exec re-bootstrap");
return -1;
}
g->start_stack = sp;
proc_set_auxv(auxv.words, auxv.nwords * sizeof(auxv.words[0]));

*out_entry_point = rr.entry_point;
Expand Down
1 change: 1 addition & 0 deletions src/core/guest.h
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ typedef struct {
uint64_t brk_current; /* Current brk position */
uint64_t stack_base; /* Bottom of stack region (dynamic, above brk) */
uint64_t stack_top; /* Top of stack (stack grows down from here) */
uint64_t start_stack; /* Initial userspace SP for /proc/<pid>/stat */

uint64_t mmap_next; /* RW mmap high-water mark for fork IPC snapshots */
uint64_t mmap_end; /* Current page-table-covered RW mmap limit */
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/fork-state.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/* Fork IPC protocol identity. Bump this whenever the header layout or ordered
* fork payload changes incompatibly.
*/
#define FORK_IPC_PROTOCOL_MAGIC 0x454C464DU /* "ELFM" */
#define FORK_IPC_PROTOCOL_MAGIC 0x454C464EU /* "ELFN" */

#define IPC_MAGIC_HEADER FORK_IPC_PROTOCOL_MAGIC
#define IPC_MAGIC_SENTINEL 0x454C4F4BU /* "ELOK" */
Expand All @@ -34,6 +34,7 @@ typedef struct {
uint64_t brk_base, brk_current;
uint64_t stack_base;
uint64_t stack_top;
uint64_t start_stack;
uint64_t mmap_next, mmap_end, pt_pool_next, ttbr0;
uint64_t mmap_rx_next;
uint64_t mmap_rx_end;
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/forkipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ int fork_child_main(int ipc_fd,
g.elf_load_min = hdr.elf_load_min;
g.stack_base = hdr.stack_base;
g.stack_top = hdr.stack_top;
g.start_stack = hdr.start_stack;
g.mmap_next = hdr.mmap_next;
g.mmap_end = hdr.mmap_end;
g.pt_pool_next = hdr.pt_pool_next;
Expand Down Expand Up @@ -1678,6 +1679,7 @@ int64_t sys_clone(hv_vcpu_t vcpu,
.brk_current = g->brk_current,

@jserv jserv Jul 19, 2026

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.

This new wire-format field is the riskiest part of the PR, but nothing tests that it survives the fork IPC round-trip. test-fork-ipc-protocol-host.c only static-asserts the magic bumped to ELFN; no test forks a child and reads the child's /proc/self/stat field 28 to confirm start_stack packs/unpacks at the right offset. A fork→child field-28 check would close this.

.stack_base = g->stack_base,
.stack_top = g->stack_top,
.start_stack = g->start_stack,
.mmap_next = g->mmap_next,
.mmap_end = g->mmap_end,
.pt_pool_next = g->pt_pool_next,
Expand Down
34 changes: 26 additions & 8 deletions src/runtime/procemu.c
Original file line number Diff line number Diff line change
Expand Up @@ -2538,7 +2538,18 @@ static int proc_open_meminfo(void)
* the tid is unparseable or the leaf is unknown. Split out of
* proc_intercept_open to keep that dispatcher readable.
*/
static int proc_open_self_task_node(const char *path, int linux_flags)
static uint64_t proc_start_stack(const guest_t *g)
{
if (g->start_stack != 0)
return g->start_stack;
if (g->stack_top > g->stack_base)
return g->stack_top - 16;
return 0;
}

static int proc_open_self_task_node(const guest_t *g,
const char *path,
int linux_flags)
{
char *endp;
long tid = strtol(path + 16, &endp, 10);
Expand All @@ -2552,13 +2563,17 @@ static int proc_open_self_task_node(const char *path, int linux_flags)
}

if (!strcmp(endp, "/stat")) {
uint64_t start_stack = proc_start_stack(g);

@jserv jserv Jul 19, 2026

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.

/proc/self/task/<tid>/stat reports the process-wide g->start_stack for every tid, whereas real Linux reports each thread's own stack start. Harmless today (single guest_t, no per-thread stack tracking, the test only exercises the main tid), but a latent gap for CLONE_VM threads reading a non-main tid's field 28.

return proc_emit_fmt(
"%ld (%.15s) R %lld %lld %lld 0 0 0 0 0 0 0 0 0 0 0 "
"20 0 %d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "
"0 0 0 0 0 0 0 0\n",
"%ld (%.15s) R %lld %lld %lld 0 0 0 " /* 1-9 */
"0 0 0 0 0 0 0 0 " /* 10-17 */
"20 0 %d 0 0 0 0 " /* 18-24 */
"18446744073709551615 0 0 %llu 0 0 0 " /* 25-31 */
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n", /* 32-52 */
tid, proc_comm_name(), (long long) proc_get_ppid(),
(long long) proc_get_pid(), /* pgid */
(long long) proc_get_sid(), thread_active_count());
(long long) proc_get_sid(), thread_active_count(),
(unsigned long long) start_stack);
}

if (!strcmp(endp, "/status")) {
Expand Down Expand Up @@ -3025,7 +3040,7 @@ int proc_intercept_open(const guest_t *g,

/* /proc/self/task/<tid>/{stat,status} and the <tid> directory. */
if (!strncmp(path, "/proc/self/task/", 16))
return proc_open_self_task_node(path, linux_flags);
return proc_open_self_task_node(g, path, linux_flags);

/* /proc/self/maps -> generated from guest region tracking. Addresses are
* page-aligned (rounded down/up) to match real Linux behavior. Output
Expand Down Expand Up @@ -3333,6 +3348,8 @@ int proc_intercept_open(const guest_t *g,
rss_pages += sz / (uint64_t) page_size;
}

uint64_t start_stack = proc_start_stack(g);

/* Fields: pid(1) (comm)(2) state(3) ppid(4) pgrp(5) session(6)
* tty_nr(7) tpgid(8) flags(9) minflt(10) cminflt(11) majflt(12)
* cmajflt(13) utime(14) stime(15) cutime(16) cstime(17)
Expand All @@ -3343,14 +3360,15 @@ int proc_intercept_open(const guest_t *g,
"%lld (%.15s) R %lld %lld %lld 0 -1 0 " /* 1-9 */
"0 0 0 0 %ld %ld 0 0 " /* 10-17 */
"20 0 %d 0 0 %llu %llu " /* 18-24 */
"18446744073709551615 0 0 0 0 0 0 " /* 25-31 */
"18446744073709551615 0 0 %llu 0 0 0 " /* 25-31 */
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n", /* 32-52 */
(long long) proc_get_pid(), proc_comm_name(),
(long long) proc_get_ppid(),
(long long) proc_get_pid(), /* pgrp = pid */
(long long) proc_get_pid(), /* session = pid */
utime_ticks, stime_ticks, thread_active_count(),
(unsigned long long) vsize, (unsigned long long) rss_pages);
(unsigned long long) vsize, (unsigned long long) rss_pages,
(unsigned long long) start_stack);
}

/* /proc/stat -> synthetic CPU statistics */
Expand Down
1 change: 1 addition & 0 deletions src/syscall/exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,7 @@ int64_t sys_execve(hv_vcpu_t vcpu,
sp = build_linux_stack(g, g->stack_top, argc, argv_const, envp_const,
&elf_info, elf_load_base, interp_base, exec_vdso,
-1 /* no AT_EXECFD */, &auxv);
g->start_stack = sp;

entry_point = (interp_base != 0) ? (interp_info.entry + interp_base)
: (elf_info.entry + elf_load_base);
Expand Down
7 changes: 5 additions & 2 deletions tests/test-fork-ipc-protocol-host.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@

#define LEGACY_ELFK_MAGIC 0x454C464BU
#define PREVIOUS_ELFL_MAGIC 0x454C464CU
#define PREVIOUS_ELFM_MAGIC 0x454C464DU

_Static_assert(FORK_IPC_PROTOCOL_MAGIC == 0x454C464DU,
"fork IPC protocol magic must remain ELFM until the next "
_Static_assert(FORK_IPC_PROTOCOL_MAGIC == 0x454C464EU,
"fork IPC protocol magic must remain ELFN until the next "
"incompatible wire-format change");
_Static_assert(IPC_MAGIC_HEADER == FORK_IPC_PROTOCOL_MAGIC,
"header magic must be the protocol identity");
_Static_assert(FORK_IPC_PROTOCOL_MAGIC != LEGACY_ELFK_MAGIC,
"current protocol must reject old ELFK children/parents");
_Static_assert(FORK_IPC_PROTOCOL_MAGIC != PREVIOUS_ELFL_MAGIC,
"NOFILE header fields require rejecting ELFL peers");
_Static_assert(FORK_IPC_PROTOCOL_MAGIC != PREVIOUS_ELFM_MAGIC,
"start_stack header field requires rejecting ELFM peers");
_Static_assert(IPC_MAGIC_SENTINEL != FORK_IPC_PROTOCOL_MAGIC,
"process-state sentinel must not alias the header protocol");

Expand Down
154 changes: 154 additions & 0 deletions tests/test-procfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>

#include "test-harness.h"
#include "test-util.h"
Expand All @@ -27,6 +29,93 @@ int passes = 0, fails = 0;
#define AT_NULL 0
#define AT_PAGESZ 6

static bool parse_proc_stat_field_u64(const char *buf,
int target,
uint64_t *out)
{
if (target < 4)
return false;

const char *p = strrchr(buf, ')');
if (!p)
return false;
p++;

int field = 3;
while (*p == ' ')
p++;
while (*p != '\0' && field <= target) {
char *endp;
if (field == 3) {

@jserv jserv Jul 19, 2026

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.

parse_proc_stat_field_u64 special-cases field 3 (endp = p+1, no field == target check), so it can't return field 3 and assumes state is exactly one character. Not exercised here (target is always 28), so no live impact — flagging only as a latent constraint if this helper gets reused.

if (*p == '\0' || p[1] != ' ')
return false;
endp = (char *) p + 1;
} else {
unsigned long long val = strtoull(p, &endp, 10);
if (endp == p)
return false;
if (field == target) {
*out = (uint64_t) val;
return true;
}
}
p = endp;
while (*p == ' ')
p++;
field++;
}
return false;
}

static bool parse_stack_range(const char *buf, uint64_t *start, uint64_t *end)
{
const char *line = buf;
while ((line = strstr(line, "[stack]")) != NULL) {
const char *begin = line;
while (begin > buf && begin[-1] != '\n')
begin--;

unsigned long long lo, hi;
if (sscanf(begin, "%llx-%llx", &lo, &hi) == 2) {
*start = (uint64_t) lo;
*end = (uint64_t) hi;
return true;
}
line += 7;
}
return false;
}

static bool read_stack_range(uint64_t *start, uint64_t *end)
{
char mapsbuf[65536];
ssize_t maps_n =
raw_read_file_nul("/proc/self/maps", mapsbuf, sizeof(mapsbuf));
return maps_n > 0 && parse_stack_range(mapsbuf, start, end);
}

static bool read_start_stack(const char *path, uint64_t *start_stack)
{
char statbuf[1024];
ssize_t stat_n = raw_read_file_nul(path, statbuf, sizeof(statbuf));
return stat_n > 0 && parse_proc_stat_field_u64(statbuf, 28, start_stack);
}

static bool start_stack_is_recorded(uint64_t start_stack, uint64_t stack_end)
{
return start_stack != stack_end - 16;
}

static int child_write_start_stack(int fd)
{
uint64_t start_stack = 0;
if (!read_start_stack("/proc/self/stat", &start_stack))
return 1;
if (write_fd_all(fd, &start_stack, sizeof(start_stack)) < 0)
return 2;
return 0;
}

int main(void)
{
char buf[4096] __attribute__((aligned(8)));
Expand Down Expand Up @@ -137,6 +226,71 @@ int main(void)
}
}

TEST("procfs: stat startstack is in [stack]");

@jserv jserv Jul 19, 2026

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.

This test (and the task-stat one below) can pass vacuously. It only asserts startstack lies inside [stack], but proc_start_stack() falls back to stack_top - 16 when g->start_stack == 0, and that fallback is also inside [stack]. So if start_stack propagation ever breaks (left 0 on some path), the membership check still passes and the regression goes undetected. The test does catch a mis-placed stat field (that would read a neighboring literal 0), but not a lost value. Consider asserting the reported value differs from the stack_top-16 fallback, or comparing against an independently derived SP.

{
uint64_t start_stack = 0, stack_start = 0, stack_end = 0;
bool ok = read_start_stack("/proc/self/stat", &start_stack) &&
read_stack_range(&stack_start, &stack_end) &&
start_stack >= stack_start && start_stack < stack_end;
EXPECT_TRUE(ok, "startstack not inside [stack]");
}

TEST("procfs: task stat startstack is in [stack]");
{
char path[128];
snprintf(path, sizeof(path), "/proc/self/task/%ld/stat",
(long) raw_getpid());
uint64_t start_stack = 0, stack_start = 0, stack_end = 0;
bool ok = read_start_stack(path, &start_stack) &&
read_stack_range(&stack_start, &stack_end) &&
start_stack >= stack_start && start_stack < stack_end;
EXPECT_TRUE(ok, "task startstack not inside [stack]");
}

TEST("procfs: stat startstack is recorded SP");
{
uint64_t start_stack = 0, stack_start = 0, stack_end = 0;
bool ok = read_start_stack("/proc/self/stat", &start_stack) &&
read_stack_range(&stack_start, &stack_end) &&
start_stack >= stack_start && start_stack < stack_end &&
start_stack_is_recorded(start_stack, stack_end);
EXPECT_TRUE(ok, "startstack used fallback value");
}

TEST("procfs: fork preserves stat startstack");
{
int pipefd[2];
uint64_t parent_start = 0, child_start = 0;
uint64_t stack_start = 0, stack_end = 0;
bool ok = read_start_stack("/proc/self/stat", &parent_start) &&
read_stack_range(&stack_start, &stack_end) &&
start_stack_is_recorded(parent_start, stack_end) &&
pipe(pipefd) == 0;
if (!ok) {
EXPECT_TRUE(false, "fork startstack setup failed");
} else {
pid_t pid = fork();
if (pid == 0) {
close(pipefd[0]);
int rc = child_write_start_stack(pipefd[1]);
close(pipefd[1]);
_exit(rc);
}
close(pipefd[1]);
ssize_t nread = read(pipefd[0], &child_start, sizeof(child_start));
close(pipefd[0]);
int status = 0;
if (pid < 0 || waitpid(pid, &status, 0) < 0)
ok = false;
else
ok = nread == (ssize_t) sizeof(child_start) &&
WIFEXITED(status) && WEXITSTATUS(status) == 0 &&
child_start == parent_start &&
start_stack_is_recorded(child_start, stack_end);
EXPECT_TRUE(ok, "fork child startstack mismatch");
}
}

SUMMARY("test-procfs");
return fails > 0 ? 1 : 0;
}
Loading