Skip to content

Commit 64a8b17

Browse files
committed
nsenter: use TryLock in reaper to prevent cascading FUSE deadlock
Replace mu.Lock() with mu.TryLock() in the zombie reaper goroutine. Go's sync.RWMutex implements writer-preference: a pending Lock() blocks all subsequent RLock() callers. When the reaper's Lock() is pending while an nsenter child holds RLock, all new FUSE Lookup handlers are blocked from acquiring RLock to start their own nsenter processes. If the nsenter child's operation triggers a FUSE request back to sysbox-fs, the FUSE handler cannot proceed, creating a deadlock. TryLock() does not register a pending writer, so FUSE handlers can still acquire RLock. If TryLock fails, the reaper sleep a second to retry. Fixes: nestybox/sysbox#1002
1 parent b70bd38 commit 64a8b17

1 file changed

Lines changed: 11 additions & 15 deletions

File tree

nsenter/reaper.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,30 +63,26 @@ func reaper(signal chan bool, mu *sync.RWMutex) {
6363
for {
6464
<-signal
6565

66-
// Without this delay, sysbox-fs sometimes hangs the FUSE request that generates an
67-
// nsenter event that requires reaping. It's not clear why, but the tell-tale sign
68-
// of the hang is that the reaper is signaled but finds nothing to reap. This delay
69-
// mitigates this condition and the reaper finds something to reap.
70-
//
71-
// The delay chosen is one that allows nsenter agents to complete their tasks before
72-
// reaping occurs. Since the reaper runs in its own goroutine, this delay only
73-
// affects it (there is no undesired side-effect on nsenters).
74-
75-
time.Sleep(time.Second)
76-
66+
// Use TryLock instead of Lock to avoid blocking subsequent RLock
67+
// callers. Go's sync.RWMutex implements writer-preference: a pending
68+
// Lock() blocks all new RLock() calls. If an nsenter child triggers
69+
// a FUSE request back to sysbox-fs, the FUSE handler needs RLock to
70+
// start a new nsenter. A blocking Lock() here would prevent that
71+
// RLock, causing a deadlock. TryLock avoids this by simply skipping
72+
// the reap cycle when nsenters are active
73+
for !mu.TryLock() {
74+
time.Sleep(time.Millisecond * 100)
75+
}
7776
for {
78-
mu.Lock()
79-
8077
// WNOHANG: if there is no child to reap, don't block
8178
wpid, err := syscall.Wait4(-1, &wstatus, syscall.WNOHANG, nil)
8279
if err != nil || wpid == 0 {
8380
logrus.Infof("reaper: nothing to reap")
84-
mu.Unlock()
8581
break
8682
}
8783

8884
logrus.Infof("reaper: reaped pid %d", wpid)
89-
mu.Unlock()
9085
}
86+
mu.Unlock()
9187
}
9288
}

0 commit comments

Comments
 (0)