@@ -69,6 +69,74 @@ pub(crate) fn build_fd_plan(fds: &[FdInfo]) -> (Vec<FdInfo>, Vec<SkippedFd>) {
6969 ( restorable, skipped)
7070}
7171
72+ /// The (start, end) of the mapping named exactly `name` (e.g. `"[vdso]"`) in
73+ /// `maps`, or `None` if absent.
74+ fn find_named_map ( maps : & [ MemoryMap ] , name : & str ) -> Option < ( u64 , u64 ) > {
75+ maps. iter ( )
76+ . find ( |m| m. path . as_deref ( ) == Some ( name) )
77+ . map ( |m| ( m. start , m. end ) )
78+ }
79+
80+ /// One planned relocation of a kernel special mapping ([vdso]/[vvar]): move the
81+ /// mapping currently at `cur_start` (length `len`) to `target_start`.
82+ #[ derive( Debug , PartialEq , Eq ) ]
83+ pub ( crate ) struct VdsoMove {
84+ pub cur_start : u64 ,
85+ pub len : u64 ,
86+ pub target_start : u64 ,
87+ }
88+
89+ /// Plan the relocations that put the stub's `[vvar]`/`[vdso]` at the addresses
90+ /// the checkpoint recorded. A mapping is moved only when present in both layouts
91+ /// and its current base differs from the target. The returned moves are ordered
92+ /// so that no move's *target* range overlaps a not-yet-executed move's *current*
93+ /// range, which would destroy a source before it is relocated. A layout that
94+ /// would require swapping two overlapping ranges (a dependency cycle) is
95+ /// reported as an error rather than corrupting the mappings.
96+ ///
97+ /// Rationale for same-kernel restore: the vDSO/vvar *code and data* the kernel
98+ /// mapped into the stub are identical to the checkpoint's (same kernel); only
99+ /// the ASLR base differs. Moving each mapping to its recorded base makes every
100+ /// pointer glibc cached into the vDSO resolve correctly on resume. The constant
101+ /// vvar-to-vdso distance is preserved automatically because both are moved to
102+ /// recorded addresses that already encode that distance.
103+ pub ( crate ) fn plan_vdso_moves (
104+ cur : & [ MemoryMap ] ,
105+ cp : & [ MemoryMap ] ,
106+ ) -> Result < Vec < VdsoMove > , String > {
107+ let mut remaining: Vec < VdsoMove > = Vec :: new ( ) ;
108+ for name in [ "[vvar]" , "[vvar_vclock]" , "[vdso]" ] {
109+ if let ( Some ( ( cs, ce) ) , Some ( ( ts, _) ) ) =
110+ ( find_named_map ( cur, name) , find_named_map ( cp, name) )
111+ {
112+ if cs != ts {
113+ remaining. push ( VdsoMove { cur_start : cs, len : ce - cs, target_start : ts } ) ;
114+ }
115+ }
116+ }
117+
118+ let mut ordered = Vec :: new ( ) ;
119+ while !remaining. is_empty ( ) {
120+ // Pick a move whose target does not overlap any *other* remaining move's
121+ // current range: relocating it cannot clobber a source we still need.
122+ let pick = remaining. iter ( ) . position ( |m| {
123+ let ( t_start, t_end) = ( m. target_start , m. target_start + m. len ) ;
124+ !remaining
125+ . iter ( )
126+ . any ( |o| o != m && t_start < ( o. cur_start + o. len ) && o. cur_start < t_end)
127+ } ) ;
128+ match pick {
129+ Some ( i) => ordered. push ( remaining. remove ( i) ) ,
130+ None => {
131+ return Err (
132+ "vdso/vvar relocation requires swapping overlapping ranges" . into ( ) ,
133+ )
134+ }
135+ }
136+ }
137+ Ok ( ordered)
138+ }
139+
72140fn prot_from_perms ( perms : & str ) -> libc:: c_int {
73141 let mut prot = 0 ;
74142 if perms. as_bytes ( ) . first ( ) == Some ( & b'r' ) { prot |= libc:: PROT_READ ; }
@@ -90,10 +158,15 @@ fn prot_from_perms(perms: &str) -> libc::c_int {
90158/// Limitation: file-backed regions are restored `MAP_PRIVATE` from the on-disk
91159/// file, so a checkpointed `MAP_SHARED` mapping is restored as private
92160/// (documented M1 limitation).
93- /// Limitation: transparent restore currently works for vDSO-free programs.
94- /// libc/glibc programs that call vDSO functions (e.g. `clock_gettime`) crash
95- /// on resume because the vDSO is not yet relocated/restored (known limitation,
96- /// next milestone).
161+ /// vDSO handling: the kernel-provided `[vdso]`/`[vvar]`/`[vvar_vclock]` mappings
162+ /// are relocated onto the checkpoint-recorded bases (see `plan_vdso_moves`), so
163+ /// libc/glibc programs whose cached vDSO pointers (e.g. `clock_gettime`) target
164+ /// the checkpoint-era base resume correctly. This assumes a same-kernel restore
165+ /// (the vDSO code is byte-identical; only the ASLR base differs).
166+ /// Limitation: the stub's own leftover mappings (launcher text, pre-exec stack,
167+ /// inherited anon) are not swept, so the restored address space is the union of
168+ /// the checkpoint image and those leftovers; `/proc/self/maps` shows them and
169+ /// they remain readable to the workload.
97170#[ cfg( target_arch = "x86_64" ) ]
98171pub ( crate ) fn restore_into (
99172 pid : i32 ,
@@ -106,6 +179,7 @@ pub(crate) fn restore_into(
106179 const MMAP : u64 = 9 ;
107180 const MPROTECT : u64 = 10 ;
108181 const MUNMAP : u64 = 11 ;
182+ const MREMAP : u64 = 25 ;
109183 const OPEN : u64 = 2 ;
110184 const CLOSE : u64 = 3 ;
111185 const LSEEK : u64 = 8 ;
@@ -257,6 +331,42 @@ pub(crate) fn restore_into(
257331 }
258332 }
259333
334+ // Relocate the kernel-provided [vdso]/[vvar] to the addresses the checkpoint
335+ // recorded. glibc caches vDSO function pointers (clock_gettime, getcpu, ...)
336+ // in process memory at the vDSO's original base; without this, a restored
337+ // libc program jumps to the checkpoint-era base, which the fresh stub mapped
338+ // elsewhere under ASLR, and faults on its first vDSO call. Same-kernel
339+ // restore only: the vDSO code is byte-identical, so moving it to the recorded
340+ // base makes every cached pointer valid. A vDSO-free checkpoint yields no
341+ // moves.
342+ {
343+ let cur = crate :: checkpoint:: capture:: parse_proc_maps ( pid)
344+ . map_err ( |e| err ( format ! ( "restore read maps for vdso relocation: {e}" ) ) ) ?;
345+ let moves = plan_vdso_moves ( & cur, & cp. process_state . memory_maps )
346+ . map_err ( |m| err ( format ! ( "restore vdso relocation: {m}" ) ) ) ?;
347+ for mv in moves {
348+ let flags = ( libc:: MREMAP_MAYMOVE | libc:: MREMAP_FIXED ) as u64 ;
349+ let r = inject:: inject_syscall_at (
350+ pid,
351+ tramp,
352+ MREMAP ,
353+ [ mv. cur_start , mv. len , mv. len , flags, mv. target_start , 0 ] ,
354+ )
355+ . map_err ( |e| {
356+ err ( format ! (
357+ "restore mremap {:#x}->{:#x}: {e}" ,
358+ mv. cur_start, mv. target_start
359+ ) )
360+ } ) ?;
361+ if r as u64 != mv. target_start {
362+ return Err ( err ( format ! (
363+ "restore mremap {:#x}->{:#x} -> {r:#x}" ,
364+ mv. cur_start, mv. target_start
365+ ) ) ) ;
366+ }
367+ }
368+ }
369+
260370 // Unmap the RWX trampoline as the very last injected syscall, after all
261371 // region and fd injections (which need it), and before the register restores
262372 // (which use ptrace, not the trampoline). The `syscall` instruction at
@@ -370,6 +480,76 @@ mod tests {
370480 "/sys/ paths must be skipped" ) ;
371481 }
372482
483+ fn map ( start : u64 , end : u64 , path : Option < & str > ) -> MemoryMap {
484+ MemoryMap { start, end, perms : "rw-p" . into ( ) , offset : 0 , path : path. map ( Into :: into) }
485+ }
486+
487+ #[ test]
488+ fn vdso_moves_relocate_present_mappings_only ( ) {
489+ // Stub layout (cur) and checkpoint layout (cp) disagree on vdso/vvar
490+ // bases; a non-special region present in both is ignored.
491+ let cur = vec ! [
492+ map( 0x1000 , 0x2000 , Some ( "[vvar]" ) ) ,
493+ map( 0x2000 , 0x3000 , Some ( "[vdso]" ) ) ,
494+ map( 0x9000 , 0xa000 , None ) ,
495+ ] ;
496+ let cp = vec ! [
497+ map( 0x5000 , 0x6000 , Some ( "[vvar]" ) ) ,
498+ map( 0x6000 , 0x7000 , Some ( "[vdso]" ) ) ,
499+ ] ;
500+ let moves = plan_vdso_moves ( & cur, & cp) . expect ( "no cycle" ) ;
501+ assert_eq ! ( moves. len( ) , 2 , "both special mappings relocate" ) ;
502+ assert ! ( moves. iter( ) . any( |m| m. cur_start == 0x1000 && m. target_start == 0x5000 ) ) ;
503+ assert ! ( moves. iter( ) . any( |m| m. cur_start == 0x2000 && m. target_start == 0x6000 ) ) ;
504+ }
505+
506+ #[ test]
507+ fn vdso_moves_skip_when_base_already_matches ( ) {
508+ let cur = vec ! [ map( 0x5000 , 0x6000 , Some ( "[vdso]" ) ) ] ;
509+ let cp = vec ! [ map( 0x5000 , 0x6000 , Some ( "[vdso]" ) ) ] ;
510+ assert ! ( plan_vdso_moves( & cur, & cp) . unwrap( ) . is_empty( ) ,
511+ "no move when the base already matches" ) ;
512+ }
513+
514+ #[ test]
515+ fn vdso_moves_skip_when_absent_from_checkpoint ( ) {
516+ // A freestanding checkpoint records no vdso; nothing to relocate.
517+ let cur = vec ! [ map( 0x2000 , 0x3000 , Some ( "[vdso]" ) ) ] ;
518+ let cp = vec ! [ map( 0x2000 , 0x3000 , None ) ] ;
519+ assert ! ( plan_vdso_moves( & cur, & cp) . unwrap( ) . is_empty( ) ) ;
520+ }
521+
522+ #[ test]
523+ fn vdso_moves_order_avoids_clobbering_a_source ( ) {
524+ // vvar must move onto the range vdso currently occupies. Moving vvar
525+ // first would destroy vdso's source, so vdso must be scheduled first.
526+ let cur = vec ! [
527+ map( 0x1000 , 0x2000 , Some ( "[vvar]" ) ) ,
528+ map( 0x5000 , 0x6000 , Some ( "[vdso]" ) ) ,
529+ ] ;
530+ let cp = vec ! [
531+ map( 0x5000 , 0x6000 , Some ( "[vvar]" ) ) , // vvar target == vdso's current base
532+ map( 0x8000 , 0x9000 , Some ( "[vdso]" ) ) ,
533+ ] ;
534+ let moves = plan_vdso_moves ( & cur, & cp) . expect ( "no cycle" ) ;
535+ assert_eq ! ( moves[ 0 ] . cur_start, 0x5000 , "vdso relocates before vvar overwrites its base" ) ;
536+ assert_eq ! ( moves[ 1 ] . cur_start, 0x1000 ) ;
537+ }
538+
539+ #[ test]
540+ fn vdso_moves_reject_unresolvable_swap ( ) {
541+ // Each mapping's target is the other's current base: no safe order.
542+ let cur = vec ! [
543+ map( 0x1000 , 0x2000 , Some ( "[vvar]" ) ) ,
544+ map( 0x5000 , 0x6000 , Some ( "[vdso]" ) ) ,
545+ ] ;
546+ let cp = vec ! [
547+ map( 0x5000 , 0x6000 , Some ( "[vvar]" ) ) ,
548+ map( 0x1000 , 0x2000 , Some ( "[vdso]" ) ) ,
549+ ] ;
550+ assert ! ( plan_vdso_moves( & cur, & cp) . is_err( ) , "a pure swap is rejected, not corrupted" ) ;
551+ }
552+
373553 #[ test]
374554 fn plan_classifies_regions ( ) {
375555 let maps = vec ! [
0 commit comments