{"id":2229013,"url":"http://patchwork.ozlabs.org/api/1.1/patches/2229013/?format=json","web_url":"http://patchwork.ozlabs.org/project/linux-cifs-client/patch/20260427154639.180684-23-dhowells@redhat.com/","project":{"id":12,"url":"http://patchwork.ozlabs.org/api/1.1/projects/12/?format=json","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":""},"msgid":"<20260427154639.180684-23-dhowells@redhat.com>","date":"2026-04-27T15:46:37","name":"[v4,22/22] afs: Fix the locking used by afs_get_link()","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"5b62e5e6ad4b320814e1e7940adf84d76eb92d1d","submitter":{"id":59,"url":"http://patchwork.ozlabs.org/api/1.1/people/59/?format=json","name":"David Howells","email":"dhowells@redhat.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/linux-cifs-client/patch/20260427154639.180684-23-dhowells@redhat.com/mbox/","series":[{"id":501682,"url":"http://patchwork.ozlabs.org/api/1.1/series/501682/?format=json","web_url":"http://patchwork.ozlabs.org/project/linux-cifs-client/list/?series=501682","date":"2026-04-27T15:46:15","name":"netfs: Miscellaneous fixes","version":4,"mbox":"http://patchwork.ozlabs.org/series/501682/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2229013/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2229013/checks/","tags":{},"headers":{"Return-Path":"\n <linux-cifs+bounces-11178-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=V3yMEffo;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=vger.kernel.org\n (client-ip=2600:3c0a:e001:db::12fc:5321; helo=sea.lore.kernel.org;\n envelope-from=linux-cifs+bounces-11178-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=\"V3yMEffo\"","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\n [IPv6:2600:3c0a:e001:db::12fc:5321])\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 4g47js1l9pz1xvV\n\tfor <incoming@patchwork.ozlabs.org>; Tue, 28 Apr 2026 02:06:57 +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 CEED6312F8A3\n\tfor <incoming@patchwork.ozlabs.org>; Mon, 27 Apr 2026 15:52:30 +0000 (UTC)","from localhost.localdomain (localhost.localdomain [127.0.0.1])\n\tby smtp.subspace.kernel.org (Postfix) with ESMTP id 5785634D4D6;\n\tMon, 27 Apr 2026 15:48:38 +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 2B0A637AA94\n\tfor <linux-cifs@vger.kernel.org>; Mon, 27 Apr 2026 15:48:36 +0000 (UTC)","from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-507-4DqsPIZzPS-rAJkKIAXW4A-1; Mon,\n 27 Apr 2026 11:48:29 -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-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id 5757C1800454;\n\tMon, 27 Apr 2026 15:48:28 +0000 (UTC)","from warthog.procyon.org.com (unknown [10.44.32.126])\n\tby mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 0FEDF19560AB;\n\tMon, 27 Apr 2026 15:48:24 +0000 (UTC)"],"ARC-Seal":"i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116;\n\tt=1777304918; cv=none;\n b=kecCQaUXVjNoulGA5zSGe9qB6C40PTvUeVrsHoM3p9SEyDBiOCFEU7nnF2gcDOdwJay1KsCZ3puw4w1RulK2UH7po8WAptCylZef8R6UUhtZDw3RTQKPh4ccJ+DItR3nsEVJyiQeW/jxEOLIkUZd7E2SYfPFhRTtYuOpviq5BWM=","ARC-Message-Signature":"i=1; a=rsa-sha256; d=subspace.kernel.org;\n\ts=arc-20240116; t=1777304918; c=relaxed/simple;\n\tbh=we/DZ7XS7ThpZConRkIb81ZLIEbmo6XC0mu3XuzZCvs=;\n\th=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References:\n\t MIME-Version;\n b=f6HlmYrheuOqeDs4aEsa1Ne8fmwzzNwlEQET8T2HgFD10f36ombEZdgFWFhxBesuGED+JZvLdpr4MJJOLWfDqOzoGYyo2GYMs7q976pHStsmDEkk4BPNoTWG5dJ9eB8yYZqiV9PSiKP5Mo4Zlp7hbPR9N+JonHmDT6cZT8kvtVs=","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=V3yMEffo; 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=1777304915;\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=MO61n1x3exEVh2twI4/GkaiLmVRA4RZFNjdjnieVLGM=;\n\tb=V3yMEffoCWjw6Z/qbakyhT+c7g2CXO+cNxikuQiyLdV0TTRnqDfTnQB+JV/9qEWzoFxVaZ\n\txXTcaj3dfkofjFRULeLLMB/iwx2UOewH89dug/OWkFE6NsNVM1xC3B0Us+P1trsTr61ufI\n\tDCKaBrq4fu2yBizVjwvAxSu2aMvvs7w=","X-MC-Unique":"4DqsPIZzPS-rAJkKIAXW4A-1","X-Mimecast-MFC-AGG-ID":"4DqsPIZzPS-rAJkKIAXW4A_1777304908","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 v4 22/22] afs: Fix the locking used by afs_get_link()","Date":"Mon, 27 Apr 2026 16:46:37 +0100","Message-ID":"<20260427154639.180684-23-dhowells@redhat.com>","In-Reply-To":"<20260427154639.180684-1-dhowells@redhat.com>","References":"<20260427154639.180684-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 do locking correctly for symbolic\nlinks.  There are a number of problems:\n\n (1) It doesn't do any locking around afs_read_single() to prevent races\n     between multiple ->get_link() calls, thereby allowing the possibility\n     of leaks.\n\n (2) It doesn't use RCU barriering when accessing the buffer pointers\n     during RCU pathwalk.\n\n (3) It can race with another thread updating the contents of the symlink\n     if a third party updated it on the server.\n\nFix this by the following means:\n\n (0) Move symlink handling into its own file as this makes it more\n     complicated.\n\n (1) Take the validate_lock around afs_read_single() to prevent races\n     between multiple ->get_link() calls.\n\n (2) 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 (3) Split the symlink read and write-to-cache routines in afs from those\n     for directories.\n\n (4) 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 (5) If there's no cache, discard the I/O buffer immediately after reading\n     and copying if there is no cache.\n\nFixes: eae9e78951bb (\"afs: Use netfslib for symlinks, allowing them to be cached\")\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        |  68 +++++------\n fs/afs/fsclient.c   |   4 +-\n fs/afs/inode.c      |  96 +---------------\n fs/afs/internal.h   |  34 ++++--\n fs/afs/symlink.c    | 267 ++++++++++++++++++++++++++++++++++++++++++++\n fs/afs/validation.c |   8 +-\n fs/afs/yfsclient.c  |   4 +-\n 8 files changed, 342 insertions(+), 140 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 3379d01c72fb..87e90b8560e6 100644\n--- a/fs/afs/dir.c\n+++ b/fs/afs/dir.c\n@@ -44,6 +44,8 @@ static int afs_symlink(struct mnt_idmap *idmap, struct inode *dir,\n static int afs_rename(struct mnt_idmap *idmap, struct inode *old_dir,\n \t\t      struct dentry *old_dentry, struct inode *new_dir,\n \t\t      struct dentry *new_dentry, unsigned int flags);\n+static int afs_dir_writepages(struct address_space *mapping,\n+\t\t\t      struct writeback_control *wbc);\n \n const struct file_operations afs_dir_file_operations = {\n \t.open\t\t= afs_dir_open,\n@@ -68,7 +70,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@@ -233,22 +235,13 @@ static ssize_t afs_do_read_single(struct afs_vnode *dvnode, struct file *file)\n \tstruct iov_iter iter;\n \tssize_t ret;\n \tloff_t i_size;\n-\tbool is_dir = (S_ISDIR(dvnode->netfs.inode.i_mode) &&\n-\t\t       !test_bit(AFS_VNODE_MOUNTPOINT, &dvnode->flags));\n \n \ti_size = i_size_read(&dvnode->netfs.inode);\n-\tif (is_dir) {\n-\t\tif (i_size < AFS_DIR_BLOCK_SIZE)\n-\t\t\treturn afs_bad(dvnode, afs_file_error_dir_small);\n-\t\tif (i_size > AFS_DIR_BLOCK_SIZE * 1024) {\n-\t\t\ttrace_afs_file_error(dvnode, -EFBIG, afs_file_error_dir_big);\n-\t\t\treturn -EFBIG;\n-\t\t}\n-\t} else {\n-\t\tif (i_size > AFSPATHMAX) {\n-\t\t\ttrace_afs_file_error(dvnode, -EFBIG, afs_file_error_dir_big);\n-\t\t\treturn -EFBIG;\n-\t\t}\n+\tif (i_size < AFS_DIR_BLOCK_SIZE)\n+\t\treturn afs_bad(dvnode, afs_file_error_dir_small);\n+\tif (i_size > AFS_DIR_BLOCK_SIZE * 1024) {\n+\t\ttrace_afs_file_error(dvnode, -EFBIG, afs_file_error_dir_big);\n+\t\treturn -EFBIG;\n \t}\n \n \t/* Expand the storage.  TODO: Shrink the storage too. */\n@@ -277,24 +270,18 @@ static ssize_t afs_do_read_single(struct afs_vnode *dvnode, struct file *file)\n \t\t\t * buffer.\n \t\t\t */\n \t\t\tret = -ESTALE;\n-\t\t} else if (is_dir) {\n+\t\t} else {\n \t\t\tint ret2 = afs_dir_check(dvnode);\n \n \t\t\tif (ret2 < 0)\n \t\t\t\tret = ret2;\n-\t\t} else if (i_size < folioq_folio_size(dvnode->directory, 0)) {\n-\t\t\t/* NUL-terminate a symlink. */\n-\t\t\tchar *symlink = kmap_local_folio(folioq_folio(dvnode->directory, 0), 0);\n-\n-\t\t\tsymlink[i_size] = 0;\n-\t\t\tkunmap_local(symlink);\n \t\t}\n \t}\n \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 +1750,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 +1773,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 +1787,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 +1812,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,15 +2196,13 @@ 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+static int afs_dir_writepages(struct address_space *mapping,\n+\t\t\t      struct writeback_control *wbc)\n {\n \tstruct afs_vnode *dvnode = AFS_FS_I(mapping->host);\n \tstruct iov_iter iter;\n-\tbool is_dir = (S_ISDIR(dvnode->netfs.inode.i_mode) &&\n-\t\t       !test_bit(AFS_VNODE_MOUNTPOINT, &dvnode->flags));\n \tint ret = 0;\n \n \t/* Need to lock to prevent the folio queue and folios from being thrown\n@@ -2212,9 +2214,7 @@ int afs_single_writepages(struct address_space *mapping,\n \t\tdown_read(&dvnode->validate_lock);\n \t}\n \n-\tif (is_dir ?\n-\t    test_bit(AFS_VNODE_DIR_VALID, &dvnode->flags) :\n-\t    atomic64_read(&dvnode->cb_expires_at) != AFS_NO_CB_PROMISE) {\n+\tif (test_bit(AFS_VNODE_DIR_VALID, &dvnode->flags)) {\n \t\tiov_iter_folio_queue(&iter, ITER_SOURCE, dvnode->directory, 0, 0,\n \t\t\t\t     i_size_read(&dvnode->netfs.inode));\n \t\tret = netfs_writeback_single(mapping, wbc, &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 06e25e1b12df..e8b0c5cec5d6 100644\n--- a/fs/afs/inode.c\n+++ b/fs/afs/inode.c\n@@ -25,96 +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-\tret = afs_read_single(vnode, NULL);\n-\tif (ret < 0)\n-\t\treturn ERR_PTR(ret);\n-\tset_bit(AFS_VNODE_DIR_READ, &vnode->flags);\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@@ -214,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@@ -756,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_evict_symlink(vnode);\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 599353c33337..f7502ee90016 100644\n--- a/fs/afs/internal.h\n+++ b/fs/afs/internal.h\n@@ -710,6 +710,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@@ -776,6 +777,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@@ -887,7 +897,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@@ -1098,13 +1108,10 @@ 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 \n /*\n  * dir_edit.c\n@@ -1246,10 +1253,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@@ -1599,6 +1602,21 @@ 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_invalidate_symlink(struct afs_vnode *vnode);\n+void afs_evict_symlink(struct afs_vnode *vnode);\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..242829121e01\n--- /dev/null\n+++ b/fs/afs/symlink.c\n@@ -0,0 +1,267 @@\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+static 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+ * In the event that a third-party update of a symlink occurs, dispose of the\n+ * copy of the old contents.  Called under ->validate_lock.\n+ */\n+void afs_invalidate_symlink(struct afs_vnode *vnode)\n+{\n+\tafs_replace_symlink(vnode, NULL);\n+}\n+\n+/*\n+ * Dispose of a symlink copy during inode deletion.\n+ */\n+void afs_evict_symlink(struct afs_vnode *vnode)\n+{\n+\tafs_invalidate_symlink(vnode);\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->content, 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\tsymlink = rcu_dereference(vnode->symlink);\n+\t\tif (!symlink || !afs_check_validity(vnode))\n+\t\t\treturn ERR_PTR(-ECHILD);\n+\t\tset_delayed_call(callback, NULL, NULL);\n+\t\treturn 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 (vnode->symlink)\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\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+\tif (!down_read_trylock(&vnode->validate_lock)) {\n+\t\tif (wbc->sync_mode == WB_SYNC_NONE)\n+\t\t\treturn 0;\n+\t\tdown_read(&vnode->validate_lock);\n+\t}\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+\tif (ret == 0) {\n+\t\tnetfs_free_folioq_buffer(vnode->directory);\n+\t\tvnode->directory = NULL;\n+\t\tvnode->directory_size = 0;\n+\t} else if (ret == 1) {\n+\t\tret = 0; /* Skipped write due to lock conflict. */\n+\t}\n+\n+\tup_read(&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/validation.c b/fs/afs/validation.c\nindex 0ba8336c9025..743f19d6ec39 100644\n--- a/fs/afs/validation.c\n+++ b/fs/afs/validation.c\n@@ -468,8 +468,12 @@ int afs_validate(struct afs_vnode *vnode, struct key *key)\n \t/* if the vnode's data version number changed then its contents are\n \t * different */\n \tzap |= test_and_clear_bit(AFS_VNODE_ZAP_DATA, &vnode->flags);\n-\tif (zap)\n-\t\tafs_zap_data(vnode);\n+\tif (zap) {\n+\t\tif (S_ISREG(vnode->netfs.inode.i_mode))\n+\t\t\tafs_zap_data(vnode);\n+\t\telse\n+\t\t\tafs_invalidate_symlink(vnode);\n+\t}\n \tup_write(&vnode->validate_lock);\n \t_leave(\" = 0\");\n \treturn 0;\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":["v4","22/22"]}