From e8c70795f960223384238ce77c28cc2aa0e19726 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Sat, 22 Aug 2026 20:37:33 -0700 Subject: [PATCH 1/2] checkpoint: assert FP_MAX fits sc_fpregs in the restore stub The riscv64 sc_fpregs memcpy carries no bound of its own: it is safe only because validate rejects fpstate_len > FP_MAX and FP_MAX (516) happens to fit the 528-byte field. Nothing enforced that relation, so raising FP_MAX or shrinking the struct would silently turn the copy into a stack overflow inside the signal frame. Assert it at the struct definition, next to the copy it protects. Signed-off-by: Cong Wang --- crates/sandlock-core/src/checkpoint/restore-stub.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/sandlock-core/src/checkpoint/restore-stub.c b/crates/sandlock-core/src/checkpoint/restore-stub.c index ecdd8dc8..7c5532f5 100644 --- a/crates/sandlock-core/src/checkpoint/restore-stub.c +++ b/crates/sandlock-core/src/checkpoint/restore-stub.c @@ -261,6 +261,10 @@ struct sigctx { u8 fpregs[528]; /* sc_fpregs: union __riscv_fp_state (kernel 528 byte union) */ }; +/* The sc_fpregs memcpy is bounded only by validate's FP_MAX check. */ +_Static_assert(FP_MAX <= sizeof((struct sigctx){0}.fpregs), + "FP_MAX must not exceed sc_fpregs"); + struct uctx { u64 uc_flags; /* 0x00 */ u64 uc_link; /* 0x08 */ From 5036c60c55c5defc2269815b7be4f7a89961af67 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Sat, 22 Aug 2026 20:40:41 -0700 Subject: [PATCH 2/2] checkpoint: share the riscv64 restart-sentinel rejection Capture and rearm_restartable_syscall each carried their own copy of the sentinel check plus a near-identical multi-line error, and the two messages had already drifted. Route both through one helper so the check and its wording cannot diverge again. Signed-off-by: Cong Wang --- .../sandlock-core/src/checkpoint/capture.rs | 10 ++----- .../src/checkpoint/restore_blob.rs | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/sandlock-core/src/checkpoint/capture.rs b/crates/sandlock-core/src/checkpoint/capture.rs index a0bdbca5..9b7b445a 100644 --- a/crates/sandlock-core/src/checkpoint/capture.rs +++ b/crates/sandlock-core/src/checkpoint/capture.rs @@ -423,14 +423,8 @@ pub(crate) fn capture(pid: i32, policy: &Sandbox) -> Result bool { /// `user_regs_struct`, so neither capture nor restore can recover it. Callers /// reject the checkpoint rather than resume with a corrupt return value. #[cfg(target_arch = "riscv64")] -pub(crate) fn restart_sentinel_in_a0(regs: &[u64]) -> Option { +fn restart_sentinel_in_a0(regs: &[u64]) -> Option { // riscv64 user_regs_struct order: pc, ra, sp, gp, tp, t0-t2, s0-s1, // a0-a7, s2-s11, t3-t6 — so a0 is index 10. const A0: usize = 10; @@ -380,6 +380,21 @@ pub(crate) fn restart_sentinel_in_a0(regs: &[u64]) -> Option { if is_restart_sentinel(a0) { Some(a0) } else { None } } +/// Refuse a riscv64 register file whose `a0` holds a restart sentinel, with +/// the shared error text; both capture and restore reject through here. +#[cfg(target_arch = "riscv64")] +pub(crate) fn reject_restart_sentinel(regs: &[u64]) -> Result<(), String> { + match restart_sentinel_in_a0(regs) { + Some(sentinel) => Err(format!( + "checkpoint captured an interrupted restartable syscall \ + (a0 = {sentinel}); riscv64 cannot recover its original argument, \ + so restore would resume with a corrupt return value; retry the \ + checkpoint while the workload is not blocked in a syscall" + )), + None => Ok(()), + } +} + /// Re-arm an interrupted, restartable syscall in the saved register file. /// /// When the checkpoint was taken (via `PTRACE_INTERRUPT`) while the process sat @@ -420,15 +435,7 @@ fn rearm_restartable_syscall(regs: &mut [u64]) -> Result<(), String> { /// syscall result. #[cfg(target_arch = "riscv64")] fn rearm_restartable_syscall(regs: &mut [u64]) -> Result<(), String> { - if let Some(sentinel) = restart_sentinel_in_a0(regs) { - return Err(format!( - "checkpoint captured an interrupted restartable syscall \ - (a0 = {sentinel}); riscv64 cannot recover its original argument, \ - so restore would resume with a corrupt return value. Retry the \ - checkpoint while the workload is not blocked in a syscall" - )); - } - Ok(()) + reject_restart_sentinel(regs) } /// Build the FP image the stub points the signal frame's `fpstate` at.