diff --git a/api/paths.py b/api/paths.py index 2fad3194..7254db26 100644 --- a/api/paths.py +++ b/api/paths.py @@ -144,7 +144,18 @@ def _atomic_write_text(path: Path, text: str, *, encoding: str = "utf-8") -> Non if existing_stat is not None and hasattr(os, "fchown"): try: os.fchown(fd, existing_stat.st_uid, existing_stat.st_gid) - except (PermissionError, NotImplementedError): + except (OSError, NotImplementedError) as exc: + unsupported_ownership_errnos = { + errno.EINVAL, + errno.ENOSYS, + errno.ENOTSUP, + } + if ( + isinstance(exc, OSError) + and not isinstance(exc, PermissionError) + and exc.errno not in unsupported_ownership_errnos + ): + raise os.close(fd) owns_fd = False os.unlink(tmp) diff --git a/tests/test_5774b_atomic_config_writes.py b/tests/test_5774b_atomic_config_writes.py index a4200a3a..bb0f06d5 100644 --- a/tests/test_5774b_atomic_config_writes.py +++ b/tests/test_5774b_atomic_config_writes.py @@ -99,15 +99,22 @@ def test_atomic_write_preserves_existing_group(tmp_path: Path) -> None: @pytest.mark.skipif(not hasattr(os, "fchown"), reason="POSIX ownership semantics") +@pytest.mark.parametrize( + "failure", + [ + PermissionError("simulated ownership denial"), + OSError(errno.EINVAL, "simulated unsupported ownership transfer"), + ], +) def test_fchown_denial_falls_back_without_temp_debris( - tmp_path: Path, monkeypatch + tmp_path: Path, monkeypatch, failure: OSError ) -> None: """A writer unable to transfer ownership must keep the old inode contract.""" target = tmp_path / "config.yaml" target.write_text("old: true\n", encoding="utf-8") def _deny_fchown(*_args) -> None: - raise PermissionError("simulated ownership denial") + raise failure monkeypatch.setattr(os, "fchown", _deny_fchown)