From nobody Fri Jun 12 12:45:07 2026 Received: from mail-wm1-f45.google.com (mail-wm1-f45.google.com [209.85.128.45]) (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 9A9083ACEE6 for ; Thu, 14 May 2026 20:34:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.128.45 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778790884; cv=none; b=teByADRgrKOuRICDH55Sa59CAJHUSb5z2Rggom8p/Qo+6E0aWmBHLra0MQeCfZ7lps4rhEIqDxZd0bfxWWlt978mg1pohqjk73Qi/rwq3H7OEV4vGVf8aAMquFpLZZ5GDU1aYAm6wP7hO/aY/MMwryq8n6ZSWx4MHEsfUnvI0Ow= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778790884; c=relaxed/simple; bh=m7QJc4bQxofcs+Cu3bMJnwPsgVk+kdl1UkCRrZUD+mQ=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=YwsKyNbsgugXITnSuhkCu/T10WIzezj4JiH36V5E8Y7OV5LEdrJ2N6rFqXfxSCOAfh67mTY6xFmGbSyL9te142IogPIdrLnneAXPx+uyfnxE+BOcOXUELnj5CF+pLJII8DreXn3ZKChpipZLKRBmfri3Te7NmTkC2uv+kZOPkcc= 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=QBLUCW/s; arc=none smtp.client-ip=209.85.128.45 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="QBLUCW/s" Received: by mail-wm1-f45.google.com with SMTP id 5b1f17b1804b1-4891b0786beso54557665e9.1 for ; Thu, 14 May 2026 13:34:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778790880; x=1779395680; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=OVjGcDYWvFPm8krx+qeokcjERyyTX5R5vL7GgQ6JTJM=; b=QBLUCW/sTmDcjWffKvucIB4AO4Gi510if5dnElYpzHWFQ4Q4159OpZzQpi0LU0bakM BB2wLBoryfthBg/AwWZAKAgq4RLM7EtiCfBwHe/ln47eQxABfGZGFpCnbSghvsflJrsK 3EBiscuf+xmMDolYPiCbRnIS8oE+z36CF6o/WCexQF/K2ml2BsLB7PvKUXXCVgg+Y18O tNjxQe40517r38NaIfZwHtnLMV52t+Jn1kalrVHfuZzgKcDNsXndCNUAbEsunwY/ffYp 9bVplaFNHoWfd1HST2twUbxeHF8xHG5ca8VH6Fp9mZA5TBmmNWLWkJsOIFzFlacWN1Ry U1AQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778790880; x=1779395680; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=OVjGcDYWvFPm8krx+qeokcjERyyTX5R5vL7GgQ6JTJM=; b=jqZhIIHJ054Hepfar8w/ML2uq64/Ulnaekd1395zcFv6owefXAADqmsqEkayqeoqZM uUiaGDKUKEkk0MoKU9pa4M85V+hHlRwSiGFJxiauJJLWAfAkZr1f9mf+AzV82vWbe1++ mVDMwqjJeqQB/KaIhhaColY1DLBQdY4RLMbtXN6ZhFGI52S2K3LkkO8SCuIgzqC0G6Cb d7BqRLbheFo1aNngiyPwhYokfMRETMbti4Aju9K9LUPpAJNLgqHA2bJZPb8gLzU8SgRe sU0q8PPqS2b8vOpesS5JGxZYHdki5OyEduxXbdp8Vd3nc9BJ+OR3HWNHbRXaLht6e+NA fBSg== X-Forwarded-Encrypted: i=1; AFNElJ/w/2zmGjnKcFpql0oE0V5XDk6PzlunvynElvwko2BhR70GOVuiIGZ+7deGr4QZuyGt6T6YtCeCO8GLzjM=@vger.kernel.org X-Gm-Message-State: AOJu0YwioZmTmuZOtsemSkE+ulbD4tW8aCu/IR+7tT6HHuQauxlrdh8L LNHaQP1hjExOQ0FGzmeedI2oz5O1M4Waa8a1LIZEu/F5gvnn06p60D06LlcV0Tcu X-Gm-Gg: Acq92OETKMZHbb8FWm/2ZcRffIDghKHR5u9ayKHIk7TrxrYbzP9+k20JuM0X4c+kr6Z Din9u7b0iGzvbfyvLT11nr2oPncTmsGb7sOhaaxzZSOb0avrUOur8Al+V3wP4G/CTaWqhYUX9a5 dF7K/JczkUO1KBDMu5wQt3WXrNHbLfLnv5WeN0TPV1S8Yh7tKXS/j2P3nQOsgRq6oFc7A4FkBBI WS9FV6Ya/AXPOwtwE969PUjlZXapNFOZToYrIhHG2TqljcL6KTK/DSkF4CMNxZ25pvV1QmQVh5s /9QJyMVkOICDCZxZhMrxiX7Z2njOAL0VzSbqB2bY9TpSXf2Alw67w37cw2j1gMqi1iu6rHxp2Ms 2/CiUx8dOELDv/OZLTiyCo3yOHbkdOUv540UVYS1saeg0+5ZuyId27IXfSMkOBnN6YmTasfS9o/ 9Os+q+a3RmgIRMmlngPSZvroiwsXykgu+eevoMblWm1d1YcpY= X-Received: by 2002:a05:600c:3f1b:b0:48a:7965:b943 with SMTP id 5b1f17b1804b1-48fe661db6dmr11736865e9.29.1778790879745; Thu, 14 May 2026 13:34:39 -0700 (PDT) Received: from turbo.teknoraver.net ([195.182.211.216]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-48fd7483926sm24633875e9.16.2026.05.14.13.34.38 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 14 May 2026 13:34:39 -0700 (PDT) From: Matteo Croce X-Google-Original-From: Matteo Croce To: OGAWA Hirofumi Cc: Timothy Redaelli , linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH] fat: stop reading directory entries past the end-of-directory marker Date: Thu, 14 May 2026 22:34:36 +0200 Message-ID: <20260514203436.13789-1-teknoraver@meta.com> X-Mailer: git-send-email 2.54.0 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" The FAT specification[1] (FAT Directory Structure -> "DIR_Name[0]") states: If DIR_Name[0] =3D=3D 0x00, then the directory entry is free (same as f= or 0xE5), and there are no allocated directory entries after this one (all of the DIR_Name[0] bytes in all of the entries after this one are also set to 0). The special 0 value, rather than the 0xE5 value, indicates to FAT file system driver code that the rest of the entries in this directory do not need to be examined because they are all free. Linux doesn't honour this: if the trailing on-disk slots is not zero-filled (buggy formatters, on-disk corruption, etc.) fat_get_entry() keeps on advancing past the 0x00 terminator and the driver parses arbitrary bytes as real directory entries. On a typical affected image, `ls /mnt` returns ~150 bogus entries with random binary names, multi-gigabyte sizes, dates ranging from 1980 to 2106, and a flood of -EIO from stat(). Earlier attempts (was "vfat: don't read garbage after last dirent"[2][3][4]) added `de->name[0] =3D=3D 0` guards at each call site. As Hirofumi pointed out on v3, those guards reject the entry but fat_get_entry() has already advanced *pos past it. The next readdir() resumes after the marker and walks straight back into the garbage. This patch: Adds fat_get_entry_eod(), a small wrapper around fat_get_entry() that returns -1 when name[0] =3D=3D 0 AND rewinds *pos to the marker. converts the read/search paths to use the new wrapper. Leaves fat_add_entries() and __fat_remove_entries() on raw fat_get_entry(): the write paths legitimately need to operate on free/zero slots. Additionally, log a rate-limited warning suggesting fsck if it sees an allocated entry after a 0x00 marker. [1] https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-9= 23143f3456c/fatgen103.doc [2] https://lore.kernel.org/lkml/20181207013410.7050-1-mcroce@redhat.com/ [3] https://lore.kernel.org/lkml/20181216231510.26854-1-mcroce@redhat.com/ [4] https://lore.kernel.org/lkml/20190201001408.7453-1-mcroce@redhat.com/ Reported-by: Timothy Redaelli Signed-off-by: Matteo Croce --- fs/fat/dir.c | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/fs/fat/dir.c b/fs/fat/dir.c index 4f6f42f33613..109330da77b1 100644 --- a/fs/fat/dir.c +++ b/fs/fat/dir.c @@ -130,6 +130,26 @@ static inline int fat_get_entry(struct inode *dir, lof= f_t *pos, return fat__get_entry(dir, pos, bh, de); } =20 +/* + * Like fat_get_entry(), but honour the FAT end-of-directory marker: + * a dirent whose first name byte is NUL terminates iteration per the + * spec. *pos is rewound to the EOD slot so callers that persist *pos + * across invocations (e.g. readdir's ctx->pos) keep reporting EOD. + */ +static int fat_get_entry_eod(struct inode *dir, loff_t *pos, + struct buffer_head **bh, + struct msdos_dir_entry **de) +{ + loff_t saved =3D *pos; + int err =3D fat_get_entry(dir, pos, bh, de); + + if (err =3D=3D 0 && (*de)->name[0] =3D=3D 0) { + *pos =3D saved; + return -1; + } + return err; +} + /* * Convert Unicode 16 to UTF-8, translated Unicode, or ASCII. * If uni_xlate is enabled and we can't get a 1:1 conversion, use a @@ -327,7 +347,7 @@ static int fat_parse_long(struct inode *dir, loff_t *po= s, =20 if (ds->id & 0x40) (*unicode)[offset + 13] =3D 0; - if (fat_get_entry(dir, pos, bh, de) < 0) + if (fat_get_entry_eod(dir, pos, bh, de) < 0) return PARSE_EOF; if (slot =3D=3D 0) break; @@ -489,7 +509,7 @@ int fat_search_long(struct inode *inode, const unsigned= char *name, =20 err =3D -ENOENT; while (1) { - if (fat_get_entry(inode, &cpos, &bh, &de) =3D=3D -1) + if (fat_get_entry_eod(inode, &cpos, &bh, &de) =3D=3D -1) goto end_of_dir; parse_record: nr_slots =3D 0; @@ -601,7 +621,7 @@ static int __fat_readdir(struct inode *inode, struct fi= le *file, =20 bh =3D NULL; get_new: - if (fat_get_entry(inode, &cpos, &bh, &de) =3D=3D -1) + if (fat_get_entry_eod(inode, &cpos, &bh, &de) =3D=3D -1) goto end_of_dir; parse_record: nr_slots =3D 0; @@ -885,7 +905,7 @@ static int fat_get_short_entry(struct inode *dir, loff_= t *pos, struct buffer_head **bh, struct msdos_dir_entry **de) { - while (fat_get_entry(dir, pos, bh, de) >=3D 0) { + while (fat_get_entry_eod(dir, pos, bh, de) >=3D 0) { /* free entry or long name entry or volume label */ if (!IS_FREE((*de)->name) && !((*de)->attr & ATTR_VOLUME)) return 0; @@ -1302,6 +1322,7 @@ int fat_add_entries(struct inode *dir, void *slots, i= nt nr_slots, struct msdos_dir_entry *de; int err, free_slots, i, nr_bhs; loff_t pos; + bool saw_eod; =20 sinfo->nr_slots =3D nr_slots; =20 @@ -1310,12 +1331,15 @@ int fat_add_entries(struct inode *dir, void *slots,= int nr_slots, bh =3D prev =3D NULL; pos =3D 0; err =3D -ENOSPC; + saw_eod =3D false; while (fat_get_entry(dir, &pos, &bh, &de) > -1) { /* check the maximum size of directory */ if (pos >=3D FAT_MAX_DIR_SIZE) goto error; =20 if (IS_FREE(de->name)) { + if (de->name[0] =3D=3D 0) + saw_eod =3D true; if (prev !=3D bh) { get_bh(bh); bhs[nr_bhs] =3D prev =3D bh; @@ -1325,6 +1349,12 @@ int fat_add_entries(struct inode *dir, void *slots, = int nr_slots, if (free_slots =3D=3D nr_slots) goto found; } else { + if (saw_eod) { + fat_msg_ratelimit(sb, KERN_WARNING, + "allocated dir entry found after end-of-directory marker (i_pos %lld)= ; please run fsck", + MSDOS_I(dir)->i_pos); + saw_eod =3D false; + } for (i =3D 0; i < nr_bhs; i++) brelse(bhs[i]); prev =3D NULL; --=20 2.54.0