Release MCP OAuth read locks before keyring access (#61401)

## Why

Encrypted MCP OAuth reads hold a shared file lock while accessing the keyring, blocking writers until the keyring lookup and decryption finish.

## What changed

- Add `LocalSecretsSnapshot` to capture ciphertext under the file lock, then release shared read locks before keyring access and decryption. Keep exclusive locks across conditional credential updates.
- Reuse the loaded passphrase when updating existing secrets, avoiding a second keyring lookup. Return an error when an existing encrypted file has no keyring passphrase instead of generating a replacement.
- Update login storage error fixtures to use directory reads that fail consistently across platforms.

## Testing

Add regression coverage for releasing read locks during keyring access, updating secrets with one keyring lookup, rejecting missing passphrases on cached reads, and reading captured snapshots after later writes.

GitOrigin-RevId: add8f60dba52f71c8f8bc10ae8b8ae2f3379ccac
This commit is contained in:
Celia Chen
2026-11-13 23:15:54 +00:00
committed by copyberry
parent 0b297ec5ee
commit 43ca762a59
11 changed files with 295 additions and 165 deletions
+19 -16
View File
@@ -11,8 +11,9 @@ use tracing_subscriber::EnvFilter;
#[test]
fn login_default_filter_records_sanitized_storage_fallback() -> anyhow::Result<()> {
let home = tempdir()?;
// Fail before accessing the real keyring, then allow the plaintext fallback to succeed.
std::fs::write(home.path().join("secrets"), "not a directory")?;
let secrets_path = home.path().join("secrets").join("codex_auth.age");
// Reading a directory fails before accessing the real keyring on every platform.
std::fs::create_dir_all(&secrets_path)?;
let log_path = home.path().join("login.log");
let subscriber = tracing_subscriber::fmt()
.without_time()
@@ -36,7 +37,7 @@ fn login_default_filter_records_sanitized_storage_fallback() -> anyhow::Result<(
failure.contains("failed to save auth to keyring, falling back to file storage"),
"{logs}"
);
assert!(failure.contains("failed to create secrets dir"), "{logs}");
assert!(failure.contains("failed to read secrets file"), "{logs}");
assert!(
logs.contains("credential storage file fallback completed"),
"{logs}"
@@ -46,15 +47,19 @@ fn login_default_filter_records_sanitized_storage_fallback() -> anyhow::Result<(
assert!(!logs.contains("private-api-key"));
let completion = logs.lines().last().unwrap();
assert!(!completion.contains(home.path().to_str().unwrap()));
let cause = std::fs::create_dir_all(home.path().join("secrets"))
.unwrap_err()
.to_string();
let cause = std::fs::read(&secrets_path).unwrap_err().to_string();
// Windows reports access denied for directory reads; Unix reports a directory error.
insta::assert_snapshot!(
logs.replace(
home.path().join("secrets").to_str().unwrap(),
"[SECRETS_PATH]"
)
.replace(&cause, "[OS_ERROR]")
logs.replace(secrets_path.to_str().unwrap(), "[SECRETS_PATH]")
.replace(&cause, "[OS_ERROR]")
.replace(
"secure_error=\"access_denied\"",
"secure_error=\"[OS_ERROR_CATEGORY]\""
)
.replace(
"secure_error=\"other\"",
"secure_error=\"[OS_ERROR_CATEGORY]\""
)
);
Ok(())
}
@@ -62,11 +67,9 @@ fn login_default_filter_records_sanitized_storage_fallback() -> anyhow::Result<(
#[test]
fn login_storage_error_displays_context_and_cause() -> anyhow::Result<()> {
let home = tempdir()?;
let secrets_path = home.path().join("secrets");
std::fs::write(&secrets_path, "not a directory")?;
let cause = std::fs::create_dir_all(&secrets_path)
.unwrap_err()
.to_string();
let secrets_path = home.path().join("secrets").join("codex_auth.age");
std::fs::create_dir_all(&secrets_path)?;
let cause = std::fs::read(&secrets_path).unwrap_err().to_string();
let error = login_with_api_key(
home.path(),
"private-api-key",
@@ -1,7 +1,6 @@
---
source: cli/src/login_storage_tests.rs
assertion_line: 47
expression: "logs.replace(home.path().join(\"secrets\").to_str().unwrap(),\n\"[SECRETS_PATH]\").replace(&cause, \"[OS_ERROR]\")"
expression: "logs.replace(secrets_path.to_str().unwrap(),\n\"[SECRETS_PATH]\").replace(&cause,\n\"[OS_ERROR]\").replace(\"secure_error=\\\"access_denied\\\"\",\n\"secure_error=\\\"[OS_ERROR_CATEGORY]\\\"\").replace(\"secure_error=\\\"other\\\"\",\n\"secure_error=\\\"[OS_ERROR_CATEGORY]\\\"\")"
---
WARN codex_login::auth::storage: failed to save auth to keyring, falling back to file storage: failed to write OAuth tokens to encrypted auth storage: failed to create secrets dir [SECRETS_PATH]: [OS_ERROR]
WARN codex_otel::auth_storage: credential storage file fallback completed credential_kind="codex" store_mode="auto" selected_store="secrets" actual_store="file" operation="save" secure_outcome="error" outcome="success" fallback_reason="secure_error" secure_error="other" storage_phase="policy" originator="none"
WARN codex_login::auth::storage: failed to save auth to keyring, falling back to file storage: failed to write OAuth tokens to encrypted auth storage: failed to read secrets file at [SECRETS_PATH]: [OS_ERROR]
WARN codex_otel::auth_storage: credential storage file fallback completed credential_kind="codex" store_mode="auto" selected_store="secrets" actual_store="file" operation="save" secure_outcome="error" outcome="success" fallback_reason="secure_error" secure_error="[OS_ERROR_CATEGORY]" storage_phase="policy" originator="none"
@@ -1,6 +1,5 @@
---
source: cli/src/login_storage_tests.rs
assertion_line: 58
expression: "message.replace(secrets_path.to_str().unwrap(),\n\"[SECRETS_PATH]\").replace(&cause, \"[OS_ERROR]\")"
---
failed to write OAuth tokens to encrypted auth storage: failed to create secrets dir [SECRETS_PATH]: [OS_ERROR]
failed to write OAuth tokens to encrypted auth storage: failed to read secrets file at [SECRETS_PATH]: [OS_ERROR]