From 468f45251d10a7c774d0b74c54f058493cff802a Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Sat, 19 Sep 2026 07:05:32 -0700 Subject: [PATCH] priv: a failed root probe is retried, not cached for the life of the daemon The Mac mini's daemon started while the machine was still booting, sudo -n true took longer than the 3 s timeout, and sync.Once kept the answer: no root, so the next self-update could not replace the binary. Co-Authored-By: Claude Fable 5.1 --- internal/priv/priv.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/internal/priv/priv.go b/internal/priv/priv.go index 55e51da..a156328 100644 --- a/internal/priv/priv.go +++ b/internal/priv/priv.go @@ -13,12 +13,24 @@ import ( ) var ( - once sync.Once - mode string // "root" | "sudo" | "doas" | "" - prefix []string + mu sync.Mutex + mode string // "root" | "sudo" | "doas" | "" + prefix []string + probed time.Time + reprobe = 30 * time.Second ) +// detect settles how root is reached. Finding a way is remembered for good; finding none +// is only believed for a while, because the probe can fail for reasons that pass — sudo +// taking longer than the timeout on a machine that is still booting left a daemon +// convinced it had no root until its next restart. func detect() { + mu.Lock() + defer mu.Unlock() + if mode != "" || (!probed.IsZero() && time.Since(probed) < reprobe) { + return + } + probed = time.Now() if os.Geteuid() == 0 { mode = "root" return @@ -38,7 +50,7 @@ func detect() { } // Mode reports how root is reached: "root", "sudo", "doas", or "" when it can't be. -func Mode() string { once.Do(detect); return mode } +func Mode() string { detect(); mu.Lock(); defer mu.Unlock(); return mode } // Available reports whether commands can be run as root without a prompt. func Available() bool { return Mode() != "" } @@ -46,7 +58,9 @@ func Available() bool { return Mode() != "" } // Wrap returns args prefixed so they run as root. Without privilege it returns args // unchanged; callers gate on Available() where that matters. func Wrap(args ...string) []string { - once.Do(detect) + detect() + mu.Lock() + defer mu.Unlock() return append(append([]string{}, prefix...), args...) }