iflytek/astron-agent · error

credential file changed while being opened

Error message

credential file changed while being opened

What it means

After opening, openCredentialFileNoFollow re-stats the open file and compares it (os.SameFile) with the pre-open Lstat result. If the stat fails, the file is no longer a regular file, or the inode changed, it concludes the credential file was replaced or modified during the open window and refuses to return the handle. This closes the TOCTOU race where a symlink/regular-file swap happens between check and open.

Solutions

  1. Retry the startup once the credential rotation completes; make rotation tools pause or signal the service after writing
  2. Use atomic write conventions that finish before the service reads (write temp + rename before service start), or restart the service after rotation
  3. Serialize credential deployment with service startup (dependency ordering in systemd/compose)
  4. Check for competing processes (two rotation agents) writing the same path

Example fix

// before
* * * * * /usr/bin/fetch-secret > /etc/tenant/credential.key   # overwrites in place, races readers
// after
* * * * * /usr/bin/fetch-secret > /etc/tenant/credential.key.tmp && mv /etc/tenant/credential.key.tmp /etc/tenant/credential.key && systemctl try-restart tenant
Defensive patterns

Strategy: retry

Validate before calling

// No reliable pre-check can prevent an open-window swap; instead, after opening,
// confirm stability:
info1, _ := os.Stat(path)
info2, _ := os.Open(path) // then stat fd and os.SameFile(info1, fdInfo)

Try / catch

var f *os.File
err := retry.Do(func() error {
    var err error
    f, err = openCredentialFileNoFollow(path)
    return err
}, retry.Attempts(3), retry.Delay(100*time.Millisecond))
if err != nil {
    return fmt.Errorf("credential file kept changing during open (rotation race?): %w", err)
}

Prevention

When it happens

Trigger: File replaced (rename/unlink+create) between Lstat and Open; Stat on the open fd fails; the opened fd points at a non-regular file; hard link swapped to a different inode mid-open.

Common situations: Secret rotation tools (vault-agent, consul-template, k8s secret remount) atomically replacing the credential file exactly while the service is starting; concurrent deploys writing the credential file; automated config sync jobs racing service startup.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/4234ef704a01ebec. Report an issue: GitHub.

Appendix: source

Thrown at core/tenant/config/credential_file_other.go:29

// O_NOFOLLOW. Supported production images use the Unix implementation above.
func openCredentialFileNoFollow(fileName string) (*os.File, error) {
	pathInfo, err := os.Lstat(fileName)
	if err != nil {
		return nil, errors.New("credential file is unavailable")
	}
	if pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.Mode().IsRegular() {
		return nil, errors.New(
			"credential file must be a regular non-symbolic-link file",
		)
	}
	file, err := os.Open(fileName)
	if err != nil {
		return nil, errors.New("credential file is unavailable")
	}
	openedInfo, err := file.Stat()
	if err != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(pathInfo, openedInfo) {
		_ = file.Close()
		return nil, errors.New("credential file changed while being opened")
	}
	return file, nil
}

View on GitHub (pinned to 5e758547a8)