//go:build unix package hostguard import ( "context" "errors" "os" "time" "golang.org/x/sys/unix" ) func acquire(ctx context.Context, dir, host string, verifyOwner ...bool) (func(), error) { if ctx == nil { return nil, ErrInvalidInput } if err := ctx.Err(); err != nil { return nil, err } if err := validateFileHost(host); err != nil { return nil, err } dirFD, err := openDirectory(dir) if err != nil { return nil, err } defer unix.Close(dirFD) var parent unix.Stat_t if err := unix.Fstat(dirFD, &parent); err != nil { return nil, err } unsafe := func(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT != unix.S_IFREG || (len(verifyOwner) == 1 && verifyOwner[0] && st.Uid != 0 && st.Uid != parent.Uid) } var prior unix.Stat_t if err := unix.Fstatat(dirFD, host+".lock", &prior, unix.AT_SYMLINK_NOFOLLOW); err == nil && unsafe(prior) { return nil, ErrUnsafeFile } flags := unix.O_RDWR | unix.O_CREAT | unix.O_NOFOLLOW | unix.O_CLOEXEC | unix.O_NONBLOCK fd, err := unix.Openat(dirFD, host+".lock", flags|unix.O_EXCL, 0660) created := err == nil if errors.Is(err, unix.EEXIST) { fd, err = unix.Openat(dirFD, host+".lock", flags, 0660) } if err != nil { if errors.Is(err, unix.ELOOP) || errors.Is(err, unix.EISDIR) { return nil, ErrUnsafeFile } return nil, err } f := os.NewFile(uintptr(fd), host+".lock") fail := func(err error) (func(), error) { f.Close(); return nil, err } var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return fail(err) } if unsafe(st) { return fail(ErrUnsafeFile) } if created && len(verifyOwner) == 1 && verifyOwner[0] && os.Geteuid() == 0 { if err := unix.Fchown(fd, -1, int(parent.Gid)); err != nil { return fail(err) } } if created || st.Mode&07777 != 0660 { if err := unix.Fchmod(fd, 0660); err != nil { return fail(err) } } delay := 10 * time.Millisecond for { if err := ctx.Err(); err != nil { return fail(err) } err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) if err == nil { unlock := func() { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN); _ = f.Close() } if err := ctx.Err(); err != nil { unlock() return nil, err } return unlock, nil } if !errors.Is(err, unix.EWOULDBLOCK) && !errors.Is(err, unix.EAGAIN) && !errors.Is(err, unix.EINTR) { return fail(err) } timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return fail(ctx.Err()) case <-timer.C: } delay = min(2*delay, 100*time.Millisecond) } }