From nobody Mon Sep 28 21:03:28 2026 Received: from mail-wm1-f50.google.com (mail-wm1-f50.google.com [209.85.128.50]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 33F7D272803 for ; Mon, 17 Aug 2026 16:17:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786983456; cv=none; b=rl3uFKpElQqotYwv1g3Q+G6Gg5eUnjlNSsMEwAk2/LTAfHhQIGayJh0dxEE62KlGKWLVlFoUDeytphO/kWwSpixKxsue8zuG1sRqyDN7MfW4A07a7Y8SUceebDx/pP7KfgwEZUItuE0eXBR035kNemrR8rqt92dOriE6fWrnGGA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786983456; c=relaxed/simple; bh=I48LyXxTdxXxQ1cyphtRSI0QVP19kxuOc3W7VYOK1lI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=FTZf9L/pe5D1vtqabumIGy+ANRVOvtJ/gnwZufs2TtQzk4XfFiT+yDbw4CMTghvafJUmzw18W7WjPua8K1dXdceRpcQCGO3oAxCXKW3kBDPuCAOA8aD1+mTYNqnqjhQLog74tm0bJu+CAi3hf6lUIiBj0u3ct5rxBnaLhct16G4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=dpoHSi73; arc=none smtp.client-ip=209.85.128.50 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="dpoHSi73" Received: by mail-wm1-f50.google.com with SMTP id 5b1f17b1804b1-49557167508so36845385e9.1 for ; Mon, 17 Aug 2026 09:17:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1786983452; x=1787588252; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=o6/t8/4N3XQWXCm14IpoEgljHMU303esyG3KYaRnKWg=; b=dpoHSi73iwBZvlJY2UbLVl4ycLfJiYp8/0ecTQMsFMQjKd8r4FfeIoR3CKUf2gPE8f SP9YnOQ3NZUAdL1+rNODsmL9wk5NsHVlSpAzMAqocz1S9jZUFwh/62QNj5hiOvfrWZGl BrKYUExA8MetQ008sy4pg9WL32yrdt3w6+WKScpwv6NWEaBYcb8CaJLi+JcozsKL4MNs gqjUHigF23TOETcAM2dHwEuHPtUBNlUvrmVlA8PIvn9Io5r6UZaKmmPPND0e8hvReygb Spua+dyJHutk3fsHICXGyH6Bdx7xU+LdUk9Aw75Lll8ByZqnH9TdHU0zse9yeDuejMGe yFjg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786983452; x=1787588252; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=o6/t8/4N3XQWXCm14IpoEgljHMU303esyG3KYaRnKWg=; b=tJJP8+BupBvRuRZ96kUNs7QECy+dNRD2C4CkcgQuQWqnCu9YoeeQO7C9UemdFWm0wn yW5bER4j5/vTPj4SMv4f04Xix1zEsP9WYzs4K1vyOxmYFdZWiMorG7WVDdZ8ZqL+BpOV Z7eFLnB7HATfn+OxmXx4sc/j3eT/mFuL2peTDYFYLeWIoQeJeTwdpaqu1pBjR16z86p6 nlVzd0SNEy74cxfNJJH6LQhnXkTB2KOVnzC8XA+W4u8wVmwPsUGEk6chVi3AW9Z9mwsQ bYcydYkEZW2R2SQqpV71XkKK8BkUd3QRZfXe5Xrg9nH3m+MS5hkLJNdwcR7cCMWu/Nza Wmmg== X-Forwarded-Encrypted: i=1; AHgh+Rq6RNtWwiT2c5FWY2nUDWisQmB2Jm+G4KBLgDorvjJLUN34pepQRrr7JuWDP12VAdKlMddvRHJ9V9Hk5kk=@vger.kernel.org X-Gm-Message-State: AOJu0Yw54VuC//4IauecGUVgk95qHIZYVmDxxHLKCY2knLUqjVLvL7ga TxPb2Z8zK73P7b35iBaBPuB05AgbuEg1ljmJH38ohsyGjWQduF/Bu6i1 X-Gm-Gg: AR+sD11Q3frhV6mjGNOp1VN04OtOxWLBK1valRhk1jcsmLamHtKxW4I4xGghV0EJrtf bL4pGVC0xFVk0jcBvxafhpesZ3FGPReegliXua72uG8oY6NJUFvJgvIQSdDs3RMCbl7RYne0/Ti MoAVTExNbeCKdRUVYvZYELL63xy+OchsiRgx1lCVu2SaA1RngeIft2UurAbPxFPzXfNPyHfbKch i/GvKbUOKUy9ySG10yPvtlIAlHGvsk02Zl4QXN6r1OIFerd6edjzGIfvdcYdOIDY1pKcNcg5z1q /Ivq9Fe+ESMYM4dr3ZQtLr7UxsRfLt13QtQAedJrLDrev8jBkwdE/y1PNB1Sd0i0TY6kgTpHzpo 92gan5Hh71yUmm39ye+4KATy0UD1udgyHcWTP/DtqKlTnRYXcpDcqOd07vvwOeRNJWlA1V3Hvzw eRbdhWgY3XtZrHnGx5zfy6Y4j4/bwjEwHx45bZGe2g72JW X-Received: by 2002:a05:600c:358a:b0:496:c06b:9fb4 with SMTP id 5b1f17b1804b1-4999fb940c5mr20370285e9.14.1786983452156; Mon, 17 Aug 2026 09:17:32 -0700 (PDT) Received: from c.. ([213.165.253.80]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49996188217sm355538945e9.13.2026.08.17.09.17.31 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 17 Aug 2026 09:17:31 -0700 (PDT) From: Narek Jilavyan To: Alexander Viro , Christian Brauner Cc: Jan Kara , Mateusz Guzik , linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org, Narek Jilavyan Subject: [PATCH v2] fs: do not cache a symlink length that disagrees with the string Date: Mon, 17 Aug 2026 16:17:30 +0000 Message-ID: <20260817161730.699293-1-njilav@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260817081756.4176757-1-njilav@gmail.com> References: <20260817081756.4176757-1-njilav@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" inode_set_cached_link() stores a caller-supplied length in i_linklen and sets IOP_CACHED_LINK. vfs_readlink() then uses that length directly: if (inode->i_opflags & IOP_CACHED_LINK) return readlink_copy(buffer, buflen, inode->i_link, inode->i_linklen); and readlink_copy() clamps only against the user buffer, not against the string, so a length larger than the symlink body becomes a copy_to_user() of adjacent kernel memory - reachable by any process calling readlink() on such a symlink. The only thing standing behind the invariant is VFS_WARN_ON_INODE(strlen(link) !=3D linklen, inode); which expands to BUILD_BUG_ON_INVALID() unless CONFIG_DEBUG_VFS is set. On a production kernel it type-checks the expression and evaluates nothing, so the value is stored unvalidated. All four in-tree callers are correct today. This makes the helper enforce its own contract rather than leaving it to the next caller. Validate unconditionally and fail safe: if the length disagrees, warn with the disparity and leave IOP_CACHED_LINK clear. i_linklen has exactly one reader in the tree and it is gated on that flag, and vfs_readlink() falls back to i_link with a strlen() of its own, so the inode degrades to the behaviour that predates the cached length rather than disclosing memory. That fallback state is not exotic: 19 other filesystems set i_link directly and never set IOP_CACHED_LINK, so it is exercised routinely. The check runs once per symlink inode setup, not once per readlink(), which is what the cache was for. Tested on 7.2 with CONFIG_DEBUG_VFS=3Dn by handing a 3-byte allocation holding "AB" to inode_set_cached_link() with a declared length of 64: before: readlink() returns 64, copying 62 bytes of adjacent kernel heap to userspace after: bad length passed for symlink [AB] (got 64, expected 2) readlink() returns 2 Fixes: ea3821990719 ("vfs: support caching symlink lengths in inodes") Suggested-by: Mateusz Guzik Signed-off-by: Narek Jilavyan --- v2: - warn with the actual length disparity instead of a bare WARN_ON_ONCE, as suggested by Mateusz Guzik. - the filesystem name is not included. struct file_system_type is not complete where inode_set_cached_link() is defined (the helper is at include/linux/fs.h:947, the struct at :2280), and dump_inode() is declared and defined inside #ifdef CONFIG_DEBUG_VFS so it does not exist on the kernels this patch is about. Both established by compiler error rather than assumption. If the fs name is wanted I can move the warn out of line into fs/inode.c, though that needs an EXPORT_SYMBOL since ext4 and erofs can be built as modules. - still refrains from caching on a mismatch rather than fixing the length up, so a buggy caller is reported rather than silently corrected. include/linux/fs.h | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/include/linux/fs.h b/include/linux/fs.h index 50ce731a2b..c7051cfc6b 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -946,9 +946,25 @@ static inline void inode_state_replace(struct inode *i= node, =20 static inline void inode_set_cached_link(struct inode *inode, char *link, = int linklen) { - VFS_WARN_ON_INODE(strlen(link) !=3D linklen, inode); + int testlen; + VFS_WARN_ON_INODE(inode->i_opflags & IOP_CACHED_LINK, inode); inode->i_link =3D link; + + /* + * i_linklen is used as a copy_to_user() length by vfs_readlink(), so it + * must not be taken on trust. If it disagrees with the string, leave + * IOP_CACHED_LINK clear: vfs_readlink() then falls back to i_link and + * recomputes the length with strlen(), which is what it did before the + * cached length was introduced. + */ + testlen =3D strlen(link); + if (testlen !=3D linklen) { + WARN_ONCE(1, "bad length passed for symlink [%s] (got %d, expected %d)", + link, linklen, testlen); + return; + } + inode->i_linklen =3D linklen; inode->i_opflags |=3D IOP_CACHED_LINK; } --=20 2.43.0