[GH-ISSUE #4904] [Bug] Configuration publication follows a symlink inserted after backup #3043

Closed
opened 2026-10-31 00:35:24 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @morluto on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4904

Before submitting

  • Searched related configuration/publication/symlink issues.
  • Removed credentials and sensitive data.

Area

Setup, packaging, or upgrade

Observed behavior

Both setup and uninstall overwrite an unrelated file when the selected configuration path becomes a symlink after its backup is copied. The new post-publication ownership checks then report failure, after the foreign target already contains the configuration update. Two new controls fail on main2fb5e204a;24 existing controls pass.

Reproduction

Executed npm run test:focused -- tests/process-global/setupConfigurationRollback.test.ts:2failed/24passed. The existing filesystem copy seam creates the real original backup, then replaces only an owned temporary configuration path with a symlink to another owned inert JSON file. The actual atomic writer runs. No real agent settings or investigated targets were touched. Each case executed once.

Expected behavior

Preserve caller-selected inputs and local evidence; ownership decisions must precede mutation. A later symlink must not redirect a configuration update into a different file. Refuse changed input and retain original backup. Keep an explicit destination through staging and publication.

Impact and scope

Observed foreign JSON contents replaced by setup and uninstall update bytes at controlled application/filesystem boundaries. Actual editor incidence, publicCLI race and nativeWindows/Linux occurrence:NOT_ENOUGH_INFO.

REA version

6.5.0 source2fb5e204a35723f6f843d56ca47b0db94d8fcc44.

Runtime and platform

Node24.18.0, macOS arm64; real temporary files, deterministic sequencing seam.

Target and artifact context

Owned inert JSON fixtures only; no engine/target execution.

Interface / Earliest failure stage

Other: shared configuration workflow / write preparation, before publication.

Relevant output or evidence

Expected foreign JSON unchanged; received setup registration JSON or uninstall removal JSON. The selected config remains a symlink. The original backup is correct. ClientConfigurationPublication delegates the selected path to write-file-atomic, whose locked implementation calls realpath(filename) again and publishes into that resolved target.

Analysis

Source-derived: resolution and ownership are discarded between read/backup and the writer. Fix the producing boundary: retain a verified input snapshot, stage through an owned private directory, and publish to an explicit destination. Do not repair only the readback result. Related #4900/#4902 protect recovery after publication and do not prevent this earlier mutation. Concurrent replacement after a final check remains a separate filesystem exclusion problem; no full race-freedom claim.

Originally created by @morluto on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4904 ### Before submitting - [x] Searched related configuration/publication/symlink issues. - [x] Removed credentials and sensitive data. ### Area Setup, packaging, or upgrade ### Observed behavior Both setup and uninstall overwrite an unrelated file when the selected configuration path becomes a symlink after its backup is copied. The new post-publication ownership checks then report failure, after the foreign target already contains the configuration update. Two new controls fail on main2fb5e204a;24 existing controls pass. ### Reproduction Executed `npm run test:focused -- tests/process-global/setupConfigurationRollback.test.ts`:2failed/24passed. The existing filesystem copy seam creates the real original backup, then replaces only an owned temporary configuration path with a symlink to another owned inert JSON file. The actual atomic writer runs. No real agent settings or investigated targets were touched. Each case executed once. ### Expected behavior Preserve caller-selected inputs and local evidence; ownership decisions must precede mutation. A later symlink must not redirect a configuration update into a different file. Refuse changed input and retain original backup. Keep an explicit destination through staging and publication. ### Impact and scope Observed foreign JSON contents replaced by setup and uninstall update bytes at controlled application/filesystem boundaries. Actual editor incidence, publicCLI race and nativeWindows/Linux occurrence:NOT_ENOUGH_INFO. ### REA version 6.5.0 source2fb5e204a35723f6f843d56ca47b0db94d8fcc44. ### Runtime and platform Node24.18.0, macOS arm64; real temporary files, deterministic sequencing seam. ### Target and artifact context Owned inert JSON fixtures only; no engine/target execution. ### Interface / Earliest failure stage Other: shared configuration workflow / write preparation, before publication. ### Relevant output or evidence Expected foreign JSON unchanged; received setup registration JSON or uninstall removal JSON. The selected config remains a symlink. The original backup is correct. ClientConfigurationPublication delegates the selected path to write-file-atomic, whose locked implementation calls realpath(filename) again and publishes into that resolved target. ### Analysis Source-derived: resolution and ownership are discarded between read/backup and the writer. Fix the producing boundary: retain a verified input snapshot, stage through an owned private directory, and publish to an explicit destination. Do not repair only the readback result. Related #4900/#4902 protect recovery after publication and do not prevent this earlier mutation. Concurrent replacement after a final check remains a separate filesystem exclusion problem; no full race-freedom claim.
bsakaguchi 2026-10-31 00:35:24 +00:00
  • closed this issue
  • added the
    triage
    bug
    labels
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Fixed in d0253c99d65afbbd12732a2fdaf711640574e17f, pushed to main.

The configuration writer now owns an exclusive private stage and publishes to one selected canonical parent plus basename. It validates expected input bytes/presence, rechecks the selected state, uses rename for existing configurations, and an exclusive hard link for first-run creation. Setup and uninstall read back the publication owner's path. The same descriptor and exact staging cleanup lease remain owned through recovery and retry; temporary permission handoffs were removed.

Executed validation on macOS arm64 / Node24.18.0: 284 passed, one skipped across14 focused test files, including both original symlink-after-backup failures, edits/deletion, staging conflicts, parent alias retargeting through both adapters, first-run final-check conflict, mixed descriptor failures, read-only restoration, and staging-directory cleanup retry. check:fast, format:check, docs:check (five consistency checks), and pre-push checks passed. Two scoped source reviews found no remaining blocker after fixes.

12 completed isolated public CLI setup/uninstall controls (six per workflow, alternating baseline/candidate; two warm repeats per build):

  • setup: warm median elapsed 1.4308→1.4234s; wait4 CPU 1.4496→1.4788s; sampled process-family peak RSS 493.3→510.7MiB. Estimated cl100k request/reply/progress tokens 24/3/0 unchanged.
  • uninstall mode0400: warm median elapsed 0.3712→0.3707s; wait4 CPU 0.5525→0.5570s; sampled process-family peak RSS 330.6→332.4MiB. Estimated cl100k request/reply/progress tokens 12/3/0 unchanged.

Equivalent filtered status, selected launcher/removal, unrelated settings and original backup were verified; uninstall preserved0400 mode. No speedup, significance, true-cold or full-agent token claim;10ms family RSS sampling can miss transients/reparented processes.

Limits: native Windows/Linux, filesystems without hard links, and actual editor concurrency were not verified. First-run publication fails closed if hard links are unavailable. These checks do not exclude same-inode writers, canonical ancestor replacement, or the final-check/rename interval; no complete filesystem transaction exclusion claim. No provider/target execution or heavy profiler/test run.

Research: the locked write-file-atomic/lib/index.js and executed failing controls establish the cause. DeepWiki returned a different implementation and incorrect locking claims, so that response was not used as authority; Context7 did not resolve applicable Node core docs. Native API semantics were checked against the official Node filesystem docs.

<!-- gh-comment-id:6242837949 --> @morluto commented on GitHub (Oct 17, 2026): Fixed in `d0253c99d65afbbd12732a2fdaf711640574e17f`, pushed to main. The configuration writer now owns an exclusive private stage and publishes to one selected canonical parent plus basename. It validates expected input bytes/presence, rechecks the selected state, uses rename for existing configurations, and an exclusive hard link for first-run creation. Setup and uninstall read back the publication owner's path. The same descriptor and exact staging cleanup lease remain owned through recovery and retry; temporary permission handoffs were removed. Executed validation on macOS arm64 / Node24.18.0: **284 passed, one skipped across14 focused test files**, including both original symlink-after-backup failures, edits/deletion, staging conflicts, parent alias retargeting through both adapters, first-run final-check conflict, mixed descriptor failures, read-only restoration, and staging-directory cleanup retry. `check:fast`, `format:check`, `docs:check` (five consistency checks), and pre-push checks passed. Two scoped source reviews found no remaining blocker after fixes. 12 completed isolated public CLI setup/uninstall controls (six per workflow, alternating baseline/candidate; two warm repeats per build): - setup: warm median elapsed 1.4308→1.4234s; wait4 CPU 1.4496→1.4788s; sampled process-family peak RSS 493.3→510.7MiB. Estimated cl100k request/reply/progress tokens 24/3/0 unchanged. - uninstall mode0400: warm median elapsed 0.3712→0.3707s; wait4 CPU 0.5525→0.5570s; sampled process-family peak RSS 330.6→332.4MiB. Estimated cl100k request/reply/progress tokens 12/3/0 unchanged. Equivalent filtered status, selected launcher/removal, unrelated settings and original backup were verified; uninstall preserved0400 mode. No speedup, significance, true-cold or full-agent token claim;10ms family RSS sampling can miss transients/reparented processes. Limits: native Windows/Linux, filesystems without hard links, and actual editor concurrency were not verified. First-run publication fails closed if hard links are unavailable. These checks do not exclude same-inode writers, canonical ancestor replacement, or the final-check/rename interval; no complete filesystem transaction exclusion claim. No provider/target execution or heavy profiler/test run. Research: the locked `write-file-atomic/lib/index.js` and executed failing controls establish the cause. DeepWiki returned a different implementation and incorrect locking claims, so that response was not used as authority; Context7 did not resolve applicable Node core docs. Native API semantics were checked against the official [Node filesystem docs](https://nodejs.org/docs/latest-v24.x/api/fs.html).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3043
No description provided.