Skip to content

fix(sexp): create files at the umask, keep journals owner-only - #674

Open
pauliuszaleckas wants to merge 1 commit into
mixelpixx:mainfrom
pauliuszaleckas:fix/538-created-file-modes
Open

pauliuszaleckas wants to merge 1 commit into
mixelpixx:mainfrom
pauliuszaleckas:fix/538-created-file-modes

Conversation

@pauliuszaleckas

Copy link
Copy Markdown
Contributor

Closes #538

What was wrong

write_new_atomic_unlocked built its scratch file with tempfile::Builder, which
creates at 0600 by design. Nothing adjusted it afterwards — on a create there is no
destination whose mode can be copied — so 0600 survived persist_noclobber and became
the created file's permanent mode. Every create-side call site was affected:
create_project, create_schematic, register_symbol_library, schematic_builder,
export_specctra_dsn and its manifest, and the Freerouting SES output. The replace path
was always correct: it copies the destination's mode onto the scratch file.

What it does now

A create-only write states which kind of file it is:

  • Ordinary — design files and exported artifacts. The scratch file asks for 0666
    and the kernel applies the umask, which is exactly what File::create would have
    produced, and what the replace path already gets from OpenOptions.
  • Private — only persist_journal's .konnect-transaction-*.json, which carries
    complete before/after images of the project. It asks for 0600 at creation and then
    sets 0600 on the open handle, before any content is written and by descriptor rather
    than by path.

That second step is the correction from #555's review: Builder::permissions supplies
only the creation mode, which the umask still masks, so umask 0277 would have left a
journal at 0400 and 0677 at 0000 — neither of which is the 0600 #538 asks for.

This follows the issue's stated expectation; the only thing beyond its text is that
enforcement, which the issue's "Not a bug" section implies but does not spell out.

Evidence

The issue's repro, against this branch

Built server, create_project + create_schematic + register_symbol_library into an
empty directory, plus create_symbol as the control the issue names (a different write
path, 0644 before this change too). The umask is genuinely consulted, not 0644
hardcoded:

file umask 022 umask 077 umask 002
permcheck.kicad_pro 0644 0600 0664
permcheck.kicad_pcb 0644 0600 0664
permcheck.kicad_sch 0644 0600 0664
solo.kicad_sch 0644 0600 0664
sym-lib-table 0644 0600 0664
my.kicad_sym (control) 0644 0600 0664

All five files the issue lists as 0600 now match the control in the same directory and
process.

Sampling the project directory while create_project runs catches the journal live at
0600 in all three runs — beside 0644, 0600 and 0664 design files respectively.

Existing projects

A replacement keeps the destination's mode, verified on this branch: a file chmod-ed to
0600 is still 0600 after an add_schematic_text, and one at 0666 is still 0666.
Anyone who already hit #538 therefore keeps 0600 and must chmod once — this fix
governs newly created files only. Recorded in DEV.md alongside the journal's Unix-only
privacy guarantee, which that file previously described without qualification.

Tests

writer::atomic_write_tests::created_file_modes_follow_the_creation_policy and
transaction::tests::transaction_journal_is_created_private each drive an #[ignore]d
probe in a child process under an explicit umask, so no test mutates the umask of the
process its parallel neighbours are writing in. The parent names the directory the child
writes into and reads the resulting modes from it directly. Expected ordinary modes are written as
literals — POSIX's 0666 & ~umask promise — not recomputed from the writer.

umask ordinary private / journal
000 0666 0600
002 0664 0600
022 0644 0600
077 0600 0600
277 0400 0600
677 0000 0600

Private content is asserted as the two properties themselves — owner read/write present,
and no group, other or execute bits, which together are exactly 0600 — through one
shared helper both tests call. transaction_journal_is_created_private compares against
a design file created by apply_entry in the same run, at umask 000 (where an ordinary
create is 0666) and umask 277 (where it is 0400), so the two policies are pinned
apart in both directions rather than coinciding by accident. That coincidence is what
made #555's assert_ne! fail under a valid umask 077; nothing here compares the two
under a umask that collapses them.

writer::atomic_write_tests::atomic_create_honors_the_process_umask keeps the
public-API, ambient-umask check from #555 against an OpenOptions control.

Negative controls

mutation tests failed
ordinary creation mode not requested (tempfile's 0600 default returns) 3 — atomic_create_honors_the_process_umask, created_file_modes_follow_the_creation_policy, transaction_journal_is_created_private
private mode set at creation only, no enforcement on the handle 2 — created_file_modes_follow_the_creation_policy, transaction_journal_is_created_private, both at umask 277
persist_journal back on the ordinary create path 1 — transaction_journal_is_created_private, at umask 000

Each control kills the guards that own its behaviour and nothing else; the third shows
journal privacy is pinned independently of the ordinary fix.

Validation

Run on this exact head:

  • cargo test --workspace --locked --lib --tests — pass (39 suites, 0 failed;
    konnect-sexp lib 196 passed, 2 ignored — the two probes)
  • cargo test --workspace --locked --doc — pass
  • cargo clippy --workspace --locked --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo test --locked --manifest-path crates/schematic-viewer/Cargo.toml — 20 passed
  • python -m unittest discover -s plugin/tests — 10 passed

Not runnable here: Nix flake check, PCM packaging validation (no jsonschema), and the
dependency-licence check (no cargo-deny). No dependency or packaging file changes, so
CI covers those.

Windows has no #[cfg(unix)] branch of this code; the non-Unix compile was checked by
building with the two cfg(unix) blocks inverted, with no unused-variable or dead-code
warnings.

Notes

  • No docs/API_MIGRATIONS.md entry: no tool argument, response field or error shape
    changes — only the mode of files on disk.
  • Not reproducible live under umask 0277: any directory created under that umask is
    itself unwritable, so create_project cannot get as far as a file. The subprocess
    tests cover those umasks directly.

Risk and rollback

Contained to the create-only writer. Ordinary creates become wider under a permissive
umask — that is the fix — while the journal becomes strictly narrower or unchanged. No
behaviour depends on the mode of a file Konnect wrote, and reverting the commit restores
0600-on-create exactly.

Credit

The diagnosis and the ordinary-path fix are from #555 by @wangzhengzhuo05, closed
unmerged with its review checklist recorded; that author asked for the claim to be
released. This branch answers that checklist — the private-mode enforcement, the
subprocess umask tests, and the controls — and keeps them as a co-author on the commit.

🤖 Generated with Claude Code

The create-only writer built its scratch file with tempfile::Builder,
which creates at 0600 by design and never adjusts it, so that mode
survived the rename: every file Konnect created — project, schematic,
board, sym-lib-table, the Specctra DSN and the Freerouting SES — landed
unreadable to a second user, a CI account or a container uid. The
replace path was always right; it copies the destination's mode.

A create now states which kind of file it is. Ordinary content asks for
0666 and lets the umask decide, like any other new file. Private
content — only the transaction journal, which holds complete before and
after images of the project — is created at 0600 and then set to 0600 on
the open handle, because a creation mode is only a request: a umask of
0277 would otherwise leave a journal at 0400, and 0677 at 0000.

Fixes mixelpixx#538

Co-Authored-By: Zhengzhuo Wang <175673456+wangzhengzhuo05@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@neusse neusse added bug Something isn't working P1 High-value workflow reliability area:platform Install, discovery, OS and KiCad-version compatibility status:waiting-on-review Next actor: maintainer labels Sep 21, 2026
@neusse neusse added status:waiting-on-dependency Next actor: the dependency owner — see linked blocking issue and removed status:waiting-on-review Next actor: maintainer labels Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform Install, discovery, OS and KiCad-version compatibility bug Something isn't working P1 High-value workflow reliability status:waiting-on-dependency Next actor: the dependency owner — see linked blocking issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Created project files land at mode 0600, ignoring umask — unreadable to a second user or CI account

2 participants