Skip to main content

Module nofollow

Module nofollow 

Source
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, as git checkout does for tracked and ignored paths.
  • Leaf files are created with O_CREAT | O_EXCL | O_NOFOLLOW after 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_path does. 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§

NoFollowDirectories
Creates and verifies directories beneath a checkout root without ever traversing a symlink.

Enums§

InTreeControlFile
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 .heddleignore or .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 path beneath root for 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 to max_bytes.
refuse_symlinked_parent
Fail if any directory between root and the leaf path is a symlink. Uncached: run immediately before the write it guards.
remove_leaf_beneath
Remove the tracked leaf path beneath root (a file or a symlink, never its target). Returns false without touching anything when a parent is a symlink or missing, matching Git: the path is not in the worktree.
remove_path_beneath
Recursively remove path beneath root without following symlinks, in the path itself or in any parent. Skips (returns false) when a parent is a symlink or missing.
write_file_beneath
Write bytes to the tracked file path beneath root: parents become real directories, a leaf symlink is replaced, nothing is followed.
write_symlink_beneath
Create the symlink path -> target beneath root, replacing any file or symlink already there. Parents become real directories; nothing is followed.