@@ -275,6 +275,11 @@ func NewCacheFromMemfdDeduped(
275275 cancel : cancel ,
276276 done : utils .NewSetOnce [* Cache ](),
277277 inflight : inflightServe ,
278+ // Publish the memfd up front (guarded by mu) so a provisional-header
279+ // resume can serve dirty pages via ServeMemfd during the compare window,
280+ // before metaOut/the deduped header resolves. runDedup frees it under mu
281+ // after the drain.
282+ memfd : memfd ,
278283 }
279284 go d .runDedup (drainCtx , base , blockSize , memfd , dirty , bestEffort , directIO , budget , inputEmpty , metaOut )
280285
@@ -353,7 +358,7 @@ func (d *DedupedMemfdCache) runDedup(
353358 compareDur := time .Since (compareStart )
354359 if err != nil {
355360 logSetOnceErr (ctx , "dedup metaOut" , metaOut .SetError (err ))
356- logSetOnceErr (ctx , "dedup done" , d .done .SetError (errors .Join (err , memfd . Close ())))
361+ logSetOnceErr (ctx , "dedup done" , d .done .SetError (errors .Join (err , d . releaseMemfd ())))
357362
358363 return
359364 }
@@ -388,11 +393,7 @@ func (d *DedupedMemfdCache) runDedup(
388393 writeStart := time .Now ()
389394 cache , err := dedupDrain (ctx , src , plan .pageDirty , blockSize , d .outPath , directIO )
390395 writeDur := time .Since (writeStart )
391- d .mu .Lock ()
392- closeErr := memfd .Close ()
393- d .memfd = nil
394- d .mu .Unlock ()
395- if closeErr != nil {
396+ if closeErr := d .releaseMemfd (); closeErr != nil {
396397 logger .L ().Warn (ctx , "close memfd after dedup drain" , zap .Error (closeErr ))
397398 }
398399
@@ -475,6 +476,40 @@ func (d *DedupedMemfdCache) Slice(off, length int64) ([]byte, error) {
475476 return c .Slice (off , length )
476477}
477478
479+ // releaseMemfd closes the memfd exactly once, under the write lock so it can't
480+ // be unmapped beneath an in-flight ServeMemfd/tryInflightRead reader.
481+ func (d * DedupedMemfdCache ) releaseMemfd () error {
482+ d .mu .Lock ()
483+ defer d .mu .Unlock ()
484+ if d .memfd == nil {
485+ return nil
486+ }
487+ err := d .memfd .Close ()
488+ d .memfd = nil
489+
490+ return err
491+ }
492+
493+ // ServeMemfd copies [off, off+len(b)) from the still-mapped memfd using
494+ // identity (device) addressing, for a provisional local header that attributes
495+ // dirty pages to the memfd at their device offset. Returns BytesNotAvailableError
496+ // once the memfd has been released (drain finished), so the caller falls back to
497+ // the drained cache via the swapped-in deduped header.
498+ func (d * DedupedMemfdCache ) ServeMemfd (b []byte , off int64 ) (int , error ) {
499+ d .mu .RLock ()
500+ defer d .mu .RUnlock ()
501+ if d .memfd == nil {
502+ return 0 , BytesNotAvailableError {}
503+ }
504+ src , err := d .memfd .Slice (off , int64 (len (b )))
505+ if err != nil {
506+ return 0 , err
507+ }
508+ copy (b , src )
509+
510+ return len (b ), nil
511+ }
512+
478513func (d * DedupedMemfdCache ) Close () error {
479514 d .cancel ()
480515 c , _ := d .done .Wait ()
@@ -486,6 +521,51 @@ func (d *DedupedMemfdCache) Close() error {
486521 return nil
487522}
488523
524+ // MemfdIdentitySource is the provisional local diff source: it serves dirty
525+ // pages from a DedupedMemfdCache's still-mapped memfd at identity (device)
526+ // offsets while dedup runs. It backs a distinct provisional build id; once the
527+ // deduped header is swapped in, reads route to the real build id instead and
528+ // this source is dropped. It is never uploaded.
529+ type MemfdIdentitySource struct {
530+ d * DedupedMemfdCache
531+ size int64
532+ }
533+
534+ func NewMemfdIdentitySource (d * DedupedMemfdCache , size int64 ) * MemfdIdentitySource {
535+ return & MemfdIdentitySource {d : d , size : size }
536+ }
537+
538+ func (s * MemfdIdentitySource ) ReadAt (b []byte , off int64 ) (int , error ) { return s .d .ServeMemfd (b , off ) }
539+
540+ func (s * MemfdIdentitySource ) Slice (off , length int64 ) ([]byte , error ) {
541+ b := make ([]byte , length )
542+ if _ , err := s .d .ServeMemfd (b , off ); err != nil {
543+ return nil , err
544+ }
545+
546+ return b , nil
547+ }
548+
549+ // IsCached reports the provisional source as resident while the memfd is mapped,
550+ // so dedup best-effort peeks and build.File.IsCached treat it as local.
551+ func (s * MemfdIdentitySource ) IsCached (_ context.Context , off , length int64 ) bool {
552+ s .d .mu .RLock ()
553+ defer s .d .mu .RUnlock ()
554+
555+ return s .d .memfd != nil && off >= 0 && off + length <= s .size
556+ }
557+
558+ func (s * MemfdIdentitySource ) Size () (int64 , error ) { return s .size , nil }
559+ func (s * MemfdIdentitySource ) BlockSize () int64 { return header .PageSize }
560+ func (s * MemfdIdentitySource ) Close () error { return nil }
561+ func (s * MemfdIdentitySource ) FileSize (context.Context ) (int64 , error ) { return s .size , nil }
562+
563+ // Path has no meaning for a memfd-backed source; it is local-only and never
564+ // uploaded, so the upload path (which needs a file path) must never reach it.
565+ func (s * MemfdIdentitySource ) Path (context.Context ) (string , error ) {
566+ return "" , errors .New ("provisional memfd source has no path" )
567+ }
568+
489569func (d * DedupedMemfdCache ) Path (ctx context.Context ) (string , error ) {
490570 c , err := d .Wait (ctx )
491571 if err != nil {
0 commit comments