Skip to content

Commit 6faaefa

Browse files
committed
fix(uffd): propagate Removed pages into DiffMetadata.Empty
Read the Removed bitmap from PageTracker and emit it as DiffMetadata.Empty so REMOVE'd pages become uuid.Nil mappings in the snapshot header (read as zero on resume). Defensively AndNot the empty set out of dirty: settle drains make these disjoint in practice (Removed pages have no PTE, WP-async only sees present pages with WP cleared), but if the invariant ever breaks the guest's last intent for a Removed page is "free, read zero on restore" — so empty must win, not stale dirty content.
1 parent b346036 commit 6faaefa

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/orchestrator/pkg/sandbox/uffd/uffd.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,33 @@ func (u *Uffd) Exit() *utils.ErrorOnce {
217217
//
218218
// It *MUST* be only called after the sandbox was successfully paused via API and after the snapshot endpoint was called.
219219
func (u *Uffd) DiffMetadata(ctx context.Context, f *fc.Process) (*header.DiffMetadata, error) {
220-
return f.DirtyMemory(ctx, u.memfile.BlockSize())
220+
handler, err := u.handler.WaitWithContext(ctx)
221+
if err != nil {
222+
return nil, fmt.Errorf("failed to get uffd: %w", err)
223+
}
224+
225+
diff, err := f.DirtyMemory(ctx, u.memfile.BlockSize())
226+
if err != nil {
227+
return nil, fmt.Errorf("failed to get dirty memory: %w", err)
228+
}
229+
230+
// Without this, REMOVE'd pages stay attributed to the previous layer's BuildId in
231+
// the merged mapping and leak their stale contents on resume; with it, those ranges
232+
// become uuid.Nil sentinels (read as zero, matching guest free-page intent).
233+
_, empty := handler.ExportPageStates()
234+
235+
// dirty ∩ empty should be empty after settle (Removed pages have no PTE,
236+
// FC's WP-async dirty scan only sees present pages with WP cleared), but
237+
// AndNot here means "empty wins" if the invariant ever breaks: the guest's
238+
// most recent intent for a Removed page is "free, read zero on restore",
239+
// so we must not let stale dirty content shadow it in this diff layer.
240+
diff.Dirty.AndNot(empty)
241+
242+
return &header.DiffMetadata{
243+
BlockSize: diff.BlockSize,
244+
Dirty: diff.Dirty,
245+
Empty: empty,
246+
}, nil
221247
}
222248

223249
// PrefetchData returns page fault data for prefetch mapping.

packages/orchestrator/pkg/sandbox/uffd/userfaultfd/userfaultfd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212
"unsafe"
1313

14+
"github.com/RoaringBitmap/roaring/v2"
1415
"go.opentelemetry.io/otel"
1516
"go.opentelemetry.io/otel/attribute"
1617
"go.opentelemetry.io/otel/trace"
@@ -130,6 +131,14 @@ func NewUserfaultfdFromFd(fd uintptr, src block.Slicer, m *memory.Mapping, logge
130131
return u, nil
131132
}
132133

134+
// ExportPageStates returns snapshots of the faulted and removed
135+
// page-index bitmaps. The diff-snapshot writer uses the removed
136+
// bitmap to mark freed pages as DiffMetadata.Empty so they resolve
137+
// to uuid.Nil (zero-on-restore) in the snapshot header.
138+
func (u *Userfaultfd) ExportPageStates() (faulted, removed *roaring.Bitmap) {
139+
return u.pageTracker.Export()
140+
}
141+
133142
func (u *Userfaultfd) readEvents(ctx context.Context) ([]*UffdRemove, []*UffdPagefault, error) {
134143
buf := make([]byte, unsafe.Sizeof(UffdMsg{}))
135144

0 commit comments

Comments
 (0)