Skip to content

fix(lock): replace the PID lockfile with an OS lock - #241

Merged
ashishkurmi merged 10 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/dmg-lock-flock
Oct 17, 2026
Merged

ashishkurmi merged 10 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/dmg-lock-flock

Conversation

@swarit-stepsecurity

Copy link
Copy Markdown
Member

The PID lockfile could block every later run, so devices went inactive and never came back:

  • A root run that died left a 0600 root-owned file in sticky /tmp. User runs couldn't read it or delete it, so they were refused forever.
  • A recycled PID, or EPERM, counted as a live holder. On Windows %TEMP% survives reboots, so this persisted.
  • Anyone could write 1 into the file and disable the agent.
  • The signal handler and watchdogs exited without releasing the lock.
  • SYSTEM and user runs on Windows never excluded each other (per-user %TEMP%).

Changes:

  • Unix: flock on /tmp/stepsecurity-dev-machine-guard.flock, a new name so legacy PID files are ignored. The file is opened O_RDONLY|O_NOFOLLOW with mode 0644 so root and user runs can both open it, and it is never unlinked.
  • Windows: a Global\StepSecurityDevMachineGuard mutex any account can open, held on a locked OS thread. WAIT_ABANDONED counts as acquired. Falls back to Local\.
  • The kernel releases the lock on any exit, including SIGKILL and os.Exit.
  • The serial is resolved before the lock, so a refused run reports a device_id; the backend 400s an empty one, which made contention invisible. Contention is reported as lock_held: ….

Known limit: a local user can still hold the lock open and block runs. That is narrower than before, since it needs a live process; fully closing it needs a root-owned lock directory.

Testing: go test ./..., go vet for windows/linux/darwin. Linux lock tests also ran in a container, covering a root holder vs a user acquirer and reacquiring after SIGKILL. Not run on Windows.

The PID-file lock could be wedged by a crashed root run (0600 file in
sticky /tmp), a recycled PID, or any user writing a live PID into it,
and was never released on os.Exit/SIGKILL.

Unix: flock on /tmp/stepsecurity-dev-machine-guard.flock (new name, so
legacy PID files are ignored), opened O_RDONLY|O_NOFOLLOW, mode 0644.
Windows: Global\StepSecurityDevMachineGuard mutex open to every account,
falling back to Local\. The OS releases both on any exit.
The failure report for a refused lock was sent before device.Gather, so
device_id was empty and the backend rejected it with 400. Probe the
serial before taking the lock, and prefix contention with lock_held:.
Holding the real /tmp lock would refuse an agent run on the same machine.
Comment thread internal/lock/lock_unix.go Fixed
@ashishkurmi
ashishkurmi requested a balanced review from Copilot October 14, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unix acquisition can block on a planted FIFO, and the new tests have Windows naming and lock-lifetime defects.

3 open findings
What changed in this PR

Replaces PID lockfiles with kernel-managed locks to prevent stale locks from blocking agent runs.

Changes:

  • Uses Unix flock and Windows named mutexes.
  • Resolves device identity before locking and identifies contention reports.
  • Adds lock lifecycle tests and updates watchdog comments.
File Description
internal/​telemetry/​telemetry.go Resolves device identity before acquisition and labels contention.
internal/​telemetry/​run_lock_test.go Tests contention reporting with device identity.
internal/​lock/​lock.go Defines shared acquisition and idempotent release.
internal/​lock/​lock_windows.go Implements named mutex locking.
internal/​lock/​lock_windows_test.go Provides isolated test mutex names.
internal/​lock/​lock_unix.go Implements persistent-file flock locking.
internal/​lock/​lock_unix_test.go Tests file permissions and legacy-file handling.
internal/​lock/​lock_test.go Tests contention, release, and process termination.
cmd/​stepsecurity-dev-machine-guard/​watchdog_windows.go Updates comments about kernel lock cleanup.
cmd/​stepsecurity-dev-machine-guard/​watchdog_unix.go Updates comments about kernel lock cleanup.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/lock/lock_unix.go Outdated
Comment on lines +51 to +62
const flags = syscall.O_RDONLY | syscall.O_NOFOLLOW | syscall.O_CLOEXEC
for range 2 {
fd, err := syscall.Open(path, flags, 0)
if errors.Is(err, syscall.ENOENT) {
fd, err = syscall.Open(path, flags|syscall.O_CREAT|syscall.O_EXCL, lockFileMode)
if errors.Is(err, syscall.EEXIST) {
continue // lost a create race; open the winner's file
}
}
if err != nil {
return nil, fmt.Errorf("opening lock file %s: %w", path, err)
}
Comment thread internal/telemetry/run_lock_test.go Outdated
// Hold a private instance lock so a real agent on this machine is unaffected.
target := filepath.Join(t.TempDir(), "test.flock")
if runtime.GOOS == "windows" {
target = fmt.Sprintf(`Local\dmg-lock-test-%d`, os.Getpid())
Comment thread internal/lock/lock_test.go Outdated
Comment on lines +21 to +24
if _, err := Acquire(nil); err != nil {
fmt.Println("acquire failed:", err)
os.Exit(1)
}
@ashishkurmi
ashishkurmi added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 17, 2026
…ock-flock

# Conflicts:
#	cmd/stepsecurity-dev-machine-guard/watchdog_windows.go
@ashishkurmi
ashishkurmi added this pull request to the merge queue Oct 17, 2026
Merged via the queue into step-security:main with commit 64df9e1 Oct 17, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants