git: 6cf0d6c3b577 - main - fsck_msdosfs: fix status accounting for lost cluster chains

From: Xin LI <delphij_at_FreeBSD.org>
Date: Thu, 03 Sep 2026 02:16:49 UTC
The branch main has been updated by delphij:

URL: https://cgit.FreeBSD.org/src/commit/?id=6cf0d6c3b5777e053074200ff99dc4b750e62b96

commit 6cf0d6c3b5777e053074200ff99dc4b750e62b96
Author:     Jie Li <sheng9qing@gmail.com>
AuthorDate: 2026-09-02 06:15:05 +0000
Commit:     Xin LI <delphij@FreeBSD.org>
CommitDate: 2026-09-03 02:16:10 +0000

    fsck_msdosfs: fix status accounting for lost cluster chains
    
    checklost() scans for lost cluster chains and attempts to
    repair each one, first by reconnecting it to LOST.DIR,
    and falling back to clearing it if reconnection fails.
    However, checklost() incorrectly updates the modification
    status flags (mod), which checkfilesys() relies on to
    determine whether to write back changes and what exit
    status to return.
    
    The current code have three issues:
    
    1. A reconnect() failure immediately sets FSERROR in mod
       via "mod |= ret = reconnect(...)".  If reconnect() failed
       (e.g., because LOST.DIR is missing, or full) but the
       fallback clear operation succeeds, clearchain() frees the
       chain and sets FSFATMOD.  However, the leftover FSERROR
       remains in mod: checkfilesys() skips marking the file system
       clean and exits with status 8, even though the file system was
       fully repaired and a subsequent run finds nothing left to do.
       This can happen with "fsck_msdosfs -y" on a volume without
       LOST.DIR.
    
    2. The same assignment overwrites checkchain()'s return value
       before it can be recorded in mod.  When checkchain()
       truncates a chain (e.g., one whose tail points to a free
       cluster), its FSFATMOD status is lost, causing checkfilesys()
       to skip updating the FAT and discard the truncation.
       With LOST.DIR present so that reconnect() succeeds,
       "fsck_msdosfs -y" reports "Truncate? yes" and "FILE SYSTEM WAS
       MODIFIED", exits 0, but leaves the identical damage on disk
       to be found again on every subsequent run.  Similarly, an
       FSFATAL return value from checkchain() is dropped, defeating
       the "if (mod & FSFATAL) break" guard that follows.
    
    3. When checkchain() returns FSERROR and clearing the chain is
       declined, no error status is recorded in mod, causing fsck_msdosfs
       to report a clean exit despite leaving un-repaired damage.
    
    Fix these issues by:
    
     - Merge checkchain()'s status into mod before calling reconnect(),
       and skip reconnect() if checkchain() returned a fatal error.
     - Defer recording FSERROR from a failed reconnect() until after the
       Clear fallback attempt, setting FSERROR only if the chain remains
       unhandled.
    
    MFC after:      1 week
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2251
---
 sbin/fsck_msdosfs/fat.c | 41 ++++++++++++++++++++++++++++++++---------
 1 file changed, 32 insertions(+), 9 deletions(-)

diff --git a/sbin/fsck_msdosfs/fat.c b/sbin/fsck_msdosfs/fat.c
index 5ead0b1bf783..d71ac2b503f5 100644
--- a/sbin/fsck_msdosfs/fat.c
+++ b/sbin/fsck_msdosfs/fat.c
@@ -1284,18 +1284,41 @@ checklost(struct fat_descriptor *fat)
 		}
 		if (fat_is_cl_head(fat, head)) {
 			ret = checkchain(fat, head, &chainlength);
-			if (ret != FSERROR && chainlength > 0) {
-				pwarn("Lost cluster chain at cluster %u\n"
-				    "%zd Cluster(s) lost\n",
-				    head, chainlength);
-				mod |= ret = reconnect(fat, head,
-				    chainlength);
+			/*
+			 * Record whether checkchain() has repaired the
+			 * chain (FSFATMOD) or died trying (FSFATAL) before
+			 * reconnect() overwrites ret, otherwise the repair
+			 * is never written back.
+			 */
+			if (ret != FSERROR) {
+				mod |= ret;
+				if (!(mod & FSFATAL) && chainlength > 0) {
+					pwarn(
+					    "Lost cluster chain at cluster %u\n"
+					    "%zd Cluster(s) lost\n",
+					    head, chainlength);
+					ret = reconnect(fat, head, chainlength);
+					if (ret != FSERROR)
+						mod |= ret;
+				}
 			}
 			if (mod & FSFATAL)
 				break;
-			if (ret == FSERROR && ask(0, "Clear")) {
-				clearchain(fat, head);
-				mod |= FSFATMOD;
+			/*
+			 * If reconnect() failed (or checkchain() truncation
+			 * was declined), defer folding FSERROR into mod until
+			 * the Clear fallback is known: if the lost chain is
+			 * cleared, the error has been resolved and must not
+			 * report an exit code 8; otherwise record FSERROR as
+			 * an unrecovered error.
+			 */
+			if (ret == FSERROR) {
+				if (ask(0, "Clear")) {
+					clearchain(fat, head);
+					mod |= FSFATMOD;
+				} else {
+					mod |= FSERROR;
+				}
 			}
 			chains--;
 		}