feat(auth): coordinate canonical projection publication
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user