| // 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) { |
| mutex_lock(&vnode->netfs.wb_lock); |
| netfs_free_folioq_buffer(vnode->directory); |
| vnode->directory = NULL; |
| vnode->directory_size = 0; |
| mutex_unlock(&vnode->netfs.wb_lock); |
| } 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, |
| }; |