Symlink Resolution and Loop Detection

A symbolic link is a file whose contents are a pathname; when path resolution lands on one mid-walk, it must substitute the link’s target for the link component and keep walking. Linux does this without editing the path string and without recursion — it pushes the remaining components of the current path onto a small symlink stack inside nameidata, reads the link body via the filesystem’s inode_operations->get_link (or the inline i_link pointer), and resumes the walk at the target. Because a link can point at another link, this nests, and because a link can point at itself — directly or through a cycle — the kernel caps the total work: at most MAXSYMLINKS (40) symlinks per single lookup, beyond which it returns ELOOP (“Too many levels of symbolic links”) (include/linux/namei.h, #define MAXSYMLINKS 40; path-lookup.rst). This note walks the actual code path in fs/namei.cstep_intopick_link → the stack-driven loop in link_path_walk — plus the O_NOFOLLOW/AT_SYMLINK_NOFOLLOW opt-outs and the special “magic links” under /proc/<pid>/fd/. (Pinned to Linux 6.12 LTS, source as of 2026-06-04.)

Mental Model — A Stack of Path Remnants, Not Recursion

Conceptually, following a -> b/c while walking a/d/e means: replace a with b/c, giving b/c/d/e, then walk that. The kernel does not literally splice strings. Instead it keeps the unfinished tail of each path on a stack: when it steps onto a and finds it is a symlink, it pushes the remnant d/e onto the stack and switches the active name to the link body b/c. When b/c finishes resolving, it pops d/e and continues. Nesting is just a deeper stack. This design — chosen in the Linux 4.2 rework that “eliminate[d] the use of recursion” (path_resolution(7)) — means the only resource cap needed is the stack depth and a total-link counter, not a C-call-recursion limit.

flowchart TB
  start["walk a/d/e"]
  hita["step onto 'a' = symlink to b/c"]
  push["push remnant 'd/e' onto<br/>nd->stack (nd->depth++);<br/>active name becomes 'b/c'"]
  walkbc["walk b/c to completion"]
  pop["pop 'd/e' (put_link, nd->depth--);<br/>resume walking d/e"]
  done["final component reached"]
  start --> hita --> push --> walkbc --> pop --> done
  cnt["each link bumps<br/>nd->total_link_count;<br/>>= 40 returns ELOOP"]
  hita -. checked at .-> cnt

The symlink stack in action. What it shows: stepping onto a symlink pushes the path’s unresolved tail onto nd->stack and redirects the walk to the link body; when the body finishes, the tail is popped and resumed. A per-lookup counter (nd->total_link_count) is incremented at every link and trips ELOOP at 40. The insight to take: symlink following is iterative, not recursive — the “depth” is data on a stack (nd->depth), and loop protection is a simple budget on the total number of links followed, which catches both true cycles and merely pathologically deep chains.

Mechanical Walk-through

After walk_component() finds a dentry and handle_mounts() crosses any mount point, control reaches step_into(). It inspects the landed-on dentry:

static const char *step_into(struct nameidata *nd, int flags,
                             struct dentry *dentry)
{
    struct path path;
    int err = handle_mounts(nd, dentry, &path);   /* cross mounts first */
    ...
    inode = path.dentry->d_inode;
    if (likely(!d_is_symlink(path.dentry)) ||
        ((flags & WALK_TRAILING) && !(nd->flags & LOOKUP_FOLLOW)) ||
        (flags & WALK_NOFOLLOW)) {
        /* not a symlink, or we should not follow it: just install the path */
        nd->path = path; nd->inode = inode; nd->seq = nd->next_seq;
        return NULL;
    }
    ...
    return pick_link(nd, &path, inode, flags);    /* it IS a symlink we must follow */
}

Three independent reasons not to follow: the object is not a symlink (!d_is_symlink), or it is the trailing component and the caller did not pass LOOKUP_FOLLOW (this is how O_NOFOLLOW/lstat get a handle on the link itself), or the caller explicitly set WALK_NOFOLLOW (used for .. and a few internal cases). Otherwise pick_link() is called to follow it. Note d_is_symlink() is a flag check on the dentry, not an inode-mode read — symlink-ness is cached in d_flags for speed.

pick_link() does the real work. First it reserves a stack slot, which is also where the loop limit is enforced:

static int reserve_stack(struct nameidata *nd, struct path *link)
{
    if (unlikely(nd->total_link_count++ >= MAXSYMLINKS))
        return -ELOOP;
    if (likely(nd->depth != EMBEDDED_LEVELS))
        return 0;                       /* embedded stack still has room */
    ...
    if (likely(nd_alloc_stack(nd)))     /* grow to a 40-slot heap stack */
        return 0;
    ...
}

Every symlink increments nd->total_link_count, and the first thing checked is whether it has reached MAXSYMLINKS (40) — if so, -ELOOP. This single counter is the entire loop-detection mechanism (more below). If under budget, pick_link() pushes the link onto the stack and reads its body:

last = nd->stack + nd->depth++;       /* push */
last->link = *link;
last->seq = nd->next_seq;
...
if (unlikely(nd->flags & LOOKUP_NO_SYMLINKS) ||
        unlikely(link->mnt->mnt_flags & MNT_NOSYMFOLLOW))
    return ERR_PTR(-ELOOP);            /* RESOLVE_NO_SYMLINKS / nosymfollow mount */
...
res = READ_ONCE(inode->i_link);       /* fast path: inline symlink body */
if (!res) {
    get = inode->i_op->get_link;      /* slow path: ask the filesystem */
    if (nd->flags & LOOKUP_RCU) {
        res = get(NULL, inode, &last->done);
        if (res == ERR_PTR(-ECHILD) && try_to_unlazy(nd))
            res = get(link->dentry, inode, &last->done);
    } else {
        res = get(link->dentry, inode, &last->done);
    }
    ...
}
if (*res == '/') {                    /* absolute link: restart at root */
    error = nd_jump_root(nd);
    while (unlikely(*++res == '/')) ; /* skip leading slashes */
}
if (*res)
    return res;                       /* hand the body back to link_path_walk */
all_done:
    put_link(nd);                     /* link body was empty / pure jump */
    return NULL;

Two ways to get the link body. The fast path reads inode->i_link — a pointer the filesystem sets for symlinks stored inline in the inode (e.g. ext4 “fast symlinks” whose target fits in the i_block array: ext4_inode_is_fast_symlink() treats a link of fewer than EXT4_N_BLOCKS * 4 = 60 bytes as inline, holding the target in the 15 i_block slots rather than a separate data block — verified in fs/ext4/inode.c). The kernel doc explains: “Short symlinks are often stored directly in the inode … the i_link pointer in the inode is set to point to wherever the symlink is stored and it can be accessed directly whenever needed.” (path-lookup.rst)

The slow path calls inode->i_op->get_link(), the per-filesystem method that returns the link body (typically from the page cache for longer symlinks). It can register a delayed_call (&last->done) so the kernel releases the page reference via put_link() once the remnant is consumed — that is why the stack slot stores a struct delayed_call done, not just the name. In RCU-walk, get_link(NULL, ...) is tried first; a filesystem that cannot serve the link lock-free returns -ECHILD, and the walk drops to ref-walk (try_to_unlazy) and retries with the dentry.

get_link superseded the older ->follow_link/->put_link pair: in 6.12, inode_operations exposes only get_link (verified in include/linux/fs.h, where the single declaration is const char *(*get_link)(struct dentry *, struct inode *, struct delayed_call *) — no follow_link/put_link remain). The delayed_call argument folds the old separate ->put_link cleanup into the same call.

Uncertain

Verify: the exact kernel version in which ->follow_link/->put_link were replaced by ->get_link with the delayed_call. Reason: not directly addressed by a primary source consulted here — the 4.2 rework (commit 894bc8c) eliminated recursion, which is a distinct change from the get_link interface introduction (believed to be slightly later, ~4.5). To resolve: bisect the inode_operations struct history in include/linux/fs.h or read the get_link introduction commit. The 6.12 end-state (only get_link exists) is verified; only the introduction version is unpinned. uncertain

If the body begins with /, it is absolute: nd_jump_root() resets the walk to the process’s root (nd->root, respecting any chroot/pivot_root) and the leading slashes are skipped. Otherwise the body is relative and resolution simply continues from the link’s parent directory. Either way the non-empty body string is returned to link_path_walk().

link_path_walk() is the driver. When walk_component() (via step_into/pick_link) returns a non-NULL string, that string is a symlink body to be walked next, and the current unfinished name is pushed:

link = walk_component(nd, WALK_MORE);
...
if (unlikely(link)) {
    if (IS_ERR(link))
        return PTR_ERR(link);
    /* a symlink to follow */
    nd->stack[depth++].name = name;   /* save remnant of current path */
    name = link;                      /* switch active name to link body */
    continue;
}

So nd->stack[].name holds the leftover of the outer path, and name becomes the link body. When the body’s final component is reached, the loop pops the saved remnant and continues:

OK:
    if (!depth) { ... return 0; }     /* whole path done */
    /* last component of nested symlink */
    name = nd->stack[--depth].name;   /* pop the outer remnant */
    link = walk_component(nd, 0);

This is the iteration that replaces recursion: depth rises as links nest and falls as each link’s tail is consumed, all without any C recursion. The nd->stack array is the embedded internal[EMBEDDED_LEVELS] (EMBEDDED_LEVELS = 2) until a third nested link forces nd_alloc_stack() to allocate a 40-slot heap stack. The doc notes this two-tier design: “a small stack that can be used to store the remaining part of up to two symlinks. In many cases this will be sufficient. If it isn’t, a separate stack is allocated with room for 40 symlinks.” (path-lookup.rst)

Loop detection — why a budget, not cycle tracking

Linux does not detect symlink loops by remembering which inodes it has visited. It simply counts. nd->total_link_count is incremented at every link and capped at 40; a true cycle (a -> b, b -> a) and a merely-too-deep chain both blow the budget and yield ELOOP. The path-lookup doc quotes Linus on the rationale: “it’s a latency and DoS issue too. We need to react well to true loops, but also to ‘very deep’ non-loops. It’s not about memory use, it’s about users triggering unreasonable CPU resources.” (path-lookup.rst) A budget catches both cases with one cheap counter.

The 40 limit has history worth knowing: before Linux 2.6.18 the recursion-depth limit was 5; 2.6.18 raised it to 8 (_POSIX_SYMLOOP_MAX is 8); and the 4.2 stack-based rework collapsed the separate recursion-depth and total-count limits into the single MAXSYMLINKS == 40 total (path_resolution(7)). So in 6.12 there is exactly one limit: 40 links total per lookup, regardless of nesting shape.

Mount-point automounts share the same budget: follow_automount() checks (*count)++ >= MAXSYMLINKS against the same nd->total_link_count, so a pathological chain of automount triggers also trips ELOOP rather than running forever.

Configuration and the Opt-Outs

These do not set a “nofollow” lookup flag; they withhold LOOKUP_FOLLOW for the trailing component. Recall step_into’s test: (flags & WALK_TRAILING) && !(nd->flags & LOOKUP_FOLLOW) means “don’t follow the final symlink.” So:

/* open(2) on the link itself, not its target */
int fd = open("/path/to/link", O_PATH | O_NOFOLLOW);
/* if the final component is a symlink, open() with O_NOFOLLOW (without
 * O_PATH) fails with ELOOP; with O_PATH you get an fd to the link itself */

The symlink(7) man page is explicit that O_NOFOLLOW affects only the last component: “If the trailing component (i.e., basename) of pathname is a symbolic link, then the open fails, with the error ELOOP.” Intermediate symlinks in the path are still followed. AT_SYMLINK_NOFOLLOW is the equivalent bit for the *at() family (fstatat, linkat, fchownat, etc.), and lstat(2) is effectively stat with this behavior baked in — it operates on the link, while stat(2) follows it. (symlink(7))

To forbid all symlink following (not just the trailing one), openat2(2) offers RESOLVE_NO_SYMLINKS, which maps to LOOKUP_NO_SYMLINKS and makes pick_link() return -ELOOP for any symlink encountered. Independently, a mount may carry the nosymfollow flag (MNT_NOSYMFOLLOW), which pick_link() also checks — symlinks on that mount are never followed. Both appear in the same guard:

if (unlikely(nd->flags & LOOKUP_NO_SYMLINKS) ||
        unlikely(link->mnt->mnt_flags & MNT_NOSYMFOLLOW))
    return ERR_PTR(-ELOOP);

The fs.protected_symlinks sysctl gates following symlinks in sticky, world-writable directories like /tmp, to defeat a classic time-of-check/time-of-use attack where an attacker plants a symlink. (Many distributions enable it by default, but the default is distribution policy, not a kernel default — the kernel initializes sysctl_protected_symlinks to 0; check sysctl fs.protected_symlinks on a given system.) may_follow_link() (invoked from pick_link for trailing links) allows the follow only if the follower owns the link, or the directory is not sticky+world-writable, or the directory owner matches the link owner — otherwise -EACCES and an audit log (AUDIT_ANOM_LINK, “follow_link”). This is a security policy layered on top of resolution, not part of resolution proper.

Some pseudo-filesystem “symlinks” are not text pathnames at all. /proc/<pid>/fd/N, /proc/<pid>/exe, /proc/<pid>/cwd, and /proc/<pid>/root are magic links: following them does not re-resolve a string — it teleports the walk directly to a kernel-held struct path. The mechanism is nd_jump_link(), called from proc_pid_get_link() (fs/proc/base.c):

static const char *proc_pid_get_link(struct dentry *dentry,
                                     struct inode *inode,
                                     struct delayed_call *done)
{
    ...
    if (!proc_fd_access_allowed(inode))   /* ptrace-grade permission check */
        goto out;
    error = PROC_I(inode)->op.proc_get_link(dentry, &path);  /* fetch the real path */
    ...
    error = nd_jump_link(&path);          /* jump the walk straight to it */
    ...
}

nd_jump_link() replaces nd->path with the supplied (mnt, dentry) pair outright:

int nd_jump_link(const struct path *path)
{
    struct nameidata *nd = current->nameidata;
    if (unlikely(nd->flags & LOOKUP_NO_MAGICLINKS))   /* RESOLVE_NO_MAGICLINKS */
        goto err;                                     /* returns -ELOOP */
    if (unlikely(nd->flags & LOOKUP_NO_XDEV)) {
        if (nd->path.mnt != path->mnt) goto err;      /* -EXDEV */
    }
    if (unlikely(nd->flags & LOOKUP_IS_SCOPED))       /* not safe for scoped lookups */
        goto err;
    path_put(&nd->path);
    nd->path = *path;
    nd->inode = nd->path.dentry->d_inode;
    nd->state |= ND_JUMPED;
    return 0;
}

Because a magic link is a direct file-handle reference, it can reach files that have no name — an unlinked-but-still-open file is reachable via /proc/<pid>/fd/N even though no path leads to it. It also bypasses textual containment: symlink(7) warns “Because they can bypass ordinary mount_namespaces(7)-based restrictions, magic links have been used as attack vectors in various exploits.” Hence the guards: RESOLVE_NO_MAGICLINKS (→ LOOKUP_NO_MAGICLINKS) blocks the jump entirely, and scoped lookups (RESOLVE_BENEATH/RESOLVE_IN_ROOT) refuse magic links because a jump can land arbitrarily outside the scope. Magic links also carry permission semantics ordinary symlinks lack: proc_fd_access_allowed() enforces a ptrace-style check before the jump, and magic links can have a non-0777 mode (ordinary symlinks are always 0777). (symlink(7), proc_pid_fd(5))

Failure Modes and Subtleties

  • ELOOP is not only loops. A 41-deep acyclic chain of symlinks returns ELOOP too. The error string “Too many levels of symbolic links” is accurate; “loop” is a misnomer for the deep-chain case. Diagnose with namei -l /path or readlink -f, which trace the chain.

  • O_NOFOLLOW protects only the last component. A path /tmp/evil/target where /tmp/evil is an attacker symlink is still traversed under O_NOFOLLOW — only target itself is protected. For full protection use openat2(2) with RESOLVE_NO_SYMLINKS or RESOLVE_BENEATH/RESOLVE_IN_ROOT.

  • Trailing slash forces a follow. lookup_last() sets LOOKUP_FOLLOW | LOOKUP_DIRECTORY when the trailing component has a trailing /, so open("link/", O_NOFOLLOW) behaves differently from open("link", O_NOFOLLOW) — the slash demands the target be a directory, which overrides the nofollow intent on the trailing component.

  • Magic links and unlinked files. Tooling that walks /proc/*/fd/ to “find what a process has open” is using magic-link jumps; the targets may be deleted (readlink shows them suffixed ” (deleted)”). This is by design and is how lsof-style introspection works.

  • RCU-walk fallback on symlinks. A symlink whose body lives in the page cache may force a drop from RCU-walk to ref-walk if get_link can’t serve it lock-free (-ECHILD). Atime updates on the link (touch_atime in pick_link) can also force try_to_unlazy. So symlink-heavy paths are less likely to stay on the fast lockless walk — a real, if minor, performance consideration.

  • The link body is bounded by PATH_MAX. A symlink target longer than PATH_MAX (4096) cannot be stored, and an intermediate path produced during resolution that exceeds PATH_MAX yields ENAMETOOLONG, a separate limit from ELOOP.

See Also