Writing configuration can mutate symlinks #89

Closed
opened 2026-07-10 08:03:46 +01:00 by danny · 1 comment
Owner

If the configuration given is a symlink to another file, writing the configuration will replace it with a plain file.

If the configuration given is a symlink to another file, writing the configuration will replace it with a plain file.
Collaborator

Root cause + fix

atomicWriteConfigFile (webui_config_edit.go) writes a save via a temp file created in
filepath.Dir(path), then os.Rename(tmpPath, path). POSIX rename() doesn't follow a symlink
at the destination - it unconditionally replaces whatever inode is at that path. If path is a
symlink, the rename destroys it and puts a plain regular file there instead, exactly as reported.

Fix: resolve path via filepath.EvalSymlinks once at the top (falling back to the given path
if resolution fails, e.g. a genuinely-missing file - the existing Stat/CreateTemp/Rename calls
already surface any real problem on their own), and do every subsequent filesystem operation
(permission lookup, temp file directory, final rename target) against the resolved path instead
of the original. This also incidentally fixes a second, related latent bug: creating the temp
file in the symlink's own directory and then renaming onto a target that lives in a different
directory (a realistic case for a symlink) risks a cross-filesystem rename failing outright
(EXDEV) even before the symlink-clobbering issue - the temp file needs to live in the same
directory as wherever the rename target actually is.

Net effect: a symlinked --config file's symlink itself is left untouched by a web UI save -
only the real target file's content changes.

Branch: issue89_symlink_write.

## Root cause + fix `atomicWriteConfigFile` (`webui_config_edit.go`) writes a save via a temp file created in `filepath.Dir(path)`, then `os.Rename(tmpPath, path)`. POSIX `rename()` doesn't follow a symlink at the *destination* - it unconditionally replaces whatever inode is at that path. If `path` is a symlink, the rename destroys it and puts a plain regular file there instead, exactly as reported. Fix: resolve `path` via `filepath.EvalSymlinks` once at the top (falling back to the given path if resolution fails, e.g. a genuinely-missing file - the existing Stat/CreateTemp/Rename calls already surface any real problem on their own), and do every subsequent filesystem operation (permission lookup, temp file directory, final rename target) against the *resolved* path instead of the original. This also incidentally fixes a second, related latent bug: creating the temp file in the symlink's own directory and then renaming onto a target that lives in a *different* directory (a realistic case for a symlink) risks a cross-filesystem rename failing outright (`EXDEV`) even before the symlink-clobbering issue - the temp file needs to live in the same directory as wherever the rename target actually is. Net effect: a symlinked `--config` file's symlink itself is left untouched by a web UI save - only the real target file's content changes. Branch: `issue89_symlink_write`.
danny closed this issue 2026-07-10 08:59:26 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
keyop-go/nfq_forwarder#89
No description provided.