mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
a0d6437ae6
testtmp.Sweep judged a template directory's owner dead with kill(pid,0)
on the pid in its name. A pid only means something inside one PID
namespace, and /tmp is shared across namespaces. A test binary run under
bwrap --unshare-pid got ESRCH for every host pid and deleted live
storetest templates mid-run ("copy template db: no such file"). In the
other direction, a pid recorded inside a sandbox can name an unrelated
host process and keep a dead owner's directory forever.
MkdirTemp now builds the directory under a staging name no sweep
matches, takes a flock on owner.lock, and renames it into place. The
lock follows the inode across the rename, and the fd is O_CLOEXEC and
held for the life of the process. Sweep removes a directory only when
it can take that lock. A directory with no owner.lock (made before this
change) keeps the old pid rule. Windows is unchanged.
Claude-Session: https://claude.ai/code/session_01Hk4vxsaKDm3E1LvsoHtHeV
64 lines
2.0 KiB
Go
64 lines
2.0 KiB
Go
//go:build !windows
|
|
|
|
package testtmp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// mkdirOwned builds the directory under a staging name, takes the flock on
|
|
// its owner.lock, and only then renames it to prefix<pid>-<random>, so a
|
|
// sweep never sees a directory of that shape without a held lock. The flock
|
|
// belongs to the open file, which refers to the inode, so it survives the
|
|
// rename. os.OpenFile opens with O_CLOEXEC, so a child the test spawns does
|
|
// not inherit the lock and keep a dead owner's directory looking alive.
|
|
func mkdirOwned(dir, prefix string) (string, *os.File, error) {
|
|
staging, err := os.MkdirTemp(dir, stagingPattern(prefix))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
fail := func(f *os.File, err error) (string, *os.File, error) {
|
|
if f != nil {
|
|
_ = f.Close()
|
|
}
|
|
_ = os.RemoveAll(staging)
|
|
return "", nil, err
|
|
}
|
|
f, err := os.OpenFile(filepath.Join(staging, LockName), os.O_RDWR|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW, 0o600)
|
|
if err != nil {
|
|
return fail(nil, err)
|
|
}
|
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
return fail(f, fmt.Errorf("lock %s: %w", f.Name(), err))
|
|
}
|
|
random := staging[strings.LastIndex(staging, "-")+1:]
|
|
final := filepath.Join(dir, fmt.Sprintf("%s%d-%s", prefix, os.Getpid(), random))
|
|
if err := os.Rename(staging, final); err != nil {
|
|
return fail(f, err)
|
|
}
|
|
return final, f, nil
|
|
}
|
|
|
|
// ownerState tests path's owner.lock. When it is free the caller now holds
|
|
// it, and must call release after removing the directory.
|
|
func ownerState(path string) (ownership, func()) {
|
|
f, err := os.OpenFile(filepath.Join(path, LockName), os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return ownerNoLock, nil
|
|
}
|
|
return ownerHeld, nil
|
|
}
|
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
_ = f.Close()
|
|
return ownerHeld, nil
|
|
}
|
|
return ownerGone, func() { _ = f.Close() }
|
|
}
|