Files
podman/libpod/lock/file/file_lock.go
Daniel J Walsh 25d66d97d2 Additional potential race condition on os.Readdir
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
2024-08-12 11:38:02 -04:00

180 lines
4.6 KiB
Go

package file
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"syscall"
"github.com/containers/storage/pkg/fileutils"
"github.com/containers/storage/pkg/lockfile"
"github.com/sirupsen/logrus"
)
// FileLocks is a struct enabling POSIX lock locking in a shared memory
// segment.
type FileLocks struct { //nolint:revive // struct name stutters
lockPath string
valid bool
}
// CreateFileLock sets up a directory containing the various lock files.
func CreateFileLock(path string) (*FileLocks, error) {
err := fileutils.Exists(path)
if err == nil {
return nil, fmt.Errorf("directory %s exists: %w", path, syscall.EEXIST)
}
if err := os.MkdirAll(path, 0711); err != nil {
return nil, err
}
locks := new(FileLocks)
locks.lockPath = path
locks.valid = true
return locks, nil
}
// OpenFileLock opens an existing directory with the lock files.
func OpenFileLock(path string) (*FileLocks, error) {
err := fileutils.Exists(path)
if err != nil {
return nil, err
}
locks := new(FileLocks)
locks.lockPath = path
locks.valid = true
return locks, nil
}
// Close closes an existing shared-memory segment.
// The segment will be rendered unusable after closing.
// WARNING: If you Close() while there are still locks locked, these locks may
// fail to release, causing a program freeze.
// Close() is only intended to be used while testing the locks.
func (locks *FileLocks) Close() error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
err := os.RemoveAll(locks.lockPath)
if err != nil {
return fmt.Errorf("deleting directory %s: %w", locks.lockPath, err)
}
return nil
}
func (locks *FileLocks) getLockPath(lck uint32) string {
return filepath.Join(locks.lockPath, strconv.FormatInt(int64(lck), 10))
}
// AllocateLock allocates a lock and returns the index of the lock that was allocated.
func (locks *FileLocks) AllocateLock() (uint32, error) {
if !locks.valid {
return 0, fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
id := uint32(0)
for ; ; id++ {
path := locks.getLockPath(id)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
continue
}
return 0, fmt.Errorf("creating lock file: %w", err)
}
f.Close()
break
}
return id, nil
}
// AllocateGivenLock allocates the given lock from the shared-memory
// segment for use by a container or pod.
// If the lock is already in use or the index is invalid an error will be
// returned.
func (locks *FileLocks) AllocateGivenLock(lck uint32) error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
f, err := os.OpenFile(locks.getLockPath(lck), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return fmt.Errorf("creating lock %d: %w", lck, err)
}
f.Close()
return nil
}
// DeallocateLock frees a lock in a shared-memory segment so it can be
// reallocated to another container or pod.
// The given lock must be already allocated, or an error will be returned.
func (locks *FileLocks) DeallocateLock(lck uint32) error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
if err := os.Remove(locks.getLockPath(lck)); err != nil {
return fmt.Errorf("deallocating lock %d: %w", lck, err)
}
return nil
}
// DeallocateAllLocks frees all locks so they can be reallocated to
// other containers and pods.
func (locks *FileLocks) DeallocateAllLocks() error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
files, err := os.ReadDir(locks.lockPath)
if err != nil {
return fmt.Errorf("reading directory %s: %w", locks.lockPath, err)
}
var lastErr error
for _, f := range files {
p := filepath.Join(locks.lockPath, f.Name())
err := os.Remove(p)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
logrus.Errorf("Deallocating lock %s", p)
}
}
return lastErr
}
// LockFileLock locks the given lock.
func (locks *FileLocks) LockFileLock(lck uint32) error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
l, err := lockfile.GetLockFile(locks.getLockPath(lck))
if err != nil {
return fmt.Errorf("acquiring lock: %w", err)
}
l.Lock()
return nil
}
// UnlockFileLock unlocks the given lock.
func (locks *FileLocks) UnlockFileLock(lck uint32) error {
if !locks.valid {
return fmt.Errorf("locks have already been closed: %w", syscall.EINVAL)
}
l, err := lockfile.GetLockFile(locks.getLockPath(lck))
if err != nil {
return fmt.Errorf("acquiring lock: %w", err)
}
l.Unlock()
return nil
}