package vault import ( "fmt" "os" "path/filepath" "sync" "syscall" "git.eeqj.de/sneak/secret/internal/secret" "github.com/spf13/afero" ) // lockFileName is the file in the state directory that LockStateDir locks. const lockFileName = "lock" // memFsLock stands in for the lock file on the in-memory filesystem, which // has no file locks. Every in-memory filesystem in the process shares it. // //nolint:gochecknoglobals // must outlive the call that takes it var memFsLock sync.Mutex // LockStateDir takes the lock that a command changing anything under // stateDir holds until it returns, and returns the function that releases // it. While one command holds it, the next one waits here. Reads take no // lock: each file or directory a command changes is replaced in a single // rename, so a reader finds it as it was before or after, never half-made. // // On the real filesystem the lock is flock(2) on the file "lock" in // stateDir, which the kernel releases when the process dies, so a killed // command never leaves the tool locked. The in-memory filesystem the tests // use has no file locks, so a process-wide mutex stands in for flock there. // Any other filesystem is refused rather than left unlocked. func LockStateDir(fs afero.Fs, stateDir string) (func(), error) { switch fs.(type) { case *afero.OsFs: return flockStateDir(stateDir) case *afero.MemMapFs: memFsLock.Lock() return memFsLock.Unlock, nil default: return nil, fmt.Errorf("%w %T", ErrNoLockForFilesystem, fs) } } // flockStateDir takes flock(2) on the lock file in stateDir, creating the // directory and the file if needed. Go opens files close-on-exec, so // programs the command runs, such as gpg, do not inherit the lock. func flockStateDir(stateDir string) (func(), error) { err := os.MkdirAll(stateDir, secret.DirPerms) if err != nil { return nil, fmt.Errorf("failed to create state directory: %w", err) } lockPath := filepath.Join(stateDir, lockFileName) //nolint:gosec // G304: the path is the lock file in the state directory file, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE, secret.FilePerms) if err != nil { return nil, fmt.Errorf("failed to open lock file: %w", err) } err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX) if err != nil { _ = file.Close() return nil, fmt.Errorf("failed to lock %s: %w", lockPath, err) } // Closing the file releases the lock. return func() { _ = file.Close() }, nil }