linux/fs/afs/symlink.c
David Howells 41376400c4
netfs: Replace wb_lock with a bit lock for asynchronicity
The netfs_inode::wb_lock mutex is used to prevent multiple simultaneous
writebacks from fighting each other (a writeback thread will write multiple
discontiguous regions within the same request).  The mutex, however, only
serialises the issuing of subrequests; it doesn't serialise the collection
of results, and, in particular, the updating of file size information and
fscache populatedness data.

Unfortunately, the mutex cannot be held around the entire process as it has
to be unlocked in the same thread in which it is locked - and we don't want
to hold up the allocator whilst we complete the writeback.

Fix this by replacing the mutex with a bit flag and a list of lock waiters
so that the lock can be dropped in the collector thread after collection is
complete.

Link: https://sashiko.dev/#/patchset/20260608145432.681865-1-dhowells%40redhat.com
Signed-off-by: David Howells <dhowells@redhat.com>
Link: https://patch.msgid.link/20260625140640.3116900-12-dhowells@redhat.com
cc: Paulo Alcantara <pc@manguebit.org>
cc: netfs@lists.linux.dev
cc: linux-fsdevel@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-07-01 15:26:29 +02:00

279 lines
6.7 KiB
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,
};