fix(utils): new non-secret atomic writes follow the process umask again
Every hand-rolled writer this PR folded into utils._atomic_write created a
NEW file with write_text()/open("w"), i.e. at 0o666 masked by the umask
(0644 under 022). The canonical helper publishes through mkstemp, whose
temp is 0600, and with no explicit mode and no existing target to copy bits
from it left that 0600 in place - so debug, model_catalog, profiles,
breadcrumbs, worktree_ops, web_result_cache, plugin_compat, write_approval,
rich_sent_store, active_sessions and the google_meet state files were
silently tightened to owner-only, the volume-mount hazard
_restore_file_metadata's own docstring warns about. Undeclared in the PR.
Fix at the canonical: when mode is None and the target does not exist,
apply default_new_file_mode() (0o666 masked by the umask, read via the
umask two-call trick with a transient 0o077 so a racing thread can only get
a tighter file). The helper is hermes_cli/backup._default_new_file_mode
moved into utils and reused. Secret writers (mode=0o600) are 0600 before,
during and after as before; an existing target keeps its bits; on non-POSIX
the helper returns None so nothing is chmod'd.
This commit is contained in:
@@ -84,3 +84,29 @@ def test_surrogate_escaped_strings_round_trip_through_atomic_json_write(tmp_path
|
||||
assert json.loads(target.read_bytes()) == payload
|
||||
assert _leftovers(target.parent, target.name) == []
|
||||
|
||||
|
||||
@pytest.mark.linux_only
|
||||
def test_new_non_secret_file_follows_umask_while_secret_and_existing_modes_hold(tmp_path):
|
||||
"""The writers this helper replaced created files at process umask; only ``mode=`` tightens."""
|
||||
import os
|
||||
import stat
|
||||
|
||||
from utils import atomic_json_write
|
||||
|
||||
old_umask = os.umask(0o022)
|
||||
try:
|
||||
fresh = tmp_path / "cache.json"
|
||||
atomic_json_write(fresh, {"a": 1})
|
||||
assert stat.S_IMODE(fresh.stat().st_mode) == 0o644, "new non-secret file must not inherit mkstemp's 0600"
|
||||
|
||||
secret = tmp_path / "creds.json"
|
||||
atomic_json_write(secret, {"token": "x"}, mode=0o600)
|
||||
assert stat.S_IMODE(secret.stat().st_mode) == 0o600
|
||||
|
||||
existing = tmp_path / "state.json"
|
||||
existing.write_text("{}", encoding="utf-8")
|
||||
os.chmod(existing, 0o640)
|
||||
atomic_json_write(existing, {"b": 2})
|
||||
assert stat.S_IMODE(existing.stat().st_mode) == 0o640
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
Reference in New Issue
Block a user