Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/2227566/?format=api
{ "id": 2227566, "url": "http://patchwork.ozlabs.org/api/patches/2227566/?format=api", "web_url": "http://patchwork.ozlabs.org/project/linux-cifs-client/patch/20260423222209.3054909-5-dhowells@redhat.com/", "project": { "id": 12, "url": "http://patchwork.ozlabs.org/api/projects/12/?format=api", "name": "Linux CIFS Client", "link_name": "linux-cifs-client", "list_id": "linux-cifs.vger.kernel.org", "list_email": "linux-cifs@vger.kernel.org", "web_url": "", "scm_url": "", "webscm_url": "", "list_archive_url": "", "list_archive_url_format": "", "commit_url_format": "" }, "msgid": "<20260423222209.3054909-5-dhowells@redhat.com>", "list_archive_url": null, "date": "2026-04-23T22:22:07", "name": "[4/4] afs: Fix RCU handling of symlinks in RCU pathwalk", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "f365ba59279a8d11eaa4840a6cbf21b88db5e8f9", "submitter": { "id": 59, "url": "http://patchwork.ozlabs.org/api/people/59/?format=api", "name": "David Howells", "email": "dhowells@redhat.com" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/linux-cifs-client/patch/20260423222209.3054909-5-dhowells@redhat.com/mbox/", "series": [ { "id": 501258, "url": "http://patchwork.ozlabs.org/api/series/501258/?format=api", "web_url": "http://patchwork.ozlabs.org/project/linux-cifs-client/list/?series=501258", "date": "2026-04-23T22:22:03", "name": "netfs: Yet further miscellaneous fixes", "version": 1, "mbox": "http://patchwork.ozlabs.org/series/501258/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/2227566/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/2227566/checks/", "tags": {}, "related": [], "headers": { "Return-Path": "\n <linux-cifs+bounces-11072-incoming=patchwork.ozlabs.org@vger.kernel.org>", "X-Original-To": [ "incoming@patchwork.ozlabs.org", "linux-cifs@vger.kernel.org" ], "Delivered-To": "patchwork-incoming@legolas.ozlabs.org", "Authentication-Results": [ "legolas.ozlabs.org;\n\tdkim=pass (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=P/6IztW7;\n\tdkim-atps=neutral", "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=vger.kernel.org\n (client-ip=172.234.253.10; helo=sea.lore.kernel.org;\n envelope-from=linux-cifs+bounces-11072-incoming=patchwork.ozlabs.org@vger.kernel.org;\n receiver=patchwork.ozlabs.org)", "smtp.subspace.kernel.org;\n\tdkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com\n header.b=\"P/6IztW7\"", "smtp.subspace.kernel.org;\n arc=none smtp.client-ip=170.10.129.124", "smtp.subspace.kernel.org;\n dmarc=pass (p=quarantine dis=none) header.from=redhat.com", "smtp.subspace.kernel.org;\n spf=pass smtp.mailfrom=redhat.com" ], "Received": [ "from sea.lore.kernel.org (sea.lore.kernel.org [172.234.253.10])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g1rJj4QSSz1xvV\n\tfor <incoming@patchwork.ozlabs.org>; Fri, 24 Apr 2026 08:25:41 +1000 (AEST)", "from smtp.subspace.kernel.org (conduit.subspace.kernel.org\n [100.90.174.1])\n\tby sea.lore.kernel.org (Postfix) with ESMTP id 58CC4302D12E\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 23 Apr 2026 22:22:49 +0000 (UTC)", "from localhost.localdomain (localhost.localdomain [127.0.0.1])\n\tby smtp.subspace.kernel.org (Postfix) with ESMTP id 331A138CFE8;\n\tThu, 23 Apr 2026 22:22:48 +0000 (UTC)", "from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.129.124])\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby smtp.subspace.kernel.org (Postfix) with ESMTPS id 1527E39B962\n\tfor <linux-cifs@vger.kernel.org>; Thu, 23 Apr 2026 22:22:46 +0000 (UTC)", "from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-657-2zaffEGoMVyjOZLej1VECw-1; Thu,\n 23 Apr 2026 18:22:41 -0400", "from mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.12])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n\t(No client certificate requested)\n\tby mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id 525771956056;\n\tThu, 23 Apr 2026 22:22:40 +0000 (UTC)", "from warthog.procyon.org.com (unknown [10.44.48.17])\n\tby mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 427C719560AB;\n\tThu, 23 Apr 2026 22:22:37 +0000 (UTC)" ], "ARC-Seal": "i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;\n\tt=1776982968; cv=none;\n b=Ispzr0ralhfjSbCK+bIv9vkj/5Ir/YqnY05EAD8R2150BZT+hK4uAUuI45YpJAcFSdWov+YN6WVZcrWo0b51ZM2WacPW6U3HjubKGQexNHIMqCcj8D5upi1Adndh8p333Z8l2ErOIbrzcg+RDbpN9TqeXRSesqH3CmCaynaF12c=", "ARC-Message-Signature": "i=1; a=rsa-sha256; d=subspace.kernel.org;\n\ts=arc-20240116; t=1776982968; c=relaxed/simple;\n\tbh=j+CNTHw4JaXJ04qPB20Oy/gfNlGU9rt5C2hsX5WusIU=;\n\th=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:\n\t MIME-Version;\n b=bV5rjHut0FDA/xwwDL19Zbhxbqt+gzUdaP5xQKrhkuVlgrEV/m+gZXzbpnWCxE53h4wDViVxbtHH+unazW7T27oRq5gFtnx7vK8g5Vogbt1vzoULwIrKbeUGkpFFzKIDGE8h1LxNTIkdeGv5K2oehZx3VxwnlzXk51jGrF9cS3k=", "ARC-Authentication-Results": "i=1; smtp.subspace.kernel.org;\n dmarc=pass (p=quarantine dis=none) header.from=redhat.com;\n spf=pass smtp.mailfrom=redhat.com;\n dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com\n header.b=P/6IztW7; arc=none smtp.client-ip=170.10.129.124", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1776982965;\n\th=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n\t to:to:cc:cc:mime-version:mime-version:\n\t content-transfer-encoding:content-transfer-encoding:\n\t in-reply-to:in-reply-to:references:references;\n\tbh=TahHXd/0zLYPsZIgom/gtrfZo11GRDjkH9gAacnDx7E=;\n\tb=P/6IztW7/+xG3veLb4aiHEPc1jvMJYL7n30UG4CxOajmPwoQtkT/KHmKUvTzuSjji6gMV1\n\tkISLjRjlbBkDzmxse7kZL/0gGJaN6wMQGeq0Pcy5OfcRRe2fJIfjzCZEoC6af/Qtj+yb67\n\tW2/K+vDGo69Hy1w+iNXMHuflmIxyjm8=", "X-MC-Unique": "2zaffEGoMVyjOZLej1VECw-1", "X-Mimecast-MFC-AGG-ID": "2zaffEGoMVyjOZLej1VECw_1776982960", "From": "David Howells <dhowells@redhat.com>", "To": "Christian Brauner <christian@brauner.io>", "Cc": "David Howells <dhowells@redhat.com>,\n\tPaulo Alcantara <pc@manguebit.org>,\n\tnetfs@lists.linux.dev,\n\tlinux-afs@lists.infradead.org,\n\tlinux-cifs@vger.kernel.org,\n\tceph-devel@vger.kernel.org,\n\tlinux-fsdevel@vger.kernel.org,\n\tlinux-kernel@vger.kernel.org,\n\tMarc Dionne <marc.dionne@auristor.com>", "Subject": "[PATCH 4/4] afs: Fix RCU handling of symlinks in RCU pathwalk", "Date": "Thu, 23 Apr 2026 23:22:07 +0100", "Message-ID": "<20260423222209.3054909-5-dhowells@redhat.com>", "In-Reply-To": "<20260423222209.3054909-1-dhowells@redhat.com>", "References": "<20260423222209.3054909-1-dhowells@redhat.com>", "Precedence": "bulk", "X-Mailing-List": "linux-cifs@vger.kernel.org", "List-Id": "<linux-cifs.vger.kernel.org>", "List-Subscribe": "<mailto:linux-cifs+subscribe@vger.kernel.org>", "List-Unsubscribe": "<mailto:linux-cifs+unsubscribe@vger.kernel.org>", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "X-Scanned-By": "MIMEDefang 3.0 on 10.30.177.12" }, "content": "The afs filesystem in the kernel doesn't handle RCU pathwalk of symlinks\ncorrectly. The problem is twofold: firstly, it doesn't treat the buffer\npointers as RCU pointers with the appropriate barriering; and secondly, it\ncan race with another thread updating the contents of the symlink because a\nthird party updated it on the server.\n\nFix this by the following means:\n\n (1) Keep a separate copy of the symlink contents with an rcu_head. This\n is always going to be a lot smaller than a page, so it can be\n kmalloc'd and save quite a bit of memory. It also needs a refcount\n for non-RCU pathwalk.\n\n (2) Split the symlink read and write-to-cache routines in afs from those\n for directories.\n\n (3) Discard the I/O buffer as soon as the write-to-cache completes as this\n is a full page (plus a folio_queue).\n\n (4) If there's no cache, discard the I/O buffer immediately after reading\n and copying if there is no cache.\n\nFixes: 6698c02d64b2 (\"afs: Locally initialise the contents of a new symlink on creation\")\nCloses: https://sashiko.dev/#/patchset/20260326104544.509518-1-dhowells%40redhat.com\nSigned-off-by: David Howells <dhowells@redhat.com>\ncc: Marc Dionne <marc.dionne@auristor.com>\ncc: linux-afs@lists.infradead.org\ncc: linux-fsdevel@vger.kernel.org\n---\n fs/afs/Makefile | 1 +\n fs/afs/dir.c | 33 +++++--\n fs/afs/fsclient.c | 4 +-\n fs/afs/inode.c | 105 +-------------------\n fs/afs/internal.h | 35 +++++--\n fs/afs/symlink.c | 242 +++++++++++++++++++++++++++++++++++++++++++++\n fs/afs/yfsclient.c | 4 +-\n 7 files changed, 303 insertions(+), 121 deletions(-)\n create mode 100644 fs/afs/symlink.c", "diff": "diff --git a/fs/afs/Makefile b/fs/afs/Makefile\nindex b49b8fe682f3..0d8f1982d596 100644\n--- a/fs/afs/Makefile\n+++ b/fs/afs/Makefile\n@@ -30,6 +30,7 @@ kafs-y := \\\n \tserver.o \\\n \tserver_list.o \\\n \tsuper.o \\\n+\tsymlink.o \\\n \tvalidation.o \\\n \tvlclient.o \\\n \tvl_alias.o \\\ndiff --git a/fs/afs/dir.c b/fs/afs/dir.c\nindex aaaa55878ffd..40f6791114ec 100644\n--- a/fs/afs/dir.c\n+++ b/fs/afs/dir.c\n@@ -68,7 +68,7 @@ const struct inode_operations afs_dir_inode_operations = {\n };\n \n const struct address_space_operations afs_dir_aops = {\n-\t.writepages\t= afs_single_writepages,\n+\t.writepages\t= afs_dir_writepages,\n };\n \n const struct dentry_operations afs_fs_dentry_operations = {\n@@ -294,7 +294,7 @@ static ssize_t afs_do_read_single(struct afs_vnode *dvnode, struct file *file)\n \treturn ret;\n }\n \n-ssize_t afs_read_single(struct afs_vnode *dvnode, struct file *file)\n+static ssize_t afs_read_single(struct afs_vnode *dvnode, struct file *file)\n {\n \tssize_t ret;\n \n@@ -1763,13 +1763,20 @@ static int afs_link(struct dentry *from, struct inode *dir,\n \treturn ret;\n }\n \n+static void afs_symlink_put(struct afs_operation *op)\n+{\n+\tkfree(op->create.symlink);\n+\top->create.symlink = NULL;\n+\tafs_create_put(op);\n+}\n+\n static const struct afs_operation_ops afs_symlink_operation = {\n \t.issue_afs_rpc\t= afs_fs_symlink,\n \t.issue_yfs_rpc\t= yfs_fs_symlink,\n \t.success\t= afs_create_success,\n \t.aborted\t= afs_check_for_remote_deletion,\n \t.edit_dir\t= afs_create_edit_dir,\n-\t.put\t\t= afs_create_put,\n+\t.put\t\t= afs_symlink_put,\n };\n \n /*\n@@ -1779,7 +1786,9 @@ static int afs_symlink(struct mnt_idmap *idmap, struct inode *dir,\n \t\t struct dentry *dentry, const char *content)\n {\n \tstruct afs_operation *op;\n+\tstruct afs_symlink *symlink;\n \tstruct afs_vnode *dvnode = AFS_FS_I(dir);\n+\tsize_t clen = strlen(content);\n \tint ret;\n \n \t_enter(\"{%llx:%llu},{%pd},%s\",\n@@ -1791,12 +1800,20 @@ static int afs_symlink(struct mnt_idmap *idmap, struct inode *dir,\n \t\tgoto error;\n \n \tret = -EINVAL;\n-\tif (strlen(content) >= AFSPATHMAX)\n+\tif (clen >= AFSPATHMAX)\n+\t\tgoto error;\n+\n+\tret = -ENOMEM;\n+\tsymlink = kmalloc_flex(struct afs_symlink, content, clen + 1, GFP_KERNEL);\n+\tif (!symlink)\n \t\tgoto error;\n+\trefcount_set(&symlink->ref, 1);\n+\tmemcpy(symlink->content, content, clen + 1);\n \n \top = afs_alloc_operation(NULL, dvnode->volume);\n \tif (IS_ERR(op)) {\n \t\tret = PTR_ERR(op);\n+\t\tkfree(symlink);\n \t\tgoto error;\n \t}\n \n@@ -1808,7 +1825,7 @@ static int afs_symlink(struct mnt_idmap *idmap, struct inode *dir,\n \top->dentry\t\t= dentry;\n \top->ops\t\t\t= &afs_symlink_operation;\n \top->create.reason\t= afs_edit_dir_for_symlink;\n-\top->create.symlink\t= content;\n+\top->create.symlink\t= symlink;\n \top->mtime\t\t= current_time(dir);\n \tret = afs_do_sync_operation(op);\n \tafs_dir_unuse_cookie(dvnode, ret);\n@@ -2192,10 +2209,10 @@ static int afs_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n }\n \n /*\n- * Write the file contents to the cache as a single blob.\n+ * Write the directory contents to the cache as a single blob.\n */\n-int afs_single_writepages(struct address_space *mapping,\n-\t\t\t struct writeback_control *wbc)\n+int afs_dir_writepages(struct address_space *mapping,\n+\t\t struct writeback_control *wbc)\n {\n \tstruct afs_vnode *dvnode = AFS_FS_I(mapping->host);\n \tstruct iov_iter iter;\ndiff --git a/fs/afs/fsclient.c b/fs/afs/fsclient.c\nindex 95494d5f2b8a..a2ffd60889f8 100644\n--- a/fs/afs/fsclient.c\n+++ b/fs/afs/fsclient.c\n@@ -886,7 +886,7 @@ void afs_fs_symlink(struct afs_operation *op)\n \tnamesz = name->len;\n \tpadsz = (4 - (namesz & 3)) & 3;\n \n-\tc_namesz = strlen(op->create.symlink);\n+\tc_namesz = strlen(op->create.symlink->content);\n \tc_padsz = (4 - (c_namesz & 3)) & 3;\n \n \treqsz = (6 * 4) + namesz + padsz + c_namesz + c_padsz + (6 * 4);\n@@ -910,7 +910,7 @@ void afs_fs_symlink(struct afs_operation *op)\n \t\tbp = (void *) bp + padsz;\n \t}\n \t*bp++ = htonl(c_namesz);\n-\tmemcpy(bp, op->create.symlink, c_namesz);\n+\tmemcpy(bp, op->create.symlink->content, c_namesz);\n \tbp = (void *) bp + c_namesz;\n \tif (c_padsz > 0) {\n \t\tmemset(bp, 0, c_padsz);\ndiff --git a/fs/afs/inode.c b/fs/afs/inode.c\nindex 5207c4a003f6..ff2b8fc7f3df 100644\n--- a/fs/afs/inode.c\n+++ b/fs/afs/inode.c\n@@ -25,105 +25,6 @@\n #include \"internal.h\"\n #include \"afs_fs.h\"\n \n-void afs_init_new_symlink(struct afs_vnode *vnode, struct afs_operation *op)\n-{\n-\tsize_t size = strlen(op->create.symlink) + 1;\n-\tsize_t dsize = 0;\n-\tchar *p;\n-\n-\tif (netfs_alloc_folioq_buffer(NULL, &vnode->directory, &dsize, size,\n-\t\t\t\t mapping_gfp_mask(vnode->netfs.inode.i_mapping)) < 0)\n-\t\treturn;\n-\n-\tvnode->directory_size = dsize;\n-\tp = kmap_local_folio(folioq_folio(vnode->directory, 0), 0);\n-\tmemcpy(p, op->create.symlink, size);\n-\tkunmap_local(p);\n-\tset_bit(AFS_VNODE_DIR_READ, &vnode->flags);\n-\tnetfs_single_mark_inode_dirty(&vnode->netfs.inode);\n-}\n-\n-static void afs_put_link(void *arg)\n-{\n-\tstruct folio *folio = virt_to_folio(arg);\n-\n-\tkunmap_local(arg);\n-\tfolio_put(folio);\n-}\n-\n-const char *afs_get_link(struct dentry *dentry, struct inode *inode,\n-\t\t\t struct delayed_call *callback)\n-{\n-\tstruct afs_vnode *vnode = AFS_FS_I(inode);\n-\tstruct folio *folio;\n-\tchar *content;\n-\tssize_t ret;\n-\n-\tif (!dentry) {\n-\t\t/* RCU pathwalk. */\n-\t\tif (!test_bit(AFS_VNODE_DIR_READ, &vnode->flags) || !afs_check_validity(vnode))\n-\t\t\treturn ERR_PTR(-ECHILD);\n-\t\tgoto good;\n-\t}\n-\n-\tif (test_bit(AFS_VNODE_DIR_READ, &vnode->flags))\n-\t\tgoto fetch;\n-\n-\tret = afs_validate(vnode, NULL);\n-\tif (ret < 0)\n-\t\treturn ERR_PTR(ret);\n-\n-\tif (!test_and_clear_bit(AFS_VNODE_ZAP_DATA, &vnode->flags) &&\n-\t test_bit(AFS_VNODE_DIR_READ, &vnode->flags))\n-\t\tgoto good;\n-\n-fetch:\n-\tif (down_write_killable(&vnode->validate_lock) < 0)\n-\t\treturn ERR_PTR(-ERESTARTSYS);\n-\tif (test_and_clear_bit(AFS_VNODE_ZAP_DATA, &vnode->flags) ||\n-\t !test_bit(AFS_VNODE_DIR_READ, &vnode->flags)) {\n-\t\tret = afs_read_single(vnode, NULL);\n-\t\tif (ret < 0) {\n-\t\t\tup_write(&vnode->validate_lock);\n-\t\t\treturn ERR_PTR(ret);\n-\t\t}\n-\t\tset_bit(AFS_VNODE_DIR_READ, &vnode->flags);\n-\t}\n-\n-\tup_write(&vnode->validate_lock);\n-\n-good:\n-\tfolio = folioq_folio(vnode->directory, 0);\n-\tfolio_get(folio);\n-\tcontent = kmap_local_folio(folio, 0);\n-\tset_delayed_call(callback, afs_put_link, content);\n-\treturn content;\n-}\n-\n-int afs_readlink(struct dentry *dentry, char __user *buffer, int buflen)\n-{\n-\tDEFINE_DELAYED_CALL(done);\n-\tconst char *content;\n-\tint len;\n-\n-\tcontent = afs_get_link(dentry, d_inode(dentry), &done);\n-\tif (IS_ERR(content)) {\n-\t\tdo_delayed_call(&done);\n-\t\treturn PTR_ERR(content);\n-\t}\n-\n-\tlen = umin(strlen(content), buflen);\n-\tif (copy_to_user(buffer, content, len))\n-\t\tlen = -EFAULT;\n-\tdo_delayed_call(&done);\n-\treturn len;\n-}\n-\n-static const struct inode_operations afs_symlink_inode_operations = {\n-\t.get_link\t= afs_get_link,\n-\t.readlink\t= afs_readlink,\n-};\n-\n static noinline void dump_vnode(struct afs_vnode *vnode, struct afs_vnode *parent_vnode)\n {\n \tstatic unsigned long once_only;\n@@ -223,7 +124,7 @@ static int afs_inode_init_from_status(struct afs_operation *op,\n \t\t\tinode->i_mode\t= S_IFLNK | status->mode;\n \t\t\tinode->i_op\t= &afs_symlink_inode_operations;\n \t\t}\n-\t\tinode->i_mapping->a_ops\t= &afs_dir_aops;\n+\t\tinode->i_mapping->a_ops\t= &afs_symlink_aops;\n \t\tinode_nohighmem(inode);\n \t\tmapping_set_release_always(inode->i_mapping);\n \t\tbreak;\n@@ -765,12 +666,14 @@ void afs_evict_inode(struct inode *inode)\n \t\t\t.range_end = LLONG_MAX,\n \t\t};\n \n-\t\tafs_single_writepages(inode->i_mapping, &wbc);\n+\t\tinode->i_mapping->a_ops->writepages(inode->i_mapping, &wbc);\n \t}\n \n \tnetfs_wait_for_outstanding_io(inode);\n \ttruncate_inode_pages_final(&inode->i_data);\n \tnetfs_free_folioq_buffer(vnode->directory);\n+\tif (vnode->symlink)\n+\t\tafs_replace_symlink(vnode, NULL);\n \n \tafs_set_cache_aux(vnode, &aux);\n \tnetfs_clear_inode_writeback(inode, &aux);\ndiff --git a/fs/afs/internal.h b/fs/afs/internal.h\nindex 009064b8d661..802ae22133ae 100644\n--- a/fs/afs/internal.h\n+++ b/fs/afs/internal.h\n@@ -711,6 +711,7 @@ struct afs_vnode {\n #define AFS_VNODE_DIR_READ\t11\t\t/* Set if we've read a dir's contents */\n \n \tstruct folio_queue\t*directory;\t/* Directory contents */\n+\tstruct afs_symlink __rcu *symlink;\t/* Symlink content */\n \tstruct list_head\twb_keys;\t/* List of keys available for writeback */\n \tstruct list_head\tpending_locks;\t/* locks waiting to be granted */\n \tstruct list_head\tgranted_locks;\t/* locks granted on this file */\n@@ -777,6 +778,15 @@ struct afs_permits {\n \tstruct afs_permit\tpermits[] __counted_by(nr_permits);\t/* List of permits sorted by key pointer */\n };\n \n+/*\n+ * Copy of symlink content for normal use.\n+ */\n+struct afs_symlink {\n+\tstruct rcu_head\t\trcu;\n+\trefcount_t\t\tref;\n+\tchar\t\t\tcontent[];\n+};\n+\n /*\n * Error prioritisation and accumulation.\n */\n@@ -888,7 +898,7 @@ struct afs_operation {\n \t\tstruct {\n \t\t\tint\treason;\t\t/* enum afs_edit_dir_reason */\n \t\t\tmode_t\tmode;\n-\t\t\tconst char *symlink;\n+\t\t\tstruct afs_symlink *symlink;\n \t\t} create;\n \t\tstruct {\n \t\t\tbool\tneed_rehash;\n@@ -1099,13 +1109,12 @@ extern const struct inode_operations afs_dir_inode_operations;\n extern const struct address_space_operations afs_dir_aops;\n extern const struct dentry_operations afs_fs_dentry_operations;\n \n-ssize_t afs_read_single(struct afs_vnode *dvnode, struct file *file);\n ssize_t afs_read_dir(struct afs_vnode *dvnode, struct file *file)\n \t__acquires(&dvnode->validate_lock);\n extern void afs_d_release(struct dentry *);\n extern void afs_check_for_remote_deletion(struct afs_operation *);\n-int afs_single_writepages(struct address_space *mapping,\n-\t\t\t struct writeback_control *wbc);\n+int afs_dir_writepages(struct address_space *mapping,\n+\t\t struct writeback_control *wbc);\n \n /*\n * dir_edit.c\n@@ -1247,10 +1256,6 @@ extern void afs_fs_probe_cleanup(struct afs_net *);\n */\n extern const struct afs_operation_ops afs_fetch_status_operation;\n \n-void afs_init_new_symlink(struct afs_vnode *vnode, struct afs_operation *op);\n-const char *afs_get_link(struct dentry *dentry, struct inode *inode,\n-\t\t\t struct delayed_call *callback);\n-int afs_readlink(struct dentry *dentry, char __user *buffer, int buflen);\n extern void afs_vnode_commit_status(struct afs_operation *, struct afs_vnode_param *);\n extern int afs_fetch_status(struct afs_vnode *, struct key *, bool, afs_access_t *);\n extern int afs_ilookup5_test_by_fid(struct inode *, void *);\n@@ -1600,6 +1605,20 @@ void afs_detach_volume_from_servers(struct afs_volume *volume, struct afs_server\n extern int __init afs_fs_init(void);\n extern void afs_fs_exit(void);\n \n+/*\n+ * symlink.c\n+ */\n+extern const struct inode_operations afs_symlink_inode_operations;\n+extern const struct address_space_operations afs_symlink_aops;\n+\n+void afs_replace_symlink(struct afs_vnode *vnode, struct afs_symlink *symlink);\n+void afs_init_new_symlink(struct afs_vnode *vnode, struct afs_operation *op);\n+const char *afs_get_link(struct dentry *dentry, struct inode *inode,\n+\t\t\t struct delayed_call *callback);\n+int afs_readlink(struct dentry *dentry, char __user *buffer, int buflen);\n+int afs_symlink_writepages(struct address_space *mapping,\n+\t\t\t struct writeback_control *wbc);\n+\n /*\n * validation.c\n */\ndiff --git a/fs/afs/symlink.c b/fs/afs/symlink.c\nnew file mode 100644\nindex 000000000000..8d2521c5f19d\n--- /dev/null\n+++ b/fs/afs/symlink.c\n@@ -0,0 +1,242 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+/* AFS filesystem symbolic link handling\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc. All Rights Reserved.\n+ * Written by David Howells (dhowells@redhat.com)\n+ */\n+\n+#include <linux/kernel.h>\n+#include <linux/fs.h>\n+#include <linux/namei.h>\n+#include <linux/pagemap.h>\n+#include <linux/iov_iter.h>\n+#include \"internal.h\"\n+\n+static void afs_put_symlink(struct afs_symlink *symlink)\n+{\n+\tif (refcount_dec_and_test(&symlink->ref))\n+\t\tkfree_rcu(symlink, rcu);\n+}\n+\n+void afs_replace_symlink(struct afs_vnode *vnode, struct afs_symlink *symlink)\n+{\n+\tstruct afs_symlink *old;\n+\n+\told = rcu_replace_pointer(vnode->symlink, symlink,\n+\t\t\t\t lockdep_is_held(&vnode->validate_lock));\n+\tif (old)\n+\t\tafs_put_symlink(old);\n+}\n+\n+/*\n+ * Set up a locally created symlink inode for immediate write to the cache.\n+ */\n+void afs_init_new_symlink(struct afs_vnode *vnode, struct afs_operation *op)\n+{\n+\tsize_t dsize = 0;\n+\tsize_t size = strlen(op->create.symlink->content) + 1;\n+\tchar *p;\n+\n+\trcu_assign_pointer(vnode->symlink, op->create.symlink);\n+\top->create.symlink = NULL;\n+\n+\tif (!fscache_cookie_enabled(netfs_i_cookie(&vnode->netfs)))\n+\t\treturn;\n+\n+\tif (netfs_alloc_folioq_buffer(NULL, &vnode->directory, &dsize, size,\n+\t\t\t\t mapping_gfp_mask(vnode->netfs.inode.i_mapping)) < 0)\n+\t\treturn;\n+\n+\tvnode->directory_size = dsize;\n+\tp = kmap_local_folio(folioq_folio(vnode->directory, 0), 0);\n+\tmemcpy(p, vnode->symlink, size);\n+\tkunmap_local(p);\n+\tnetfs_single_mark_inode_dirty(&vnode->netfs.inode);\n+}\n+\n+/*\n+ * Read a symlink in a single download.\n+ */\n+static ssize_t afs_do_read_symlink(struct afs_vnode *vnode)\n+{\n+\tstruct afs_symlink *symlink;\n+\tstruct iov_iter iter;\n+\tssize_t ret;\n+\tloff_t i_size;\n+\n+\ti_size = i_size_read(&vnode->netfs.inode);\n+\tif (i_size > PAGE_SIZE - 1) {\n+\t\ttrace_afs_file_error(vnode, -EFBIG, afs_file_error_dir_big);\n+\t\treturn -EFBIG;\n+\t}\n+\n+\tif (!vnode->directory) {\n+\t\tsize_t cur_size = 0;\n+\n+\t\tret = netfs_alloc_folioq_buffer(NULL,\n+\t\t\t\t\t\t&vnode->directory, &cur_size, PAGE_SIZE,\n+\t\t\t\t\t\tmapping_gfp_mask(vnode->netfs.inode.i_mapping));\n+\t\tvnode->directory_size = PAGE_SIZE - 1;\n+\t\tif (ret < 0)\n+\t\t\treturn ret;\n+\t}\n+\n+\tiov_iter_folio_queue(&iter, ITER_DEST, vnode->directory, 0, 0, PAGE_SIZE);\n+\n+\t/* AFS requires us to perform the read of a symlink as a single unit to\n+\t * avoid issues with the content being changed between reads.\n+\t */\n+\tret = netfs_read_single(&vnode->netfs.inode, NULL, &iter);\n+\tif (ret >= 0) {\n+\t\ti_size = i_size_read(&vnode->netfs.inode);\n+\t\tif (i_size > PAGE_SIZE - 1) {\n+\t\t\ttrace_afs_file_error(vnode, -EFBIG, afs_file_error_dir_big);\n+\t\t\treturn -EFBIG;\n+\t\t}\n+\t\tvnode->directory_size = i_size;\n+\n+\t\t/* Copy the symlink. */\n+\t\tsymlink = kmalloc_flex(struct afs_symlink, content, i_size + 1,\n+\t\t\t\t GFP_KERNEL);\n+\t\tif (!symlink)\n+\t\t\treturn -ENOMEM;\n+\n+\t\trefcount_set(&symlink->ref, 1);\n+\t\tsymlink->content[i_size] = 0;\n+\n+\t\tconst char *s = kmap_local_folio(folioq_folio(vnode->directory, 0), 0);\n+\n+\t\tmemcpy(symlink->content, s, i_size);\n+\t\tkunmap_local(s);\n+\n+\t\tafs_replace_symlink(vnode, symlink);\n+\t}\n+\n+\tif (!fscache_cookie_enabled(netfs_i_cookie(&vnode->netfs))) {\n+\t\tnetfs_free_folioq_buffer(vnode->directory);\n+\t\tvnode->directory = NULL;\n+\t\tvnode->directory_size = 0;\n+\t}\n+\n+\treturn ret;\n+}\n+\n+static ssize_t afs_read_symlink(struct afs_vnode *vnode)\n+{\n+\tssize_t ret;\n+\n+\tfscache_use_cookie(afs_vnode_cache(vnode), false);\n+\tret = afs_do_read_symlink(vnode);\n+\tfscache_unuse_cookie(afs_vnode_cache(vnode), NULL, NULL);\n+\treturn ret;\n+}\n+\n+static void afs_put_link(void *arg)\n+{\n+\tafs_put_symlink(arg);\n+}\n+\n+const char *afs_get_link(struct dentry *dentry, struct inode *inode,\n+\t\t\t struct delayed_call *callback)\n+{\n+\tstruct afs_symlink *symlink;\n+\tstruct afs_vnode *vnode = AFS_FS_I(inode);\n+\tssize_t ret;\n+\n+\tif (!dentry) {\n+\t\t/* RCU pathwalk. */\n+\t\tif (!vnode->symlink || !afs_check_validity(vnode))\n+\t\t\treturn ERR_PTR(-ECHILD);\n+\t\tset_delayed_call(callback, NULL, NULL);\n+\t\treturn rcu_dereference(vnode->symlink)->content;\n+\t}\n+\n+\tif (vnode->symlink) {\n+\t\tret = afs_validate(vnode, NULL);\n+\t\tif (ret < 0)\n+\t\t\treturn ERR_PTR(ret);\n+\n+\t\tdown_read(&vnode->validate_lock);\n+\t\tif (!test_bit(AFS_VNODE_ZAP_DATA, &vnode->flags))\n+\t\t\tgoto good;\n+\t\tup_read(&vnode->validate_lock);\n+\t}\n+\n+\tif (down_write_killable(&vnode->validate_lock) < 0)\n+\t\treturn ERR_PTR(-ERESTARTSYS);\n+\tif (!vnode->symlink ||\n+\t test_and_clear_bit(AFS_VNODE_ZAP_DATA, &vnode->flags)) {\n+\t\tret = afs_read_symlink(vnode);\n+\t\tif (ret < 0) {\n+\t\t\tup_write(&vnode->validate_lock);\n+\t\t\treturn ERR_PTR(ret);\n+\t\t}\n+\t}\n+\n+\tdowngrade_write(&vnode->validate_lock);\n+\t\n+good:\n+\tsymlink = rcu_dereference_protected(vnode->symlink,\n+\t\t\t\t\t lockdep_is_held(&vnode->validate_lock));\n+\trefcount_inc(&symlink->ref);\n+\tup_read(&vnode->validate_lock);\n+\n+\tset_delayed_call(callback, afs_put_link, symlink);\n+\treturn symlink->content;\n+}\n+\n+int afs_readlink(struct dentry *dentry, char __user *buffer, int buflen)\n+{\n+\tDEFINE_DELAYED_CALL(done);\n+\tconst char *content;\n+\tint len;\n+\n+\tcontent = afs_get_link(dentry, d_inode(dentry), &done);\n+\tif (IS_ERR(content)) {\n+\t\tdo_delayed_call(&done);\n+\t\treturn PTR_ERR(content);\n+\t}\n+\n+\tlen = umin(strlen(content), buflen);\n+\tif (copy_to_user(buffer, content, len))\n+\t\tlen = -EFAULT;\n+\tdo_delayed_call(&done);\n+\treturn len;\n+}\n+\n+/*\n+ * Write the symlink contents to the cache as a single blob. We then throw\n+ * away the page we used to receive it.\n+ */\n+int afs_symlink_writepages(struct address_space *mapping,\n+\t\t\t struct writeback_control *wbc)\n+{\n+\tstruct afs_vnode *vnode = AFS_FS_I(mapping->host);\n+\tstruct iov_iter iter;\n+\tint ret = 0;\n+\n+\tdown_write(&vnode->validate_lock);\n+\n+\tif (vnode->directory &&\n+\t atomic64_read(&vnode->cb_expires_at) != AFS_NO_CB_PROMISE) {\n+\t\tiov_iter_folio_queue(&iter, ITER_SOURCE, vnode->directory, 0, 0,\n+\t\t\t\t i_size_read(&vnode->netfs.inode));\n+\t\tret = netfs_writeback_single(mapping, wbc, &iter);\n+\t}\n+\n+\tnetfs_free_folioq_buffer(vnode->directory);\n+\tvnode->directory = NULL;\n+\tvnode->directory_size = 0;\n+\n+\tup_write(&vnode->validate_lock);\n+\treturn ret;\n+}\n+\n+const struct inode_operations afs_symlink_inode_operations = {\n+\t.get_link\t= afs_get_link,\n+\t.readlink\t= afs_readlink,\n+};\n+\n+const struct address_space_operations afs_symlink_aops = {\n+\t.writepages\t= afs_symlink_writepages,\n+};\ndiff --git a/fs/afs/yfsclient.c b/fs/afs/yfsclient.c\nindex 24fb562ebd33..d941179730a9 100644\n--- a/fs/afs/yfsclient.c\n+++ b/fs/afs/yfsclient.c\n@@ -960,7 +960,7 @@ void yfs_fs_symlink(struct afs_operation *op)\n \n \t_enter(\"\");\n \n-\tcontents_sz = strlen(op->create.symlink);\n+\tcontents_sz = strlen(op->create.symlink->content);\n \tcall = afs_alloc_flat_call(op->net, &yfs_RXYFSSymlink,\n \t\t\t\t sizeof(__be32) +\n \t\t\t\t sizeof(struct yfs_xdr_RPCFlags) +\n@@ -981,7 +981,7 @@ void yfs_fs_symlink(struct afs_operation *op)\n \tbp = xdr_encode_u32(bp, 0); /* RPC flags */\n \tbp = xdr_encode_YFSFid(bp, &dvp->fid);\n \tbp = xdr_encode_name(bp, name);\n-\tbp = xdr_encode_string(bp, op->create.symlink, contents_sz);\n+\tbp = xdr_encode_string(bp, op->create.symlink->content, contents_sz);\n \tbp = xdr_encode_YFSStoreStatus(bp, &mode, &op->mtime);\n \tyfs_check_req(call, bp);\n \n", "prefixes": [ "4/4" ] }