//go:build linux package authconfig import ( "context" "errors" "os" "sync" "time" "golang.org/x/sys/unix" ) type transactionLock struct { directoryFD int fileFD int closed bool } type projectionLockHooks struct { beforeCreate func() afterOpen func() } var projectionLockHookState struct { sync.Mutex hooks projectionLockHooks } func setProjectionLockHooksForTest(hooks projectionLockHooks) func() { projectionLockHookState.Lock() previous := projectionLockHookState.hooks projectionLockHookState.hooks = hooks projectionLockHookState.Unlock() return func() { projectionLockHookState.Lock() projectionLockHookState.hooks = previous projectionLockHookState.Unlock() } } func projectionLockHooksForTest() projectionLockHooks { projectionLockHookState.Lock() defer projectionLockHookState.Unlock() return projectionLockHookState.hooks } func acquireTransactionLock(ctx context.Context, directory string) (*transactionLock, error) { if ctx == nil || ctx.Err() != nil || requirePrivateDirectory(directory) != nil { return nil, errProjectionIntegrity } directoryFD, err := unix.Open(directory, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0) if err != nil { return nil, errProjectionIntegrity } lock := &transactionLock{directoryFD: directoryFD, fileFD: -1} fail := func() (*transactionLock, error) { _ = lock.Close() return nil, errProjectionIntegrity } if !validTransactionDirectory(directoryFD) { return fail() } fileFD, err := openTransactionLockFile(directoryFD) if err != nil { return fail() } lock.fileFD = fileFD if hook := projectionLockHooksForTest().afterOpen; hook != nil { hook() } var opened unix.Stat_t if unix.Fstat(fileFD, &opened) != nil || !validTransactionLockFile(opened) || !sameTransactionLockName(directoryFD, opened) { return fail() } if err := flockTransactionFile(ctx, fileFD); err != nil { _ = lock.Close() return nil, err } if !sameTransactionLockName(directoryFD, opened) { return fail() } return lock, nil } func openTransactionLockFile(directoryFD int) (int, error) { for attempt := 0; attempt < 2; attempt++ { fd, err := unix.Openat(directoryFD, transactionLockFileName, unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) if err == nil { return fd, nil } if !errors.Is(err, unix.ENOENT) { return -1, errProjectionIntegrity } if hook := projectionLockHooksForTest().beforeCreate; hook != nil { hook() } fd, err = unix.Openat(directoryFD, transactionLockFileName, unix.O_RDWR|unix.O_CREAT|unix.O_EXCL|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0o600) if err == nil { if unix.Fchmod(fd, 0o600) != nil || unix.Fsync(fd) != nil || unix.Fsync(directoryFD) != nil { unix.Close(fd) return -1, errProjectionIntegrity } return fd, nil } if !errors.Is(err, unix.EEXIST) { return -1, errProjectionIntegrity } } return -1, errProjectionIntegrity } func validTransactionDirectory(fd int) bool { var stat unix.Stat_t return unix.Fstat(fd, &stat) == nil && stat.Mode&unix.S_IFMT == unix.S_IFDIR && stat.Uid == uint32(os.Geteuid()) && stat.Mode&0o077 == 0 } func validTransactionLockFile(stat unix.Stat_t) bool { return stat.Mode&unix.S_IFMT == unix.S_IFREG && stat.Nlink == 1 && stat.Uid == uint32(os.Geteuid()) && stat.Mode&0o7777 == 0o600 } func sameTransactionLockName(directoryFD int, opened unix.Stat_t) bool { var named unix.Stat_t return unix.Fstatat(directoryFD, transactionLockFileName, &named, unix.AT_SYMLINK_NOFOLLOW) == nil && validTransactionLockFile(named) && named.Dev == opened.Dev && named.Ino == opened.Ino } func flockTransactionFile(ctx context.Context, fd int) error { for { if err := ctx.Err(); err != nil { return err } err := unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB) if err == nil { return nil } if !errors.Is(err, unix.EWOULDBLOCK) { return errProjectionIntegrity } if hook := coordinatorHooks().onOuterLockContention; hook != nil { hook() } timer := time.NewTimer(10 * time.Millisecond) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() case <-timer.C: } } } func (lock *transactionLock) Close() error { if lock == nil || lock.closed { return nil } lock.closed = true var err error if lock.fileFD >= 0 { if unlockErr := unix.Flock(lock.fileFD, unix.LOCK_UN); unlockErr != nil { err = errors.Join(err, errProjectionCleanup) } if closeErr := unix.Close(lock.fileFD); closeErr != nil { err = errors.Join(err, errProjectionCleanup) } lock.fileFD = -1 } if lock.directoryFD >= 0 { if closeErr := unix.Close(lock.directoryFD); closeErr != nil { err = errors.Join(err, errProjectionCleanup) } lock.directoryFD = -1 } return err }