From edb0a607bfd01c6e734ba6ac3c4d8ce5ae8ee644 Mon Sep 17 00:00:00 2001 From: yoni Date: Wed, 29 Jul 2026 18:05:58 +0000 Subject: [PATCH] fix(llm): open the store lock file with O_NOFOLLOW The predictable lock path was opened with Path.open("w"), following (and truncating through) a pre-positioned symlink. Open it via os.open with O_NOFOLLOW and no O_TRUNC, raising StoreLockError on a symlinked lock path. --- strix/config/subscription_store.py | 10 +++++++++- tests/test_subscription_store.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/strix/config/subscription_store.py b/strix/config/subscription_store.py index cfc9d745..d8be8625 100644 --- a/strix/config/subscription_store.py +++ b/strix/config/subscription_store.py @@ -129,7 +129,15 @@ def _acquire_flock(path: Path) -> TextIOWrapper: raise StoreLockError(msg) from exc lock_path = path.with_suffix(".lock") lock_path.parent.mkdir(parents=True, exist_ok=True) - handle = lock_path.open("w") + # O_NOFOLLOW rejects a pre-positioned symlink at the predictable lock path + # (so an attacker can't redirect the open), and no O_TRUNC since the lock + # file is only an flock anchor whose contents we never use. + try: + fd = os.open(str(lock_path), os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600) + except OSError as exc: + msg = f"could not open lock file {lock_path}: {exc}" + raise StoreLockError(msg) from exc + handle = os.fdopen(fd, "r+") try: while True: try: diff --git a/tests/test_subscription_store.py b/tests/test_subscription_store.py index 6f8a8be7..a3224d13 100644 --- a/tests/test_subscription_store.py +++ b/tests/test_subscription_store.py @@ -88,3 +88,20 @@ def test_mutation_aborts_when_lock_cannot_be_acquired( with pytest.raises(subscription_store.StoreLockError): grok.save_record({"type": "oauth", "access": "g", "refresh": "r"}) assert not store.exists() + + +def test_lock_file_rejects_a_pre_positioned_symlink( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + store_dir = tmp_path / ".strix" + store_dir.mkdir() + store = store_dir / "subscription-auth.json" + monkeypatch.setattr(grok, "AUTH_PATH", store) + # Attacker pre-plants a symlink where the lock file would be created. + outside = tmp_path / "attacker-target" + store.with_suffix(".lock").symlink_to(outside) + + with pytest.raises(subscription_store.StoreLockError): + grok.save_record({"type": "oauth", "access": "g", "refresh": "r"}) + # The symlink target was never created/truncated through the lock open. + assert not outside.exists()