Dealing with semantically important xattrs

We previously spoke about extended attributes like they were just another piece of metadata attached to files.

However some have rather awkward interfaces as far as copying is concerned, some because they don't depend on the file's contents itself, and some because they are filesystem specific.

Selinux labels

Selinux is a complicated mandatory access control mechanism.

Rather than store the access control rules in the file, like POSIX ACLs do, the rules are stored elsewhere in the kernel and a reference to what kind of file it is, is stored in the file as an extended attribute called the "security label".

The security label and the security context of the process accessing the file are looked up in the ACL rules in the kernel to determine whether the operation is permitted.

The details of how to define Selinux rules is complicated and beyond the scope of this article. We only care how we should reapply the rules when moving the file.

While we could copy the label from the old file into the new file, as we did for POSIX ACLs, Selinux contexts are defined by their file paths rather than the inodes, so after we move a file we should relabel it to what the file should have in the new location.

Using selinux_restorecon(3) might be tempting, but it leaves open a race condition where the file would be created with the wrong context so it temporarily accessible with the wrong label.

If the file context should be preserved from the original file, then you must read the context from the extended attribute, either directly with fgetxattr(2) or fgetfilecon(3), and then set the context before creating the new file with setfscreatecon(3).

If instead it should have the label that the path database says it should be, then the required context can be found by using selabel_open(3) with SELABEL_CTX_FILE to get a reference to the file contexts database, then getting the label it should have at that path using selabel_lookup(3), and setting the context for new files with setfscreatecon(3).

Existing files can have their labels changed with selinux_restorecon(3).

The setfscreatecon(3) API is unfortunate as it involves global state. Recent enough versions of Linux have the O_TMPFILE flag for file creation, which doesn't create a directory entry for the file when it is created, so you can modify the file before it is visible to other processes, and can be bound into place with linkat(2).

int set_selinux_create_context(const char *tgt, mode_t srcmode) {
    int ret = 0;
    struct selabel_handle *hnd = NULL;
    char *context = NULL;

    hnd = selabel_open(SELABEL_CTX_FILE, NULL, 0);
    if (hnd == NULL) {
        if (errno != ENOENT) {
            ret = 1;
        }
        goto cleanup;
    }

    ret = selabel_lookup(hnd, &context, tgt, srcmode);
    if (ret != 0) {
        goto cleanup;
    }

    ret = setfscreatecon(context);

cleanup:
    freecon(context);
    if (hnd != NULL)
        selabel_close(hnd);
    return ret;
}

SMACK

This is another security technology.

Like Selinux it has labels. These are stored in extended attributes matching security.SMACK64*, so require root privileges to copy faithfully.

btrfs flags

Only worth copying if both source and destination are on btrfs, but if you then move a file back to btrfs you might want to restore them.

The only flag of real interest is "btrfs.compression", which is safe to ignore if moving to a file system which doesn't support it.

A "brain dead" implementation for this and SMACK is to check the prefix, and silently accept failure if setting the attribute fails.

static int copy_xattrs(int srcfd, int tgtfd) {
    ssize_t ret;
    char *names = NULL;
    void *value = NULL;
    size_t names_size = 0, value_size = 0;

    ret = xattr_list(srcfd, &names, &names_size);
    if (ret < 0)
        goto cleanup;

    for (char *name = names; name < names + names_size;
         name = strchrnul(name, '\0') + 1) {
        /* Skip xattrs that need special handling */
        if (!str_starts_with(name, "user.") &&
            !str_starts_with(name, "security.SMACK64") &&
            !str_starts_with(name, "btrfs.")) {
            continue;
        }

        ret = xattr_get(srcfd, name, &value, &value_size);
        if (ret < 0)
            goto cleanup;

        ret = TEMP_FAILURE_RETRY(fsetxattr(tgtfd, name, value, value_size, 0));
        if (ret < 0) {
            if (errno == EINVAL &&
                (str_starts_with(name, "security.SMACK64") ||
                 str_starts_with(name, "btrfs."))) {
                continue;
            }
            goto cleanup;
        }
    }

cleanup:
    free(names);
    free(value);
    return ret;
}

As with previous articles, the full version of the my-mv.c source file and the Makefile may be downloaded.

The Makefile has changed since earlier since it now needs to link against libselinux.

So now we've got an equivalent to a slow rename(2), right?

Not quite, rename(2) is atomic. It disappears from the old location and reappears whole at the new one at the same time.