Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BEntry Rename, Symlinks, and Reference Semantics

Avoid stale paths and accidental symlink traversal in Haiku by understanding entry_ref identity, BEntry rename behavior, and safe replacement.

Haiku’s Storage Kit distinguishes a pathname, an entry_ref, and a BEntry. They all describe filesystem entries, but their behavior under rename and symlink traversal differs. Confusing them can make an application operate on the wrong object, especially when it checks a path and later renames or deletes it. The correct choice depends on whether the code needs a textual route, a reference to a directory entry, or an object that performs entry operations.

An entry_ref identifies a leaf name in a parent directory on a device. It may represent a concrete existing entry or an abstract name whose parent exists but whose leaf does not. If an ancestor directory moves, the reference still identifies the same leaf in the moved directory. If the leaf itself is renamed, the old reference becomes abstract; it does not automatically change to the new name. A pathname has the opposite weakness: moving an ancestor can invalidate the textual path even when the file itself remains the same.

Choose the representation for the operation

Use a pathname when the user supplied or needs a displayable route. Use entry_ref when passing a directory entry through Haiku messages or retaining a name relative to a parent. Use BEntry when an operation needs to resolve, rename, move, or remove an entry through Storage Kit APIs. For file contents, open a BFile or another stream separately; a BEntry is not a file descriptor and does not keep content open.

The distinction makes error handling clearer. A reference can still be structurally meaningful after the leaf disappears, while BEntry::InitCheck() or SetTo() can report that a concrete object no longer exists. A successful path construction does not prove that the path will resolve later. Every operation that depends on current filesystem state should check its own status.

BEntry constructors and SetTo() calls accept a traverse boolean. When true, a symbolic link is resolved to its target; when false, the BEntry refers to the link itself. This is not merely a convenience flag. It changes which object a later rename, move, or remove operation acts on.

For a file manager operation such as “rename this link,” using traverse=false is generally the intended policy. For “open the content this link points to,” following the target may be intended, but the target must still be validated and opened with appropriate permission checks. Make the policy visible in the code, not hidden in a default constructor argument or helper.

Symlink traversal can cross directory boundaries and point outside an application-managed tree. If an import/export routine must remain beneath a root, do not assume a textual prefix check is sufficient. Resolve components using suitable filesystem APIs, reject unexpected links, and re-check as close as possible to the operation. A path that was safe when first inspected can be swapped before use.

Rename changes the leaf identity

BEntry::Rename() renames an existing entry. The public documentation notes that the entry must exist; the clobber argument controls replacement behavior when the destination already exists. Treat clobber=true as destructive, not as a generic way to make a retry succeed. If the destination contains user data, replacement can destroy it.

After a successful rename, update any stored entry_ref or path state by asking the resulting BEntry for its new reference or by resolving the new destination. Do not continue to use the old leaf reference and assume it follows the rename. Other open file descriptors can continue to refer to the open object, while name-based lookups observe the new name. This is a standard filesystem split between open-object identity and directory-entry naming.

If rename fails because the destination exists or the volume is read-only, preserve the original and report the specific failure. A safe “save as” flow can write a temporary sibling, flush and close it, then rename into the selected destination according to an explicit replace policy. The exact atomicity/durability guarantees depend on filesystem and failure mode; do not promise that all crashes leave either the old or new file perfectly durable without consulting the filesystem contract.

MoveTo and Remove have their own consequences

MoveTo() moves an entry to a directory or directory-plus-name destination and also has a clobber option. On the same filesystem, a rename-like move can be efficient. Across volumes, an operation may fail or require a higher-level copy/delete strategy; do not assume a single atomic move across devices. Check the status and define rollback behavior for any multi-step copy.

Remove() removes the directory entry. If file descriptors are open, the file’s data can remain available through those descriptors until they close, while the BEntry becomes abstract. This can surprise code that deletes an entry and then tries to reopen it by name. If the application owns an open stream, close it deliberately and document whether deletion is intended to remove only the name or invalidate active work.

Never use Remove() as cleanup on a path that has not been revalidated. A time-of-check/time-of-use race can redirect a path to a different user file. Prefer retaining the correct entry context, narrow the writable directory, and use unique temporary names. For cleanup of generated files, verify the expected parent and ownership instead of deleting by a broad filename pattern.

A defensive rename outline

The following illustrates the essential checks; a production app should also define user consent and handle name collisions:

BEntry entry(&sourceRef, false); // act on the link itself, if sourceRef is a symlink
status_t status = entry.InitCheck();
if (status != B_OK)
    return status;

status = entry.Rename(newLeafName, false); // do not overwrite an existing entry
if (status != B_OK)
    return status;

entry_ref renamedRef;
status = entry.GetRef(&renamedRef);
if (status != B_OK)
    return status;

The constructor, InitCheck(), Rename(), and GetRef() are public Storage Kit operations. The choice of traverse=false and clobber=false is an explicit policy in this example, not a universal answer. If the user’s intent is to rename a symlink target, set traversal according to that intent and explain the effect before changing data.

Notifications and concurrent mutation

A different process can rename or remove an entry between enumeration and action. Storage Kit node monitoring can notify an application that a directory or node changed, but notifications do not lock the filesystem or guarantee that the next operation succeeds. Treat them as invalidation signals: refresh UI state and retry the actual lookup.

Do not store a BEntry indefinitely and assume its internal state tracks all external renames. Reinitialize from a current reference or path when necessary. For long-running tasks, keep an open handle only when continued access to the same object is desired, and separately decide how to react if the directory name changes.

Verification cases

Test a file rename, a directory rename with a stored descendant entry_ref, a leaf rename followed by use of the old reference, a symlink to a local target, a symlink outside the current tree, and a dangling symlink. Test destinations that already exist with both clobber settings, a read-only volume, and a destination on another volume. Verify that failures preserve source data and that the UI refreshes after an external rename.

When debugging, record the original parent/leaf, whether traversal was enabled, operation name, destination, volume, and exact status. Avoid logging private path components in shared reports. A precise record makes it clear whether the defect is a stale reference, symlink policy, permission problem, or clobber behavior.

The reliable mental model is that names are not identities: entry_ref follows a moved parent but not a renamed leaf, BEntry applies an explicit traversal and mutation policy, and open streams can outlive a removed directory entry. Once those boundaries are respected, file-management operations become safer under renames, symbolic links, and concurrent filesystem changes.

Related:

Sources:

Comments