Expand description
No-follow worktree writes (heddle#2017).
Tracked symlinks are checked out exactly as Git checks them out, whatever
their target: absolute, ../-escaping, or dangling. Creating a symlink
writes nothing outside the checkout.
The hazard is other writes that traverse one. A malicious repository can
track link -> /home/user/.ssh in one state and link/authorized_keys in a
later one, or a user can leave an untracked or ignored symlink where a
later state tracks a directory. A writer that opens root/link/file with
ordinary path syscalls follows link and writes outside the checkout.
The rule these helpers enforce, matching Git’s checkout: no worktree write or removal traverses a symlink below the checkout root.
- Directories are created one component at a time with
lstat+mkdir. A symlink where a directory belongs is unlinked (which never touches its target) and replaced by a real directory, asgit checkoutdoes for tracked and ignored paths. - Leaf files are created with
O_CREAT | O_EXCL | O_NOFOLLOWafter the old leaf is unlinked, so a symlink at the leaf is replaced, not written through. - Removals skip a path whose parent is a symlink or missing, as Git’s
has_symlink_or_noent_leading_pathdoes. The tracked path is not in the worktree, and whatever the symlink points at is not ours to delete.
The checkout root itself is trusted. It may legitimately sit beneath
symlinked ancestors (/var -> /private/var on macOS, a symlinked home).
These checks defend against repository content. They do not defend against a concurrent local process that swaps directories for symlinks between the check and the write; such a process can already write the worktree directly.
Structs§
- NoFollow
Directories - Creates and verifies directories beneath a checkout root without ever traversing a symlink.
Enums§
- InTree
Control File - What reading an in-tree control file found.
Constants§
- MAX_
IN_ TREE_ CONTROL_ FILE_ BYTES - Upper bound on an in-tree control file such as
.heddleignoreor.gitignore. Real ignore files are a few KiB; anything larger is refused rather than read into memory.
Functions§
- create_
new_ nofollow - Create a new regular file at
path, refusing to follow a symlink there. Fails if anything already occupies the name. - create_
replacing_ leaf - Unlink any file or symlink at
path, then create a fresh regular file there without following symlinks. The parent must already be verified. - open_
existing_ nofollow - Open an existing file for metadata changes without following a symlink at
path. - prepare_
regular_ file_ beneath - Prepare
pathbeneathrootfor an ordinary read-modify-write that must not touch a user’s symlinks: missing parent directories are created, but an existing symlinked parent, or a symlink or non-regular file at the leaf, is refused rather than followed or replaced. Used where the path is a convention inside the repository (.claude/settings.json) that a tracked symlink may legitimately redirect elsewhere. - read_
in_ tree_ control_ file - Read a control file the repository itself supplies (
.heddleignore,.gitignore): only a regular file, opened without following a symlink, and only up tomax_bytes. - refuse_
symlinked_ parent - Fail if any directory between
rootand the leafpathis a symlink. Uncached: run immediately before the write it guards. - remove_
leaf_ beneath - Remove the tracked leaf
pathbeneathroot(a file or a symlink, never its target). Returnsfalsewithout touching anything when a parent is a symlink or missing, matching Git: the path is not in the worktree. - remove_
path_ beneath - Recursively remove
pathbeneathrootwithout following symlinks, in the path itself or in any parent. Skips (returnsfalse) when a parent is a symlink or missing. - write_
file_ beneath - Write
bytesto the tracked filepathbeneathroot: parents become real directories, a leaf symlink is replaced, nothing is followed. - write_
symlink_ beneath - Create the symlink
path -> targetbeneathroot, replacing any file or symlink already there. Parents become real directories; nothing is followed.