5.2.4 Symlinks

A symlink key uses two mechanisms, and both are load-bearing.

The symlink flag on the key record marks the key's structural type. It is set at creation by REG_OPTION_CREATE_LINK and is immutable afterwards.

RSI_WRITE_KEY can update only the descriptor and the last write time, so there is no operation that could change it.

The default value, of type REG_LINK, supplies the target path.

It is an ordinary layered value, which means a higher-precedence layer can redirect a symlink by writing a different REG_LINK default value, and removing that layer restores the original target.

The flag marks identity; the value provides the target.

5.2.4.1 Resolution #

LCS follows symlinks during path resolution and the fd it returns refers to the resolved target — its GUID, its position in the tree, its ancestor chain — not to the link.

The target is resolved by issuing a separate RSI_QUERY_VALUES for the key's default value (the empty name) and applying ordinary layer resolution to the result, so the target participates in the layer system exactly as any other value does.

If the effective default value is missing, or is not of type REG_LINK, resolution fails with EINVAL.

LCS does not validate the type at write time: a layer that writes a REG_SZ default value over a symlink's target breaks resolution at the next open, and removing that layer fixes it.

The offending value stays in the registry; a failed resolution writes nothing.

5.2.4.2 The target path #

The REG_LINK payload is a length-delimited UTF-8 registry path. No trailing null is required or permitted — the length delimits it, and a null byte inside that length is rejected like any other. Forward slashes are handled as separators as everywhere else.

The target is validated with exactly the same rules as a syscall path: UTF-8, no null bytes, per-component and total length, no empty components, no trailing separator, maximum depth. The only difference is that a syscall path arrives null-terminated and has its terminator stripped first.

A target is always interpreted as absolute — its first component is routed as a hive name.

Resolution has no check that rejects a relative-looking target as malformed. A target of Sub\Key is not an error; it is a request for a hive named Sub, and it yields ENOENT unless such a hive happens to be registered, in which case it resolves there.

Writing one is different. REG_IOC_SET_VALUE of a REG_LINK value routes the target's first component at write time, with the writer's scope GUIDs, and answers EINVAL if it names no registered hive. That is what stops a dangling target being captured later by whoever registers a private hive of that name: a target that cannot be written cannot be captured, and the failure lands on whoever authored it. The cost is that a symlink cannot forward-reference a hive registered after it. A target that arrives in source data or a backup stream is not gated, which is why resolution still has to treat the shape as a hive request.

CurrentUser\ rewriting is not applied (§5.2.1), so a target beginning with CurrentUser\ routes as a hive of that name and cannot be registered, and therefore always fails. Ordinary hive routing does apply, private hives included, for the resolving thread.

5.2.4.3 Depth #

Symlink resolution is bounded by SymlinkDepthLimit, default 16, configurable from 1 to 64. Exceeding it is ELOOP.

REG_OPEN_LINK on reg_open_key opens the symlink key rather than following it, which is how a symlink is managed at all — deleted, retargeted, inspected.

It applies to the final path component only. A symlink encountered part-way along a path is followed whether or not the flag is set.

The access check follows the same rule: with REG_OPEN_LINK the check is against the link, otherwise against the target.

5.2.4.5 Creation #

Creating a symlink needs all of:

  • KEY_CREATE_SUB_KEY on the parent, as for any key;
  • KEY_CREATE_LINK on the parent;
  • either an enabled SeTcbPrivilege or membership of Administrators.

The last is a genuine disjunction — either satisfies it — and the privilege branch marks the privilege used.

Failing it is EPERM.

Edit this page