AUTH-4699, AUTH-8460, TUN-10179: Fix .lock file deletion race condition

Replace the lock file mechanism with PID+start-time based stale
detection so that no cleanup is required on process death.

When both org and app token locks were held, the first signal handler
to call os.Exit() would kill the process before the second handler
could delete its lock file. The orphaned lock file then caused the
next invocation to wait ~128 seconds in an exponential backoff loop
before forcibly deleting it. The same issue occurred on SIGKILL, OOM,
or any non-signal death.

Lock files now contain the holder's PID and process start time as
JSON. On acquisition, if a lock file already exists, the recorded
process is checked for liveness via gopsutil. Stale locks are
reclaimed immediately with no backoff. Atomic O_CREATE|O_EXCL
prevents races between concurrent acquirers.

Also adds a companion .url file so processes waiting on an active
lock can print the auth URL for the user.
This commit is contained in:
Evan Raw
2026-04-21 17:41:00 -05:00
parent 23b15d0eb6
commit da81fb02ec
5 changed files with 293 additions and 151 deletions

View File

@@ -26,7 +26,10 @@ const (
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger) ([]byte, error) {
//
// If urlFilePath is non-empty, the generated auth URL is written to that path so
// other waiting processes can display it to the user. Pass "" to skip.
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger, urlFilePath string) ([]byte, error) {
encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
@@ -36,6 +39,11 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
return nil, err
}
// write auth URL to companion file so other waiting processes can display it
if urlFilePath != "" {
_ = os.WriteFile(urlFilePath, []byte(requestURL), 0600) // nolint: gosec
}
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
err = OpenBrowser(requestURL)
if err != nil {
@@ -129,11 +137,11 @@ func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte,
return nil, "", err
}
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
resp, err := client.Do(req) // nolint: gosec
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
// ignore everything other than server errors as the resource
// may not exist until the user does the interaction