macOS/APFS: Is there a supported way to create a hard link from an open file descriptor without pathname substitution?

We are developing a security-sensitive local execution system on macOS and need clarification about supported Darwin/APFS filesystem APIs.

Our requirement is to create an additional directory entry (hard link) for an existing, authorized regular-file object while retaining its original directory entry.

SECURITY REQUIREMENTS

  1. The source file has already been opened and its identity verified using a held file descriptor and fstat().

  2. The source must remain the exact same filesystem object (device/inode identity), not a copy or clone.

  3. An attacker operating under the same user account may concurrently rename, unlink, or replace the source pathname.

  4. The publication operation must not create a hard link to, or otherwise mutate, a substituted source object.

  5. The new link must be created within an already-open, authorized destination directory.

  6. Unsupported configurations must be detectable before attempting a potentially unsafe publication mutation.

  7. The original source directory entry must remain present.

We have reviewed linkat(), fclonefileat(), rename-based approaches, filesystem flags, and directory-descriptor-relative operations.

The difficulty is that linkat() identifies its source through a pathname. Holding a verified source file descriptor does not appear to guarantee that the pathname still identifies the same object when linkat() performs the mutation.

A subsequent fstat() or pathname identity check cannot undo an already-created link to a substituted object.

QUESTIONS

  1. Does macOS provide a supported public API that creates an additional hard link directly from an existing open file descriptor, without resolving a replaceable source pathname?

  2. Does Darwin support an equivalent of Linux linkat() with AT_EMPTY_PATH, or another documented descriptor-based source-linking mechanism?

  3. Can any supported macOS filesystem API guarantee that a concurrent same-user replacement of the source directory entry cannot redirect the publication mutation?

  4. If descriptor-based hard linking is unavailable, is there a supported filesystem or namespace protection mechanism that prevents same-owner source-entry substitution throughout the operation?

  5. Do AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW, AT_RESOLVE_BENEATH, AT_RESOLVE_NOFOLLOW_ANY, or related Darwin flags provide any guarantee that the linked source is the previously verified inode rather than a replacement?

  6. Are the relevant guarantees dependent on macOS version, APFS volume configuration, filesystem flags, privileges, or entitlements?

  7. Is there a supported way to determine whether the required operation and guarantees are available before performing the publication mutation?

We are specifically interested in documented and supported APIs, their identity guarantees, availability conditions, and limitations.

We are not seeking a workaround based solely on comparing file identities after the operation, copying/cloning the file, or assuming that same-user pathname replacement cannot occur.

If no supported public API provides these guarantees, confirmation of that limitation would also be helpful.

Thank you.

  1. Does macOS provide a supported public API that creates an additional hard link directly from an existing open file descriptor, without resolving a replaceable source pathname?
  2. Does Darwin support an equivalent of Linux linkat() with AT_EMPTY_PATH, or another documented descriptor-based source-linking mechanism?
  3. Can any supported macOS filesystem API guarantee that a concurrent same-user replacement of the source directory entry cannot redirect the publication mutation?

It's a very reasonable request and I'd recommend filing a bug asking for us to support this, but, no, we don't currently have an API for this. If you file a bug on this, please post the bug number back here.

  1. If descriptor-based hard linking is unavailable, is there a supported filesystem or namespace protection mechanism that prevents same-owner source-entry substitution throughout the operation?
  2. Do AT_SYMLINK_FOLLOW, AT_SYMLINK_NOFOLLOW, AT_RESOLVE_BENEATH, AT_RESOLVE_NOFOLLOW_ANY, or related Darwin flags provide any guarantee that the linked source is the previously verified inode rather than a replacement?

You can't prevent the issue, but you can detect that it's occurred and address it then. The code is below, but what you need to check is:

  • Confirm that both open files are on the same file volume -> ATTR_CMN_FSID

  • Confirm that both files reference the same object -> ATTR_CMN_FILEID

  • Confirm that both files are hard links (they could also be unmodified clones) -> ATTR_FILE_LINKCOUNT

Here's what that looks like:

//  main.m
//  linkat_fd
//
//  Created by Kevin Elliott on 10/12/26.
//

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#include <sys/types.h>
#include <sys/_types/_fsid_t.h>
#include <sys/attr.h>
#include <stdint.h>
#include <stdbool.h>

// Define a structured buffer matching the requested common attributes layout.
// Elements always follow a specific packing rule and sequence based on the sys/attr.h definitions.
struct FileIdentityBuffer {
    uint32_t       total_length; // Mandated first element by getattrlist
    fsid_t         fsid;          // From ATTR_CMN_FSID
    uint64_t       object_id;    // From ATTR_CMN_FILEID
    uint32_t       link_count;   // From ATTR_FILE_LINKCOUNT (returns mode & link count combined)
} __attribute__((aligned(4), packed));

bool check_files_hardlink_match(int fd1, int fd2) {
    struct attrlist attr_list;
    memset(&attr_list, 0, sizeof(attr_list));
    attr_list.bitmapcount = ATTR_BIT_MAP_COUNT; // Must be set to this macro constant
    attr_list.commonattr =  ATTR_CMN_FSID | ATTR_CMN_FILEID;
    attr_list.fileattr =  ATTR_FILE_LINKCOUNT;

    struct FileIdentityBuffer identity1;
    memset(&identity1, 0, sizeof(identity1));
    if (fgetattrlist(fd1, &attr_list, &identity1, sizeof(struct FileIdentityBuffer), FSOPT_NOFOLLOW) != 0) {
        perror("getattrlist failed");
        return false;
    }

    struct FileIdentityBuffer identity2;
    memset(&identity2, 0, sizeof(identity2));
    if (fgetattrlist(fd2, &attr_list, &identity2, sizeof(struct FileIdentityBuffer), FSOPT_NOFOLLOW) != 0) {
        perror("getattrlist failed");
        return false;
    }
    // Compare matching specifications
    bool same_device = ((identity1.fsid.val[0] == identity2.fsid.val[0])
                        && (identity1.fsid.val[1] == identity2.fsid.val[1]));
    bool same_object = (identity1.object_id == identity2.object_id);
    bool both_link = ((identity1.link_count > 1) && (identity2.link_count > 1));
    
    return (same_device && same_object && both_link);
}

int main(int argc, const char * argv[]) {
    
    const char *dir_path = "/Volumes/HomeBase/Temp/";
    const char *fp1_name = "file1.txt";
    const char *fp2_name = "file2.txt";
    
    int dir = open(dir_path, O_RDONLY);
    if (dir == -1) {
        perror("Failed to open dir");
        return 1;
    }
    
    int target = openat(dir, fp1_name, O_RDONLY);
    if (target == -1) {
        perror("Failed to open target");
        return 1;
    }
    int result = linkat(dir, fp1_name, dir, fp2_name, 0);
    if (result == -1) {
        perror("linkat failed, ignoring");
    }
    int link = openat(dir, fp2_name, O_RDONLY);
    if (link == -1) {
        perror("Failed to open link");
        return 1;
    }
    if (check_files_hardlink_match(target, link)) {
        printf("Hard Link Match!\n");
    } else {
        printf("Unrelated Files.\n");
    }
    return 0;
}
  1. Are the relevant guarantees dependent on macOS version, APFS volume configuration, filesystem flags, privileges, or entitlements?

The code should work fine across all local file systems we ship. Network file systems are a trickier case, as it depends on the details of their implementation, but I'd expect those to fail closed (claiming files were unrelated when they were hard links), not open.

  1. Is there a supported way to determine whether the required operation and guarantees are available before performing the publication mutation?

I'm not sure what your ultimate goal here is, but if you wanted to do full validation before making the file available at a known location, you'd typically create and validate the file at a private location, then move it to its final location using "renameat" and RENAME_EXCL or RENAME_SWAP.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thank you for the detailed response and the code example. This is very helpful.

Your confirmation that macOS currently has no supported API for creating a hard link directly from an open file descriptor resolves our primary question.

We also understand your suggested approach of using ATTR_CMN_FSID, ATTR_CMN_FILEID, and ATTR_FILE_LINKCOUNT to detect whether the resulting hard link references the intended object.

However, our security requirement is specifically to prevent an unauthorized filesystem mutation, rather than detect one afterward.

In our threat model, another process operating under the same user account may concurrently replace the source pathname. If linkat() creates a hard link to that substituted object, detecting the mismatch afterward would not satisfy our requirement because the unauthorized mutation has already occurred.

I would appreciate clarification on two final points:

  1. Regarding your suggestion to validate a file privately and then publish it using renameat() with RENAME_EXCL or RENAME_SWAP:

Is there a supported mechanism that guarantees the published object is the exact previously validated inode, even when another process under the same user account can concurrently modify the source directory?

Our requirement also includes retaining the original directory entry, rather than moving or replacing it.

  1. If no such mechanism exists, would it be accurate to conclude that macOS currently provides no supported public API or filesystem mechanism that guarantees race-free, same-object hard-link publication under this specific same-user adversarial threat model?

We want to ensure we are not overlooking a supported macOS capability before considering a different execution platform.

Thank you again for your guidance.

macOS/APFS: Is there a supported way to create a hard link from an open file descriptor without pathname substitution?
 
 
Q