Skip to content

Commit afa183a

Browse files
nixprimegvisor-bot
authored andcommitted
Add runsc wait --fscheckpoint/fsrestore.
Note that runsc wait --fsrestore only waits for filesystem restore operations to complete for the given container, rather than the whole sandbox (like wait --fscheckpoint, --checkpoint, and --restore do). PiperOrigin-RevId: 886982088
1 parent 0ee4bfc commit afa183a

11 files changed

Lines changed: 311 additions & 40 deletions

File tree

‎pkg/sentry/kernel/BUILD‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ declare_mutex(
101101
prefix = "fsContext",
102102
)
103103

104+
declare_mutex(
105+
name = "fs_save_mutex",
106+
out = "fs_save_mutex.go",
107+
package = "kernel",
108+
prefix = "fsSave",
109+
)
110+
104111
declare_mutex(
105112
name = "running_tasks_mutex",
106113
out = "running_tasks_mutex.go",
@@ -254,6 +261,7 @@ go_library(
254261
"fs_context.go",
255262
"fs_context_mutex.go",
256263
"fs_context_refs.go",
264+
"fs_save_mutex.go",
257265
"fscheckpoint.go",
258266
"ipc_namespace.go",
259267
"kcov.go",

‎pkg/sentry/kernel/fscheckpoint.go‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (k *Kernel) FSSave(ctx context.Context, opts *FSSaveOpts) error {
7777
if opts.ExitAfterSaving {
7878
defer k.Kill(linux.WaitStatusExit(0)) // consistent with sentry/state.SaveOpts.Save
7979
}
80-
return k.quiescePausedAnd(ctx, func() error {
80+
err := k.quiescePausedAnd(ctx, func() error {
8181
var (
8282
asyncPageSaveWg sync.WaitGroup
8383
asyncPageSaveErr error
@@ -226,6 +226,13 @@ func (k *Kernel) FSSave(ctx context.Context, opts *FSSaveOpts) error {
226226
}
227227
return nil
228228
})
229+
k.fsSaveMu.Lock()
230+
defer k.fsSaveMu.Unlock()
231+
for _, c := range k.fsSaveWaiters {
232+
c <- err
233+
}
234+
k.fsSaveWaiters = nil
235+
return err
229236
}
230237

231238
type countingWriter struct {
@@ -239,3 +246,16 @@ func (cw *countingWriter) Write(src []byte) (int, error) {
239246
cw.count += uint64(n)
240247
return n, err
241248
}
249+
250+
// WaitForFSSave waits for a call to k.FSSave() to complete, then returns the
251+
// error returned by that call.
252+
//
253+
// This API is difficult to use without races, but is consistent with
254+
// k.WaitForCheckpoint().
255+
func (k *Kernel) WaitForFSSave() error {
256+
c := make(chan error, 1)
257+
k.fsSaveMu.Lock()
258+
k.fsSaveWaiters = append(k.fsSaveWaiters, c)
259+
k.fsSaveMu.Unlock()
260+
return <-c
261+
}

‎pkg/sentry/kernel/kernel.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,11 @@ type Kernel struct {
401401

402402
// MaxKeySetSize is the maximum number of keys in a key set.
403403
MaxKeySetSize atomicbitops.Int32
404+
405+
// fsSaveWaiters holds waiters for Kernel.WaitForFSSave. fsSaveWaiters is
406+
// protected by fsSaveMu.
407+
fsSaveMu fsSaveMutex `state:"nosave"`
408+
fsSaveWaiters []chan error `state:"nosave"`
404409
}
405410

406411
// InitKernelArgs holds arguments to Init.

‎pkg/sentry/pgalloc/save_restore.go‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -905,9 +905,15 @@ type LoadOpts struct {
905905
// returns, PagesFileOffset will be updated to the offset of the first byte
906906
// in PagesFile after this MemoryFile's contents.
907907
//
908+
// Reading from PagesFile may continue after LoadFrom returns. If
909+
// DoneCallback is not nil, it will be called when reading for this
910+
// MemoryFile completes. DoneCallback will be called whether or not
911+
// LoadFrom returns a non-nil error.
912+
//
908913
// Invariant: PagesFileOffset must be page-aligned.
909914
PagesFile *AsyncPagesFileLoad
910915
PagesFileOffset uint64
916+
DoneCallback func(error)
911917

912918
// Optional timeline for the restore process.
913919
// If async page loading is enabled, a forked timeline will be created, so
@@ -917,10 +923,16 @@ type LoadOpts struct {
917923
}
918924

919925
// LoadFrom loads MemoryFile state from the given stream.
920-
func (f *MemoryFile) LoadFrom(ctx context.Context, r io.Reader, opts *LoadOpts) error {
926+
func (f *MemoryFile) LoadFrom(ctx context.Context, r io.Reader, opts *LoadOpts) (err error) {
921927
mfTimeline := opts.Timeline.Fork(fmt.Sprintf("mf:%p", f)).Lease()
922928
defer mfTimeline.End()
923929

930+
defer func() {
931+
if opts.DoneCallback != nil {
932+
opts.DoneCallback(err)
933+
}
934+
}()
935+
924936
// Load metadata.
925937
timeMetadataStart := gohacks.Nanotime()
926938
var mfs memoryFileSaved
@@ -995,10 +1007,11 @@ func (f *MemoryFile) LoadFrom(ctx context.Context, r io.Reader, opts *LoadOpts)
9951007
}
9961008
}
9971009
amfl = &asyncMemoryFileLoad{
998-
f: f,
999-
pf: opts.PagesFile,
1000-
df: df,
1001-
timeline: mfTimeline.Transfer(),
1010+
f: f,
1011+
pf: opts.PagesFile,
1012+
df: df,
1013+
doneCallback: opts.DoneCallback,
1014+
timeline: mfTimeline.Transfer(),
10021015
}
10031016
amfl.pf.amflsMu.Lock()
10041017
if err := amfl.pf.err(); err != nil {
@@ -1008,6 +1021,7 @@ func (f *MemoryFile) LoadFrom(ctx context.Context, r io.Reader, opts *LoadOpts)
10081021
amfl.pf.amfls.PushBack(amfl)
10091022
amfl.pf.amflsMu.Unlock()
10101023
f.asyncPageLoad.Store(amfl)
1024+
opts.DoneCallback = nil
10111025
defer func() {
10121026
amfl.pf.amflsMu.Lock()
10131027
defer amfl.pf.amflsMu.Unlock()
@@ -1025,6 +1039,10 @@ func (f *MemoryFile) LoadFrom(ctx context.Context, r io.Reader, opts *LoadOpts)
10251039
amfl.pf.amfls.Remove(amfl)
10261040
amfl.f.asyncPageLoad.Store(nil)
10271041
amfl.timeline.End()
1042+
if amfl.doneCallback != nil {
1043+
amfl.doneCallback(nil)
1044+
amfl.doneCallback = nil
1045+
}
10281046
}
10291047
}()
10301048
}
@@ -1205,10 +1223,11 @@ func (apfl *AsyncPagesFileLoad) err() error {
12051223
// asyncMemoryFileLoad holds async page loading state for a single MemoryFile.
12061224
type asyncMemoryFileLoad struct {
12071225
// Immutable fields:
1208-
f *MemoryFile
1209-
pf *AsyncPagesFileLoad
1210-
df stateio.DestinationFile
1211-
timeline *timing.Timeline
1226+
f *MemoryFile
1227+
pf *AsyncPagesFileLoad
1228+
df stateio.DestinationFile
1229+
doneCallback func(error)
1230+
timeline *timing.Timeline
12121231

12131232
// minUnloaded is the MemoryFile offset of the first unloaded byte.
12141233
minUnloaded atomicbitops.Uint64
@@ -1597,6 +1616,10 @@ func (apfl *AsyncPagesFileLoad) main() {
15971616
ul.started = false
15981617
ul.waiters = nil
15991618
}
1619+
if amfl.doneCallback != nil {
1620+
amfl.doneCallback(apfl.err())
1621+
amfl.doneCallback = nil
1622+
}
16001623
}
16011624
apfl.mu.Unlock()
16021625
apfl.amflsMu.Unlock()
@@ -1876,6 +1899,10 @@ func (apfl *AsyncPagesFileLoad) main() {
18761899
apfl.amfls.Remove(amfl)
18771900
amfl.f.asyncPageLoad.Store(nil)
18781901
amfl.timeline.End()
1902+
if amfl.doneCallback != nil {
1903+
amfl.doneCallback(nil)
1904+
amfl.doneCallback = nil
1905+
}
18791906
} else {
18801907
amfl.minUnloaded.Store(amfl.unloaded.FirstSegment().Start())
18811908
}

‎runsc/boot/controller.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ const (
110110
// ContMgrWaitRestore waits for the Kernel restore to complete.
111111
ContMgrWaitRestore = "containerManager.WaitRestore"
112112

113+
// ContMgrWaitFSCheckpoint waits for the next filesystem checkpoint save to
114+
// complete.
115+
ContMgrWaitFSCheckpoint = "containerManager.WaitFSCheckpoint"
116+
117+
// ContMgrWaitFSRestore waits for filesystem checkpoint restore to complete
118+
// for all current containers.
119+
ContMgrWaitFSRestore = "containerManager.WaitFSRestore"
120+
113121
// ContMgrRootContainerStart starts a new sandbox with a root container.
114122
ContMgrRootContainerStart = "containerManager.StartRoot"
115123

@@ -870,6 +878,26 @@ func (cm *containerManager) WaitRestore(*struct{}, *struct{}) error {
870878
return err
871879
}
872880

881+
func (cm *containerManager) WaitFSCheckpoint(*struct{}, *struct{}) error {
882+
log.Debugf("containerManager.WaitFSCheckpoint")
883+
err := cm.l.k.WaitForFSSave()
884+
log.Debugf("containerManager.WaitFSCheckpoint done, err = %v", err)
885+
return err
886+
}
887+
888+
// WaitFSRestoreArgs holds arguments to containerManager.WaitFSRestore.
889+
type WaitFSRestoreArgs struct {
890+
// CID is the container ID.
891+
CID string
892+
}
893+
894+
func (cm *containerManager) WaitFSRestore(args *WaitFSRestoreArgs, _ *struct{}) error {
895+
log.Debugf("containerManager.WaitFSRestore")
896+
err := cm.l.fsRestore.wait(args.CID)
897+
log.Debugf("containerManager.WaitFSRestore done, err = %v", err)
898+
return err
899+
}
900+
873901
// SignalDeliveryMode enumerates different signal delivery modes.
874902
type SignalDeliveryMode int
875903

‎runsc/boot/fscheckpoint.go‎

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ type fsRestore struct {
8484
apfl *pgalloc.AsyncPagesFileLoad
8585
mfs map[checkpoint.ResourceID]*fscheckpoint.MemoryFile
8686
tmpfs map[checkpoint.ResourceID]*fscheckpoint.Tmpfs
87+
88+
waitMu sync.Mutex
89+
waitMap map[string]*fsRestoreContainer // key is container ID
90+
}
91+
92+
type fsRestoreContainer struct {
93+
// These fields are protected by fsRestore.waitMu.
94+
err error
95+
asyncLoads int // number of MemoryFiles currently in async page loading
96+
cond sync.Cond
8797
}
8898

8999
// fsRestoreOpts holds options to startFSRestore.
@@ -111,8 +121,9 @@ func makeFSRestoreOptsForLocalCheckpoint(args *Args) (fsRestoreOpts, error) {
111121
// startFSRestore takes ownership of resources in opts.
112122
func startFSRestore(opts *fsRestoreOpts) (*fsRestore, error) {
113123
fsr := &fsRestore{
114-
mfs: make(map[checkpoint.ResourceID]*fscheckpoint.MemoryFile),
115-
tmpfs: make(map[checkpoint.ResourceID]*fscheckpoint.Tmpfs),
124+
mfs: make(map[checkpoint.ResourceID]*fscheckpoint.MemoryFile),
125+
tmpfs: make(map[checkpoint.ResourceID]*fscheckpoint.Tmpfs),
126+
waitMap: make(map[string]*fsRestoreContainer),
116127
}
117128

118129
// TODO: NOLINT - Currently we read the whole pages metadata file into a
@@ -225,32 +236,71 @@ func startFSRestore(opts *fsRestoreOpts) (*fsRestore, error) {
225236
return fsr, nil
226237
}
227238

228-
func (fsr *fsRestore) memoryFileLoadArgs(id checkpoint.ResourceID) (io.Reader, uint64, error) {
239+
// +checklocks:fsr.waitMu
240+
func (fsr *fsRestore) ensureContainer(cid string) *fsRestoreContainer {
241+
c := fsr.waitMap[cid]
242+
if c == nil {
243+
c = &fsRestoreContainer{}
244+
c.cond.L = &fsr.waitMu
245+
fsr.waitMap[cid] = c
246+
}
247+
return c
248+
}
249+
250+
func (c *fsRestoreContainer) setError(err error) error {
251+
if c.err == nil && err != nil {
252+
c.err = err
253+
c.cond.Broadcast()
254+
}
255+
return err
256+
}
257+
258+
func (fsr *fsRestore) memoryFileLoadArgs(id checkpoint.ResourceID, cid string) (io.Reader, uint64, func(error), error) {
229259
if fsr == nil {
230-
return nil, 0, nil
260+
return nil, 0, func(error) {}, nil
231261
}
262+
232263
fsr.wg.Wait()
233264
if fsr.manifestErr != nil {
234-
return nil, 0, fsr.manifestErr
265+
return nil, 0, nil, fsr.manifestErr
235266
}
236267
mmf := fsr.mfs[id]
237268
if mmf == nil {
238-
return nil, 0, nil
269+
return nil, 0, func(error) {}, nil
239270
}
240271
pagesMetadata, err := fsr.getPagesMetadata()
241-
if mmf.PagesMetadataEnd <= uint64(len(pagesMetadata)) {
242-
return bytes.NewReader(pagesMetadata[mmf.PagesMetadataStart:mmf.PagesMetadataEnd]), mmf.PagesStart, nil
243-
}
244-
if err != nil {
245-
return nil, 0, fmt.Errorf("failed to read pages metadata: %w", err)
272+
273+
fsr.waitMu.Lock()
274+
defer fsr.waitMu.Unlock()
275+
c := fsr.ensureContainer(cid)
276+
if mmf.PagesMetadataEnd > uint64(len(pagesMetadata)) {
277+
if err != nil {
278+
return nil, 0, nil, c.setError(fmt.Errorf("failed to read pages metadata: %w", err))
279+
}
280+
return nil, 0, nil, c.setError(fmt.Errorf("MemoryFile %q has pages metadata range [%d, %d) beyond pages metadata file size %d", mmf.ResourceID, mmf.PagesMetadataStart, mmf.PagesMetadataEnd, len(pagesMetadata)))
246281
}
247-
return nil, 0, fmt.Errorf("MemoryFile %q has pages metadata range [%d, %d) beyond pages metadata file size %d", mmf.ResourceID, mmf.PagesMetadataStart, mmf.PagesMetadataEnd, len(pagesMetadata))
282+
c.asyncLoads++
283+
return bytes.NewReader(pagesMetadata[mmf.PagesMetadataStart:mmf.PagesMetadataEnd]), mmf.PagesStart, func(err error) {
284+
fsr.waitMu.Lock()
285+
defer fsr.waitMu.Unlock()
286+
c.asyncLoads--
287+
switch {
288+
case err != nil:
289+
if c.err == nil {
290+
c.err = err
291+
}
292+
fallthrough
293+
case c.asyncLoads == 0:
294+
c.cond.Broadcast()
295+
}
296+
}, nil
248297
}
249298

250-
func (fsr *fsRestore) tmpfsSourceTar(id checkpoint.ResourceID) (io.ReadCloser, error) {
299+
func (fsr *fsRestore) tmpfsSourceTar(id checkpoint.ResourceID, cid string) (io.ReadCloser, error) {
251300
if fsr == nil {
252301
return nil, nil
253302
}
303+
254304
fsr.wg.Wait()
255305
if fsr.manifestErr != nil {
256306
return nil, fsr.manifestErr
@@ -260,11 +310,43 @@ func (fsr *fsRestore) tmpfsSourceTar(id checkpoint.ResourceID) (io.ReadCloser, e
260310
return nil, nil
261311
}
262312
multiTar, err := fsr.getMultiTar()
313+
314+
fsr.waitMu.Lock()
315+
defer fsr.waitMu.Unlock()
263316
if mt.TarEnd <= uint64(len(multiTar)) {
264317
return io.NopCloser(bytes.NewReader(multiTar[mt.TarStart:mt.TarEnd])), nil
265318
}
319+
c := fsr.ensureContainer(cid)
266320
if err != nil {
267-
return nil, fmt.Errorf("failed to read tar archive: %w", err)
321+
return nil, c.setError(fmt.Errorf("failed to read tar archive: %w", err))
322+
}
323+
return nil, c.setError(fmt.Errorf("tmpfs %q has tar range [%d, %d) beyond multi-tar file size %d", mt.ResourceID, mt.TarStart, mt.TarEnd, len(multiTar)))
324+
}
325+
326+
// wait blocks until either all filesystems have been restored for the
327+
// container with the given ID, or an error occurs while restoring filesystems
328+
// for that container.
329+
func (fsr *fsRestore) wait(cid string) error {
330+
if fsr == nil {
331+
return fmt.Errorf("filesystem restore is not enabled")
332+
}
333+
fsr.wg.Wait()
334+
if fsr.manifestErr != nil {
335+
return fsr.manifestErr
336+
}
337+
fsr.waitMu.Lock()
338+
defer fsr.waitMu.Unlock()
339+
c := fsr.waitMap[cid]
340+
if c == nil {
341+
return fmt.Errorf("no filesystems restored for container %s", cid)
342+
}
343+
for {
344+
if c.err != nil {
345+
return c.err
346+
}
347+
if c.asyncLoads == 0 {
348+
return nil
349+
}
350+
c.cond.Wait()
268351
}
269-
return nil, fmt.Errorf("tmpfs %q has tar range [%d, %d) beyond multi-tar file size %d", mt.ResourceID, mt.TarStart, mt.TarEnd, len(multiTar))
270352
}

0 commit comments

Comments
 (0)