root/fs/afs/symlink.c
// SPDX-License-Identifier: GPL-2.0-or-later
/* AFS filesystem symbolic link handling
 *
 * Copyright (C) 2026 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 */

#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/namei.h>
#include <linux/pagemap.h>
#include <linux/iov_iter.h>
#include "internal.h"

static void afs_put_symlink(struct afs_symlink *symlink)
{
        if (refcount_dec_and_test(&symlink->ref))
                kfree_rcu(symlink, rcu);
}

static void afs_replace_symlink(struct afs_vnode *vnode, struct afs_symlink *symlink)
{
        struct afs_symlink *old;

        old = rcu_replace_pointer(vnode->symlink, symlink,
                                  lockdep_is_held(&vnode->validate_lock));
        if (old)
                afs_put_symlink(old);
}

/*
 * In the event that a third-party update of a symlink occurs, dispose of the
 * copy of the old contents.  Called under ->validate_lock.
 */
void afs_invalidate_symlink(struct afs_vnode *vnode)
{
        afs_replace_symlink(vnode, NULL);
}

/*
 * Dispose of a symlink copy during inode deletion.
 */
void afs_evict_symlink(struct afs_vnode *vnode)
{
        struct afs_symlink *old;

        old = rcu_replace_pointer(vnode->symlink, NULL, true);
        if (old)
                afs_put_symlink(old);

}

/*
 * Set up a locally created symlink inode for immediate write to the cache.
 */
void afs_init_new_symlink(struct afs_vnode *vnode, struct afs_operation *op)
{
        struct afs_symlink *symlink = op->create.symlink;
        size_t dsize = 0;
        size_t size = strlen(symlink->content) + 1;
        char *p;

        rcu_assign_pointer(vnode->symlink, symlink);
        op->create.symlink = NULL;

        if (!fscache_cookie_enabled(netfs_i_cookie(&vnode->netfs)))
                return;

        if (netfs_alloc_folioq_buffer(NULL, &vnode->directory, &dsize, size,
                                      mapping_gfp_mask(vnode->netfs.inode.i_mapping)) < 0)
                return;

        vnode->directory_size = dsize;
        p = kmap_local_folio(folioq_folio(vnode->directory, 0), 0);
        memcpy(p, symlink->content, size);
        kunmap_local(p);
        netfs_single_mark_inode_dirty(&vnode->netfs.inode);
}

/*
 * Read a symlink in a single download.
 */
static ssize_t afs_do_read_symlink(struct afs_vnode *vnode)
{
        struct afs_symlink *symlink;
        struct iov_iter iter;
        ssize_t ret;
        loff_t i_size;

        i_size = i_size_read(&vnode->netfs.inode);
        if (i_size > PAGE_SIZE - 1) {
                trace_afs_file_error(vnode, -EFBIG, afs_file_error_dir_big);
                return -EFBIG;
        }

        if (!vnode->directory) {
                size_t cur_size = 0;

                ret = netfs_alloc_folioq_buffer(NULL,
                                                &vnode->directory, &cur_size, PAGE_SIZE,
                                                mapping_gfp_mask(vnode->netfs.inode.i_mapping));
                vnode->directory_size = PAGE_SIZE - 1;
                if (ret < 0)
                        return ret;
        }

        iov_iter_folio_queue(&iter, ITER_DEST, vnode->directory, 0, 0, PAGE_SIZE);

        /* AFS requires us to perform the read of a symlink as a single unit to
         * avoid issues with the content being changed between reads.
         */
        ret = netfs_read_single(&vnode->netfs.inode, NULL, &iter);
        if (ret >= 0) {
                i_size = ret;
                if (i_size > PAGE_SIZE - 1) {
                        trace_afs_file_error(vnode, -EFBIG, afs_file_error_dir_big);
                        return -EFBIG;
                }
                vnode->directory_size = i_size;

                /* Copy the symlink. */
                symlink = kmalloc_flex(struct afs_symlink, content, i_size + 1,
                                       GFP_KERNEL);
                if (!symlink)
                        return -ENOMEM;

                refcount_set(&symlink->ref, 1);
                symlink->content[i_size] = 0;

                const char *s = kmap_local_folio(folioq_folio(vnode->directory, 0), 0);

                memcpy(symlink->content, s, i_size);
                kunmap_local(s);

                afs_replace_symlink(vnode, symlink);
        }

        if (!fscache_cookie_enabled(netfs_i_cookie(&vnode->netfs))) {
                netfs_free_folioq_buffer(vnode->directory);
                vnode->directory = NULL;
                vnode->directory_size = 0;
        }

        return ret;
}

static ssize_t afs_read_symlink(struct afs_vnode *vnode)
{
        ssize_t ret;

        fscache_use_cookie(afs_vnode_cache(vnode), false);
        ret = afs_do_read_symlink(vnode);
        fscache_unuse_cookie(afs_vnode_cache(vnode), NULL, NULL);
        return ret;
}

static void afs_put_link(void *arg)
{
        afs_put_symlink(arg);
}

const char *afs_get_link(struct dentry *dentry, struct inode *inode,
                         struct delayed_call *callback)
{
        struct afs_symlink *symlink;
        struct afs_vnode *vnode = AFS_FS_I(inode);
        ssize_t ret;

        if (!dentry) {
                /* RCU pathwalk. */
                symlink = rcu_dereference(vnode->symlink);
                if (!symlink || !afs_check_validity(vnode))
                        return ERR_PTR(-ECHILD);
                set_delayed_call(callback, NULL, NULL);
                return symlink->content;
        }

        if (vnode->symlink) {
                ret = afs_validate(vnode, NULL);
                if (ret < 0)
                        return ERR_PTR(ret);

                down_read(&vnode->validate_lock);
                if (vnode->symlink)
                        goto good;
                up_read(&vnode->validate_lock);
        }

        if (down_write_killable(&vnode->validate_lock) < 0)
                return ERR_PTR(-ERESTARTSYS);
        if (!vnode->symlink) {
                ret = afs_read_symlink(vnode);
                if (ret < 0) {
                        up_write(&vnode->validate_lock);
                        return ERR_PTR(ret);
                }
        }

        downgrade_write(&vnode->validate_lock);
        
good:
        symlink = rcu_dereference_protected(vnode->symlink,
                                            lockdep_is_held(&vnode->validate_lock));
        refcount_inc(&symlink->ref);
        up_read(&vnode->validate_lock);

        set_delayed_call(callback, afs_put_link, symlink);
        return symlink->content;
}

int afs_readlink(struct dentry *dentry, char __user *buffer, int buflen)
{
        DEFINE_DELAYED_CALL(done);
        const char *content;
        int len;

        content = afs_get_link(dentry, d_inode(dentry), &done);
        if (IS_ERR(content)) {
                do_delayed_call(&done);
                return PTR_ERR(content);
        }

        len = umin(strlen(content), buflen);
        if (copy_to_user(buffer, content, len))
                len = -EFAULT;
        do_delayed_call(&done);
        return len;
}

/*
 * Write the symlink contents to the cache as a single blob.  We then throw
 * away the page we used to receive it.
 */
int afs_symlink_writepages(struct address_space *mapping,
                           struct writeback_control *wbc)
{
        struct afs_vnode *vnode = AFS_FS_I(mapping->host);
        struct iov_iter iter;
        int ret = 0;

        if (!down_read_trylock(&vnode->validate_lock)) {
                if (wbc->sync_mode == WB_SYNC_NONE) {
                        /* The VFS will have undirtied the inode. */
                        netfs_single_mark_inode_dirty(&vnode->netfs.inode);
                        return 0;
                }
                down_read(&vnode->validate_lock);
        }

        if (vnode->directory &&
            atomic64_read(&vnode->cb_expires_at) != AFS_NO_CB_PROMISE) {
                iov_iter_folio_queue(&iter, ITER_SOURCE, vnode->directory, 0, 0,
                                     i_size_read(&vnode->netfs.inode));
                ret = netfs_writeback_single(mapping, wbc, &iter);
        }

        if (ret == 0) {
                netfs_wb_begin(&vnode->netfs, false);
                netfs_free_folioq_buffer(vnode->directory);
                vnode->directory = NULL;
                vnode->directory_size = 0;
                netfs_wb_end(&vnode->netfs);
        } else if (ret == 1) {
                ret = 0; /* Skipped write due to lock conflict. */
        }

        up_read(&vnode->validate_lock);
        return ret;
}

const struct inode_operations afs_symlink_inode_operations = {
        .get_link       = afs_get_link,
        .readlink       = afs_readlink,
};

const struct address_space_operations afs_symlink_aops = {
        .writepages     = afs_symlink_writepages,
};