With AGENTSYNC_ALLOW_SYMLINK_DEST unset, apply is documented as refusing to write through a symlinked destination. The refusal lives in iox.AtomicWrite (resolveSymlinkDest), which render.Writer.Write reaches only when the content differs. Before that, Write's convergence arm handles the content-identical case itself: it reads through the link, and when op.Mode != 0 and the permission bits differ it calls os.Chmod(op.Path, want). os.Chmod follows symlinks, so the TARGET's mode is rewritten through a link the user never opted into.
Measured on 7f2d4ad (probe in a throwaway clone): link → regular file with identical content, target 0600, op.Mode 0644, env unset → Write returns nil, the link is preserved, the target is now 0644. No ErrSymlinkDest.
Where. internal/render/writer.go, the convergence branch (os.ReadFile(op.Path) then the op.Mode != 0 chmod) runs before AtomicWrite; resolveSymlinkDest is never consulted on that path.
Why it matters now. #247 makes every read-side surface obey the switch, and reconcile's [o]verride re-applies through Writer.Write. [o] is therefore withheld on the symlink refusal arm in #247, and the SECURITY/architecture prose there names this issue as the one write path that still follows a link with the switch unset. The same convergence read is also the unguarded read behind #241 (a FIFO at a rendered destination hangs apply), so a fix that routes the convergence arm through the same shape and symlink gates would close both.
Expected. With the switch unset, Write refuses a symlinked destination on the mode arm exactly as AtomicWrite refuses it on the content arm (or skips the chmod and reports it), so "refuses to write through a link" is true of every write.
With
AGENTSYNC_ALLOW_SYMLINK_DESTunset,applyis documented as refusing to write through a symlinked destination. The refusal lives iniox.AtomicWrite(resolveSymlinkDest), whichrender.Writer.Writereaches only when the content differs. Before that,Write's convergence arm handles the content-identical case itself: it reads through the link, and whenop.Mode != 0and the permission bits differ it callsos.Chmod(op.Path, want).os.Chmodfollows symlinks, so the TARGET's mode is rewritten through a link the user never opted into.Measured on
7f2d4ad(probe in a throwaway clone): link → regular file with identical content, target 0600,op.Mode0644, env unset →Writereturns nil, the link is preserved, the target is now 0644. NoErrSymlinkDest.Where.
internal/render/writer.go, the convergence branch (os.ReadFile(op.Path)then theop.Mode != 0chmod) runs beforeAtomicWrite;resolveSymlinkDestis never consulted on that path.Why it matters now. #247 makes every read-side surface obey the switch, and
reconcile's[o]verridere-applies throughWriter.Write.[o]is therefore withheld on the symlink refusal arm in #247, and the SECURITY/architecture prose there names this issue as the one write path that still follows a link with the switch unset. The same convergence read is also the unguarded read behind #241 (a FIFO at a rendered destination hangsapply), so a fix that routes the convergence arm through the same shape and symlink gates would close both.Expected. With the switch unset,
Writerefuses a symlinked destination on the mode arm exactly asAtomicWriterefuses it on the content arm (or skips the chmod and reports it), so "refuses to write through a link" is true of every write.