From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EDFFD37E5ED; Mon, 14 Sep 2026 05:49:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364983; cv=none; b=QqJ5UOOqGRGF1eotUcgMAaTp9g6GoVysKEx/thFsjKQfluMlR0zWob/4l1apVp0Pe9LXlYwoJf6Z4a4uCvaMHcfRxDDqPj2iexs0mnvimR9sL/EjftEzD7kfybodZtyQ2VHnWvuOCnN70jEi082WUOxSDB3igvH47HRuWwc3qwY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364983; c=relaxed/simple; bh=1eLOnwow/djpmHX7VdGdO1rAK4zqVtvR3BsezklmFPM=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=TufrnFmi2YMWF7iJaX1K5aRmqy6ttK+D/sAuoFmG9QRNFzROFbl4L1x492r3CO7qjiIPUOLemlmE7T3XnIFIw6wQSdSJqeUW96azzG+C66SEw3yCGvgvDj3W/Q0/3tL5cpSNu5EZqAtlYn5eU3Hwdi7o958B6wi8X+p+Z78Qoio= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 0e973cdeb00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:9d8a02c0-9260-4933-827c-b4698088e522,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:071d2da1b0cfd34bcf5343e86be42af5,BulkI D:nil,BulkQuantity:0,SF:81|82|102|136|850|865|898,TC:nil,Content:0|15|50|9 9,EDM:-3|-100,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,O SI:0,OSA:0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 0e973cdeb00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 561642727; Mon, 14 Sep 2026 13:49:31 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , stable@vger.kernel.org Subject: [PATCH v11 1/6] ntfs: fix volume flag update races Date: Mon, 14 Sep 2026 13:49:21 +0800 Message-Id: <20260914054926.78808-2-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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" ntfs_set_volume_flags() and ntfs_clear_volume_flags() both read vol->vol_flags outside any lock to compute the new value before handing it to ntfs_write_volume_flags(), which only takes ni->mrec_lock around the actual write. The read-modify-write is therefore not atomic, and two concurrent callers can lose an update: ntfs_sync_fs() may derive a "clean" value from vol->vol_flags while a writer concurrently records an error and sets VOLUME_IS_DIRTY; the locked write then silently overwrites the freshly-set dirty bit. The on-disk volume looks clean despite the recorded errors, so chkdsk will not run on the next mount and corrupted metadata can persist. Fix by moving the read-modify-write inside the mrec_lock: pass the bits to set and to clear separately, and combine them with the current flag state under the lock inside ntfs_write_volume_flags(). The set/clear helpers pass only the bits to modify, not the complete flag state. The bit manipulation is done on CPU-endian values, and the result is converted back to little-endian before storing it. The wrappers keep their signatures so callers are unchanged. Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/super.c | 63 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index f4a73e45773d..6ba19986a598 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -353,31 +353,45 @@ void ntfs_handle_error(struct super_block *sb) } =20 /* - * ntfs_write_volume_flags - write new flags to the volume information fla= gs + * ntfs_write_volume_flags - apply flag changes to the volume information = flags * @vol: ntfs volume on which to modify the flags - * @flags: new flags value for the volume information flags + * @set_bits: bits to set in the volume information flags + * @clear_bits: bits to clear in the volume information flags * * Internal function. You probably want to use ntfs_{set,clear}_volume_fl= ags() * instead (see below). * - * Replace the volume information flags on the volume @vol with the value - * supplied in @flags. Note, this overwrites the volume information flags= , so - * make sure to combine the flags you want to modify with the old flags an= d use - * the result when calling ntfs_write_volume_flags(). + * Combine @set_bits and @clear_bits with the current in-memory flag state= and + * write the result back. The set/clear helpers pass only the bits to mod= ify, + * not the complete flag state. The read-modify-write happens under + * ni->mrec_lock so that concurrent set/clear operations cannot lose updat= es. + * All bit manipulation is done on CPU-endian values, and the result is + * converted back to little-endian before storing it. * * Return 0 on success and -errno on error. */ -static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 f= lags) +static int ntfs_write_volume_flags(struct ntfs_volume *vol, + const __le16 set_bits, const __le16 clear_bits, + const bool skip_if_errors) { struct ntfs_inode *ni =3D NTFS_I(vol->vol_ino); struct volume_information *vi; struct ntfs_attr_search_ctx *ctx; + u16 flags; int err; =20 - ntfs_debug("Entering, old flags =3D 0x%x, new flags =3D 0x%x.", - le16_to_cpu(vol->vol_flags), le16_to_cpu(flags)); mutex_lock(&ni->mrec_lock); - if (vol->vol_flags =3D=3D flags) + + if (skip_if_errors && NVolErrors(vol)) + goto done; + + flags =3D le16_to_cpu(vol->vol_flags); + flags |=3D le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK); + flags &=3D ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK)); + ntfs_debug("Entering, old flags =3D 0x%x, new flags =3D 0x%x.", + le16_to_cpu(vol->vol_flags), flags); + + if (le16_to_cpu(vol->vol_flags) =3D=3D flags) goto done; =20 ctx =3D ntfs_attr_get_search_ctx(ni, NULL); @@ -393,7 +407,7 @@ static int ntfs_write_volume_flags(struct ntfs_volume *= vol, const __le16 flags) =20 vi =3D (struct volume_information *)((u8 *)ctx->attr + le16_to_cpu(ctx->attr->data.resident.value_offset)); - vol->vol_flags =3D vi->flags =3D flags; + vol->vol_flags =3D vi->flags =3D cpu_to_le16(flags); mark_mft_record_dirty(ctx->ntfs_ino); ntfs_attr_put_search_ctx(ctx); done: @@ -414,13 +428,14 @@ static int ntfs_write_volume_flags(struct ntfs_volume= *vol, const __le16 flags) * @flags: flags to set on the volume * * Set the bits in @flags in the volume information flags on the volume @v= ol. + * The bits are combined with the current flag state under the lock in + * ntfs_write_volume_flags(), so concurrent updates are not lost. * * Return 0 on success and -errno on error. */ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags) { - flags &=3D VOLUME_FLAGS_MASK; - return ntfs_write_volume_flags(vol, vol->vol_flags | flags); + return ntfs_write_volume_flags(vol, flags, 0, false); } =20 /* @@ -429,14 +444,27 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __= le16 flags) * @flags: flags to clear on the volume * * Clear the bits in @flags in the volume information flags on the volume = @vol. + * The bits are combined with the current flag state under the lock in + * ntfs_write_volume_flags(), so concurrent updates are not lost. * * Return 0 on success and -errno on error. */ int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags) { - flags &=3D VOLUME_FLAGS_MASK; - flags =3D vol->vol_flags & cpu_to_le16(~le16_to_cpu(flags)); - return ntfs_write_volume_flags(vol, flags); + return ntfs_write_volume_flags(vol, 0, flags, false); +} + +/* + * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exi= st + * @vol: ntfs volume whose dirty bit should be cleared + * + * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so + * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has = been + * recorded. + */ +static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol) +{ + return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true); } =20 int ntfs_write_volume_label(struct ntfs_volume *vol, char *label) @@ -1858,8 +1886,7 @@ static int ntfs_sync_fs(struct super_block *sb, int w= ait) return 0; =20 /* If there are some dirty buffers in the bdev inode */ - if (!NVolErrors(vol) && - ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) { + if (ntfs_clear_volume_dirty_if_no_errors(vol)) { ntfs_warning(sb, "Failed to clear dirty bit in volume information flags.= Run chkdsk."); err =3D -EIO; } --=20 2.25.1 From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D917737F309; Mon, 14 Sep 2026 05:49:39 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364982; cv=none; b=OoVAmzjrPhv8EcXi4SDJjrMZwaoMYwuwpg7x+VbDx3dy1YbJG058/VmAPV091JqwlXV0Q7fv22XZec9/GWq1jULLS6Ey4/hRPB5e3rZV0dfKzXSfNL6kq9dat38vWGAK34hTiIoni1Mu7xvRqmtA6wza1ebfvkMu6LXIQRDNoZs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364982; c=relaxed/simple; bh=A09OHkw0psSLkx6FKOss/BUH+O/19kPKvUcWwko7h00=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=o2T8ASVO/fI5aWpjnshBVPxWL3UGMi6bGtO2tuvgKHhVu7K5cdFAuXbH8aTX5LhTx6dwaNifInCmQGs1BdAy2ajbvBkPu6JOUBHdBXrvDuEdfWO2lV2LlR5tpyD+p5YfehJ0PiOVn/f2I2nPw7tb9hyhnzK9iWBBA1McGyp7HaU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 0f104ac0b00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:7d5fdb5a-0fee-4fba-bdee-2ece3cbab930,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:34ea739899712a946a605e90b900336f,BulkI D:nil,BulkQuantity:0,SF:81|82|102|850|865|898,TC:nil,Content:0|15|50|99,ED M:-3,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,OSI:0,OSA: 0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 0f104ac0b00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 15398452; Mon, 14 Sep 2026 13:49:32 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , Baolin Liu , stable@vger.kernel.org Subject: [PATCH v11 2/6] ntfs: set the volume dirty bit unconditionally on metadata changes Date: Mon, 14 Sep 2026 13:49:22 +0800 Message-Id: <20260914054926.78808-3-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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 callers in file.c and namei.c skip ntfs_set_volume_flags() when the in-memory vol_flags already show VOLUME_IS_DIRTY, but that check runs without any lock: if it observes the bit set and ntfs_sync_fs() clears it under the mrec_lock before the caller's metadata update completes, the set is skipped and the volume can end up clean on disk despite the modification, so chkdsk will not run on the next mount. Drop the caller-side checks and call ntfs_set_volume_flags() unconditionally: ntfs_write_volume_flags() already skips the write under the mrec_lock when the combined value is unchanged. That unconditional call costs one mrec_lock acquisition per metadata operation even in the already-dirty steady state; it cannot be avoided, because deciding to skip the call without the lock is itself what allows a concurrent ntfs_sync_fs() clear to lose the set. The IOCB_NOWAIT path in ntfs_file_write_iter() goes through the same sleeping call: a RWF_NOWAIT write can block in the marking, as it already could before this change whenever the volume appeared clean. Giving that path a non-blocking variant is left as follow-up work. The callers keep the pre-existing behavior of proceeding when the marking fails, so the dirty bit remains best-effort. This closes the variant where the set is skipped outright. A clear for a concurrent, error-free sync can still land between the set and the end of the metadata operation; that mark-at-start lifecycle is pre-existing and is not changed by this patch. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/file.c | 20 +++++++++++--------- fs/ntfs/namei.c | 24 ++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index 007d1614b9ac..cfc7b36b7dff 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -325,8 +325,7 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry= *dentry, goto out; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 if (ia_valid & ATTR_SIZE) { err =3D ntfs_setattr_size(vi, attr); @@ -620,8 +619,13 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb= , struct iov_iter *from) goto out_lock; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + /* + * The volume must be marked dirty before the modification is made, + * without an unlocked check of the in-memory flag: ntfs_sync_fs() + * can clear the bit concurrently and the modification would then + * land on a volume that is clean on disk. + */ + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 pos =3D iocb->ki_pos; count =3D ret; @@ -1153,11 +1157,9 @@ static long ntfs_fallocate(struct file *file, int mo= de, loff_t offset, loff_t le return err; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) { - err =3D ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); - if (err) - return err; - } + err =3D ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + if (err) + return err; =20 old_size =3D i_size_read(vi); =20 diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index fdf52fac4329..3e0adb9a0ea4 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -757,8 +757,7 @@ static int ntfs_create(struct mnt_idmap *idmap, struct = inode *dir, return err; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 ni =3D __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NUL= L, 0); kmem_cache_free(ntfs_name_cache, uname); @@ -1032,8 +1031,7 @@ static int ntfs_unlink(struct inode *dir, struct dent= ry *dentry) return err; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 err =3D ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true); if (err) @@ -1076,8 +1074,7 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *id= map, struct inode *dir, return ERR_PTR(err); } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 ni =3D __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0); kmem_cache_free(ntfs_name_cache, uname); @@ -1118,8 +1115,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentr= y *dentry) return err; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 err =3D ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true); if (err) @@ -1305,8 +1301,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struc= t inode *old_dir, new_dir_first =3D is_subdir(new_dentry->d_parent, old_dentry->d_parent); =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 mutex_lock_nested(&old_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); if (new_ni) @@ -1429,8 +1424,7 @@ static int ntfs_symlink(struct mnt_idmap *idmap, stru= ct inode *dir, goto out; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 ni =3D __ntfs_create(idmap, dir, usrc, usrc_len, S_IFLNK | 0777, 0, symname, symlen); @@ -1474,8 +1468,7 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct= inode *dir, return err; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 switch (mode & S_IFMT) { case S_IFCHR: @@ -1521,8 +1514,7 @@ static int ntfs_link(struct dentry *old_dentry, struc= t inode *dir, return -ENOMEM; } =20 - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 ihold(vi); mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL); --=20 2.25.1 From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4DEF937E5F2; Mon, 14 Sep 2026 05:49:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364987; cv=none; b=Y7RYWZ2++4fmSJmOHcLYvOeq1KZUvKV3eqLYwgKJdXqjAUX9c4k3qT0YYVu0/5pr54Y9TOrhznL27zTxn4QIyqxHB7ZQXGKGuHSV//pevuTb/YhJvuHuzj7scoqW7aROS6YhXl6VMMDy2wethQN35Vq2djse9WCiO2cBu/mxfzM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364987; c=relaxed/simple; bh=93vdgpY6omFfcHnYSO/4Uk2q3F1I6AOR19ix+OVOUek=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=pM6uI8+lBUOUOXqwN8KgWGPh0WXc22hI+ZRTP2PPHlNsAAbOGVZGuLKQU0x1QnxM3SyqA/lQE1o1PM0TmvgdJ2pgktgA+uVWHT25oOd2tbqMMrKLP2ioHMvolW1mESvoymFwUZpk17tu8iOrE04/iO2A7ElT0L/FrqxR2D6Dqps= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 0f98ff64b00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:412a94f7-e4b4-48f5-982d-671b0aff344b,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:4929fb4a0f0e6190f0beb8a05205b431,BulkI D:nil,BulkQuantity:0,SF:81|82|102|136|850|865|898,TC:nil,Content:0|15|50|9 9,EDM:-3|-100,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,O SI:0,OSA:0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 0f98ff64b00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 788765784; Mon, 14 Sep 2026 13:49:33 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , stable@vger.kernel.org Subject: [PATCH v11 3/6] ntfs: sync the volume dirty bit with the recorded error state Date: Mon, 14 Sep 2026 13:49:23 +0800 Message-Id: <20260914054926.78808-4-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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 runtime metadata-corruption paths in fs/ntfs only record the in-memory NVolErrors() flag; whether VOLUME_IS_DIRTY ever reaches disk depends on ntfs_set_volume_flags() being called by some other path, which for most error sites never happens. A volume can therefore unmount with a clean on-disk flag despite recorded corruption, and chkdsk will not run on the next mount. Persisting the dirty bit from the error paths themselves does not work: they run under a wide variety of ntfs locks, and the dirty-bit write takes the $Volume mrec_lock and maps the $Volume mft record, which on an $MFT page-cache miss takes the $MFT runlist lock for writing. That is enough to self-deadlock or form ABBA cycles from several of them: the $MFT extend undo paths hold the $MFT runlist lock and then take vol->lcnbmp_lock inside ntfs_cluster_free(); the cluster allocation and free rollback paths hold vol->lcnbmp_lock; and the whole mft record allocation tree is reachable from ntfs_write_volume_label()'s attribute-list maintenance while it holds the $Volume mrec_lock itself. Instead, make the persistence a property of the sync paths, which run without ntfs locks held. The new ntfs_sync_volume_dirty_state() sets VOLUME_IS_DIRTY when NVolErrors() is recorded and clears it otherwise, evaluating the error flag under the $Volume mrec_lock. It is called from ntfs_sync_fs(), from the remount-to-read-only path of ntfs_reconfigure(), and from ntfs_put_super(), which previously evaluated NVolErrors() outside the lock before clearing the dirty bit unconditionally, and which now also persists the dirty bit for volumes with recorded errors so they unmount with chkdsk scheduled. The ntfs_clear_volume_flags() wrapper, whose last callers this patch replaces, has no users left and is removed. The guarantee this provides is eventual, not instantaneous: the error paths record NVolErrors() with a lock-free set_bit(), so a persistence point that evaluates the flag just before an error is recorded can still leave the on-disk bit clean until the next one. This is sound because NVolErrors() is sticky for the lifetime of the mount and every persistence point re-derives the on-disk bit from it; the last one, ntfs_put_super(), runs after evict_inodes() on a quiesced filesystem, so a volume that is read-write at unmount time cannot unmount clean. A volume that is already read-only when the error is recorded (errors=3Dremount-ro flips the superblock on the first error, as does an earlier remount-ro) has no persistence point left and keeps whatever on-disk bit it had; that behaviour is unchanged. The residual window is a crash between the error and the next persistence point. The persistence paths never write a hibernated volume: resuming Windows from a modified image corrupts it. Record the mount-time hibernation verdict in the new NV_Hibernated volume flag and make ntfs_sync_volume_dirty_state() a no-op while it is set, so the dirty bit is left exactly as it is on disk and only the in-memory error state is kept. Without this, an rw mount of a hibernated volume with the default errors=3Dcontinue would gain a filesystem-internal write on the first sync, remount or unmount. Other writes to such a mount, like the mount-time logfile emptying, are pre-existing and unchanged. Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/ntfs.h | 1 - fs/ntfs/super.c | 129 ++++++++++++++++++++++++++++++++--------------- fs/ntfs/volume.h | 4 ++ 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 45f77848a9cf..a5cd5493c501 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -219,7 +219,6 @@ struct option_t { }; extern const struct option_t on_errors_arr[]; int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags); -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags); int ntfs_write_volume_label(struct ntfs_volume *vol, char *label); =20 /* From fs/ntfs/mst.c */ diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 6ba19986a598..733565953302 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -262,6 +262,8 @@ static int ntfs_parse_param(struct fs_context *fc, stru= ct fs_parameter *param) return 0; } =20 +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol); + static int ntfs_reconfigure(struct fs_context *fc) { struct super_block *sb =3D fc->root->d_sb; @@ -312,10 +314,24 @@ static int ntfs_reconfigure(struct fs_context *fc) } } else if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY)) { /* Remounting read-only. */ - if (!NVolErrors(vol)) { - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) - ntfs_warning(sb, - "Failed to clear dirty bit in volume information flags. Run chkdsk."= ); + /* + * With errors recorded the dirty bit is set rather than + * cleared, and it is committed right away: the VFS does + * not sync the filesystem during a remount, and once the + * remount succeeds no further persistence point exists - + * ntfs_sync_fs() is only ever invoked for read-write + * superblocks (all its VFS callers skip read-only ones) + * and ntfs_put_super() skips them, so the only remaining + * write would be the evict-time commit at unmount, which + * a crash never reaches. An error recorded only after + * the remount is still never persisted. + */ + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to update dirty bit in volume information flags. Run chkdsk."= ); + } else if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_commit_inode(vol->vol_ino); + blkdev_issue_flush(sb->s_bdev); } } =20 @@ -357,9 +373,10 @@ void ntfs_handle_error(struct super_block *sb) * @vol: ntfs volume on which to modify the flags * @set_bits: bits to set in the volume information flags * @clear_bits: bits to clear in the volume information flags + * @dirty_if_errors: force VOLUME_IS_DIRTY on when NVolErrors() is set * * Internal function. You probably want to use ntfs_{set,clear}_volume_fl= ags() - * instead (see below). + * or ntfs_sync_volume_dirty_state() instead (see below). * * Combine @set_bits and @clear_bits with the current in-memory flag state= and * write the result back. The set/clear helpers pass only the bits to mod= ify, @@ -368,11 +385,18 @@ void ntfs_handle_error(struct super_block *sb) * All bit manipulation is done on CPU-endian values, and the result is * converted back to little-endian before storing it. * + * When @dirty_if_errors is true and errors have been recorded on @vol, + * VOLUME_IS_DIRTY is forced on after the requested changes. NVolErrors()= is + * evaluated under the same mrec_lock, which orders this against other + * locked flag updates; the runtime error paths themselves record the flag + * lock-free, so see ntfs_sync_volume_dirty_state() for the guarantee this + * provides against them. + * * Return 0 on success and -errno on error. */ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 set_bits, const __le16 clear_bits, - const bool skip_if_errors) + const bool dirty_if_errors) { struct ntfs_inode *ni =3D NTFS_I(vol->vol_ino); struct volume_information *vi; @@ -382,12 +406,11 @@ static int ntfs_write_volume_flags(struct ntfs_volume= *vol, =20 mutex_lock(&ni->mrec_lock); =20 - if (skip_if_errors && NVolErrors(vol)) - goto done; - flags =3D le16_to_cpu(vol->vol_flags); flags |=3D le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK); flags &=3D ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK)); + if (dirty_if_errors && NVolErrors(vol)) + flags |=3D le16_to_cpu(VOLUME_IS_DIRTY); ntfs_debug("Entering, old flags =3D 0x%x, new flags =3D 0x%x.", le16_to_cpu(vol->vol_flags), flags); =20 @@ -439,31 +462,43 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __= le16 flags) } =20 /* - * ntfs_clear_volume_flags - clear bits in the volume information flags - * @vol: ntfs volume on which to modify the flags - * @flags: flags to clear on the volume + * ntfs_sync_volume_dirty_state - persist the dirty bit per the error state + * @vol: ntfs volume whose dirty bit to persist * - * Clear the bits in @flags in the volume information flags on the volume = @vol. - * The bits are combined with the current flag state under the lock in - * ntfs_write_volume_flags(), so concurrent updates are not lost. + * Set VOLUME_IS_DIRTY if errors have been recorded on @vol and clear it + * otherwise, under the $Volume mrec_lock. * - * Return 0 on success and -errno on error. - */ -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags) -{ - return ntfs_write_volume_flags(vol, 0, flags, false); -} - -/* - * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exi= st - * @vol: ntfs volume whose dirty bit should be cleared + * The guarantee this provides is eventual, not instantaneous: the runtime + * error paths record NVolErrors() with a lock-free set_bit(), so a + * persistence point that evaluates the flag just before an error is + * recorded can still leave the on-disk bit clean. This is sound because + * NVolErrors() is sticky (nothing clears it for the lifetime of the mount) + * and every persistence point re-derives the on-disk bit from it; the + * last one, ntfs_put_super(), runs after evict_inodes() on a quiesced + * filesystem, so a volume that is read-write at unmount time cannot + * unmount clean. A volume that is already read-only when the error is + * recorded (errors=3Dremount-ro flips the superblock on the first error, + * as does an earlier remount-ro) has no persistence point left and + * keeps whatever on-disk bit it had; that behaviour is unchanged. The + * residual window is a crash between the error and the next + * persistence point. + * + * This is the single point that persists the in-memory error state to dis= k. + * The runtime error paths only record NVolErrors() because they run under= a + * variety of ntfs locks the dirty-bit write cannot be taken under (runlist + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first + * ntfs_sync_fs(), a remount, or the unmount then persists the flag here. + * + * A hibernated volume is not written from these persistence paths: + * resuming Windows from a modified image corrupts it, so the dirty bit + * is left as it is on disk and only the in-memory error state is kept. * - * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so - * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has = been - * recorded. + * Return 0 on success and -errno on error. */ -static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol) +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol) { + if (NVolHibernated(vol)) + return 0; return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true); } =20 @@ -1615,6 +1650,11 @@ static bool load_system_files(struct ntfs_volume *vo= l) ntfs_error(sb, "%s. Mounting read-only%s", es1, es2); } NVolSetErrors(vol); + /* + * Remember it for the lifetime of the mount: see + * ntfs_sync_volume_dirty_state(). + */ + NVolSetHibernated(vol); } =20 /* If (still) a read-write mount, empty the logfile. */ @@ -1772,22 +1812,31 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); =20 /* - * If a read-write mount and no volume errors have occurred, mark the - * volume clean. Also, re-commit all affected inodes. + * If a read-write mount, persist the error state in the volume flags: + * mark the volume clean if no volume errors have occurred, and make + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the + * next mount. Also, re-commit all affected inodes. */ if (!sb_rdonly(sb)) { + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to sync dirty bit in volume information flags. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * The dirty bit is on disk now; only warn when the + * sync actually succeeded, or this message would + * contradict the one above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } + /* Commits the updated volume flags if they were written. */ + ntfs_commit_inode(vol->vol_ino); if (!NVolErrors(vol)) { - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) - ntfs_warning(sb, - "Failed to clear dirty bit in volume information flags. Run chkdsk."= ); - ntfs_commit_inode(vol->vol_ino); ntfs_commit_inode(vol->root_ino); if (vol->mftmirr_ino) ntfs_commit_inode(vol->mftmirr_ino); ntfs_commit_inode(vol->mft_ino); - } else { - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); } } =20 @@ -1886,8 +1935,8 @@ static int ntfs_sync_fs(struct super_block *sb, int w= ait) return 0; =20 /* If there are some dirty buffers in the bdev inode */ - if (ntfs_clear_volume_dirty_if_no_errors(vol)) { - ntfs_warning(sb, "Failed to clear dirty bit in volume information flags.= Run chkdsk."); + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. = Run chkdsk."); err =3D -EIO; } sync_inodes_sb(sb); diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h index bc85a9592245..c7cd27b6dc1a 100644 --- a/fs/ntfs/volume.h +++ b/fs/ntfs/volume.h @@ -181,6 +181,8 @@ struct ntfs_volume { * Windows-reserved names (CON, AUX, NUL, COM1, * LPT1, etc.) or invalid characters. * + * NV_Hibernated Windows is hibernated on the volume; the sync + * paths must not write the volume flags. * NV_Discard Issue discard/TRIM commands for freed clusters. * NV_DisableSparse Disable creation of sparse regions. * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native= _symlink=3Drel). @@ -199,6 +201,7 @@ enum { NV_ShowHiddenFiles, NV_HideDotFiles, NV_CheckWindowsNames, + NV_Hibernated, NV_Discard, NV_DisableSparse, NV_NativeSymlinkRel, @@ -237,6 +240,7 @@ DEFINE_NVOL_BIT_OPS(SysImmutable) DEFINE_NVOL_BIT_OPS(ShowHiddenFiles) DEFINE_NVOL_BIT_OPS(HideDotFiles) DEFINE_NVOL_BIT_OPS(CheckWindowsNames) +DEFINE_NVOL_BIT_OPS(Hibernated) DEFINE_NVOL_BIT_OPS(Discard) DEFINE_NVOL_BIT_OPS(DisableSparse) DEFINE_NVOL_BIT_OPS(NativeSymlinkRel) --=20 2.25.1 From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D0BDF37C0FD; Mon, 14 Sep 2026 05:49:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364984; cv=none; b=FcMyJSYkHuWLlYaOHE8NyKRHfOJd3p0hO2l/phP7sxfFvl74HMcvomAoDJnlTNfQ+Uk8I8gk0lzmUT0K24+1Eh0Wg7onyyiCBp4IYE8SoIT7TXXfzHscdSo1kvu85788Lf4OdM/qUAlXAtBWxMSwYV2j95tDWZKOID5oh3Dx5Ek= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364984; c=relaxed/simple; bh=uptId5k+YVSOOa7XT+eqPnKSj+g74WfcsvXMwKlWOvA=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=RIr8j2D+c5sQJGyuSBye72xJyTboksFQwGnHFs9+7JAugGF0dDxWot+D4VehFLt+tYJzx8S3gv3smAAFQJSA0JR1+XJQNI3/ePf2mcz7LzVVvmEf0A5PBMNG3TzN4s9yt8X1/Kb+7vVI6LaKEYn3Nt4RH420LydVqw/7o8JaLMI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 1033c396b00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:4d9a6da6-1c5a-4c28-a145-6b4243fa3ea2,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:11a8076f2b94554c8305827c7aa4998e,BulkI D:nil,BulkQuantity:0,SF:81|82|102|850|865|898,TC:nil,Content:0|15|50|99,ED M:-3,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,OSI:0,OSA: 0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 1033c396b00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 360547553; Mon, 14 Sep 2026 13:49:34 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , Baolin Liu , stable@vger.kernel.org Subject: [PATCH v11 4/6] ntfs: persist the dirty state after the final put_super() commits Date: Mon, 14 Sep 2026 13:49:24 +0800 Message-Id: <20260914054926.78808-5-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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 just-in-case mftmirr/mft commits and the final write_inode_now() in ntfs_put_super() can record NVolErrors() after the dirty state has been persisted, so errors from those points would leave the volume unmounted with a clean on-disk dirty bit - contradicting the "cannot unmount clean" guarantee ntfs_sync_volume_dirty_state() is meant to provide. Move the persistence to the end of ntfs_put_super(): keep the gated re-commits and the tail commits where they are, run ntfs_sync_volume_dirty_state() and the $Volume commit after the last write_inode_now(), and release vol->vol_ino only after the sync. The release order of the special inodes matters for the $Volume commit: writing the $Volume record mirrors it through ntfs_sync_mft_mirror() (record number 3 is below vol->mftmirr_size), which fails with -EIO and leaves the mirror stale once vol->mftmirr_ino is gone, so the mirror inode is released only after that commit. vol->vol_ino is then put before vol->mft_ino is dropped: if the commit failed before it could clear the dirty flag, ntfs_evict_big_inode() commits the inode again on its way out, and __ntfs_write_inode() resolves the runlist through vol->mft_ino. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/super.c | 75 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 733565953302..3574c224fe28 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -1812,26 +1812,13 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); =20 /* - * If a read-write mount, persist the error state in the volume flags: - * mark the volume clean if no volume errors have occurred, and make - * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the - * next mount. Also, re-commit all affected inodes. + * If a read-write mount, re-commit all affected inodes once more. + * The dirty state itself is persisted at the end of ntfs_put_super(), + * after the last commits and the final write_inode_now(): those can + * still record errors via __ntfs_write_inode(), and the sync must + * evaluate NVolErrors() with the last setter already run. */ if (!sb_rdonly(sb)) { - if (ntfs_sync_volume_dirty_state(vol)) { - ntfs_warning(sb, - "Failed to sync dirty bit in volume information flags. Run chkdsk."); - } else if (NVolErrors(vol)) { - /* - * The dirty bit is on disk now; only warn when the - * sync actually succeeded, or this message would - * contradict the one above. - */ - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); - } - /* Commits the updated volume flags if they were written. */ - ntfs_commit_inode(vol->vol_ino); if (!NVolErrors(vol)) { ntfs_commit_inode(vol->root_ino); if (vol->mftmirr_ino) @@ -1840,9 +1827,6 @@ static void ntfs_put_super(struct super_block *sb) } } =20 - iput(vol->vol_ino); - vol->vol_ino =3D NULL; - /* NTFS 3.0+ specific clean up. */ if (vol->major_ver >=3D 3) { if (vol->extend_ino) { @@ -1872,8 +1856,6 @@ static void ntfs_put_super(struct super_block *sb) /* Re-commit the mft mirror and mft just in case. */ ntfs_commit_inode(vol->mftmirr_ino); ntfs_commit_inode(vol->mft_ino); - iput(vol->mftmirr_ino); - vol->mftmirr_ino =3D NULL; } /* * We should have no dirty inodes left, due to @@ -1883,6 +1865,53 @@ static void ntfs_put_super(struct super_block *sb) ntfs_commit_inode(vol->mft_ino); write_inode_now(vol->mft_ino, 1); =20 + /* + * If a read-write mount, persist the error state in the volume flags: + * mark the volume clean if no volume errors have occurred, and make + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the + * next mount. + */ + if (!sb_rdonly(sb)) { + if (ntfs_sync_volume_dirty_state(vol)) { + ntfs_warning(sb, + "Failed to sync dirty bit in volume information flags. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * The dirty bit is on disk now; only warn when the + * sync actually succeeded, or this message would + * contradict the one above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } + /* + * Commits the updated volume flags if they were written. + * The mft mirror must still be around for this: the + * $Volume record (mft record number 3, below + * vol->mftmirr_size) is mirrored by write_mft_record() + * through ntfs_sync_mft_mirror(), which fails with -EIO + * and leaves the mirror stale once vol->mftmirr_ino is + * gone, so the mirror inode is only released after this + * commit. + */ + ntfs_commit_inode(vol->vol_ino); + } + + /* + * Release $Volume while the mft inode is still available: if the + * commit above failed before it could clear the dirty flag, + * ntfs_evict_big_inode() commits the inode again on its way out, + * and __ntfs_write_inode() needs vol->mft_ino to look up the + * runlist of the record to write. + */ + iput(vol->vol_ino); + vol->vol_ino =3D NULL; + + if (vol->mftmirr_ino) { + iput(vol->mftmirr_ino); + vol->mftmirr_ino =3D NULL; + } + iput(vol->mft_ino); vol->mft_ino =3D NULL; blkdev_issue_flush(sb->s_bdev); --=20 2.25.1 From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 68CA0279DC3; Mon, 14 Sep 2026 05:49:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364984; cv=none; b=Xrn7WDZpt+9MSTR/CBqdki9/ZeuF+XiJZhm0y4ev+ltztH0Mng7BDuM0nEm/y1ciLZClsBJk/L9EGn0Fo43BQbN6rxsbC3pMZG6n5UhXzfBc3pFpcO4l6KHoPR6GXCzyPzroQCQGRnEqBWpypzg0LHQND7VMH7hF5Px2IVtqWfo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364984; c=relaxed/simple; bh=xChlXecKLbkvgCzgn8vqQ0fkcsQHkoGwqkq+MFIKsoA=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=oxSfZie9zcsdrp34JHQiES7DtWEUemziYxtoQcPdIWNHlhypo0sWbAX34I8M58UGMrcwCCfstaJY4bRbugXu4ntsSlTfg+fUcdj6RZKY93oZAyvoggRiDGJ9ec3GRhGA4xs2BaQCMe1HHhxz5wRQBs2mzJsZCUZQuxoyPsJ21TI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 10d89696b00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:5d415c7a-d7fb-4c00-91e1-61ca17bbd0f5,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:6ef593a8e4f9c954b41404b7337df4c9,BulkI D:nil,BulkQuantity:0,SF:81|82|102|850|865|898,TC:nil,Content:0|15|50|99,ED M:-3,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,OSI:0,OSA: 0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 10d89696b00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 267740798; Mon, 14 Sep 2026 13:49:35 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , Baolin Liu , stable@vger.kernel.org Subject: [PATCH v11 5/6] ntfs: do not clear the volume dirty bit during sync Date: Mon, 14 Sep 2026 13:49:25 +0800 Message-Id: <20260914054926.78808-6-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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" ntfs_sync_fs() clears VOLUME_IS_DIRTY while the volume is still mounted read-write, so a sync running concurrently with an in-flight metadata modification can clear and persist a bit that was just set: the modification then lands on a volume that is clean on disk, and a crash does not run chkdsk. Closing this from the writer side would mean holding the $Volume mrec_lock across the whole operation. Drop the ntfs_sync_fs() persistence point and leave the clearing to the remount-to-read-only path, which the VFS reaches only after sb_prepare_remount_readonly() has drained in-flight writers, and to ntfs_put_super(), which runs after evict_inodes() on a quiesced filesystem. A mounted read-write volume now keeps the dirty bit until it is dismounted cleanly, which matches the NTFS semantics; the cost is a needless chkdsk if the machine crashes between a sync and the unmount. Also report sync_blockdev() and blkdev_issue_flush() failures instead of discarding them: a failed cache flush is real, and __sync_filesystem() propagates it to syncfs() callers. The marking comment in ntfs_file_write_iter() is updated to match: the dirty bit is only cleared at the quiescent transitions, under the same $Volume mrec_lock the marking takes. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/file.c | 7 ++++--- fs/ntfs/super.c | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c index cfc7b36b7dff..99a2c7a5cf81 100644 --- a/fs/ntfs/file.c +++ b/fs/ntfs/file.c @@ -621,9 +621,10 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb= , struct iov_iter *from) =20 /* * The volume must be marked dirty before the modification is made, - * without an unlocked check of the in-memory flag: ntfs_sync_fs() - * can clear the bit concurrently and the modification would then - * land on a volume that is clean on disk. + * without an unlocked check of the in-memory flag: the dirty bit + * is only cleared at the quiescent transitions, under the same + * $Volume mrec_lock this call takes, so an unlocked skip could + * lose the set to one of them. */ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY); =20 diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 3574c224fe28..f155f16b252d 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -486,8 +486,8 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le= 16 flags) * This is the single point that persists the in-memory error state to dis= k. * The runtime error paths only record NVolErrors() because they run under= a * variety of ntfs locks the dirty-bit write cannot be taken under (runlist - * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first - * ntfs_sync_fs(), a remount, or the unmount then persists the flag here. + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); a remount to + * read-only, or the unmount, then persists the flag here. * * A hibernated volume is not written from these persistence paths: * resuming Windows from a modified image corrupts it, so the dirty bit @@ -1963,14 +1963,18 @@ static int ntfs_sync_fs(struct super_block *sb, int= wait) if (!wait) return 0; =20 - /* If there are some dirty buffers in the bdev inode */ - if (ntfs_sync_volume_dirty_state(vol)) { - ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. = Run chkdsk."); - err =3D -EIO; - } + /* + * The volume dirty bit is deliberately not cleared here: a sync + * running concurrently with an in-flight modification could clear + * and persist a bit that was just set, leaving the modification + * on a volume that is clean on disk. The bit is only cleared + * and persisted at quiescent state transitions: remounting + * read-only and clean unmount. + */ sync_inodes_sb(sb); - sync_blockdev(sb->s_bdev); - blkdev_issue_flush(sb->s_bdev); + err =3D sync_blockdev(sb->s_bdev); + if (!err) + err =3D blkdev_issue_flush(sb->s_bdev); return err; } =20 --=20 2.25.1 From nobody Fri Sep 25 10:38:36 2026 Received: from mailgw.kylinos.cn (mailgw.kylinos.cn [124.126.103.232]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 88E591DED42; Mon, 14 Sep 2026 05:49:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=124.126.103.232 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364989; cv=none; b=AGjt8MJr0g3JWlO6+2vq7qUUqFR7iOwuzjkfXAf+hZ/V6FoQ0Qaa1ST3ETivZcDp2TsckXve/6x/5BNoYGomhLUDvaWOXCWaRnumXXkva6xmV1/5Iahty5Q06HhnWdVj8PsBamrDZpeZdRFVtZujwiJ2X6/f1yqTvc6/8p60DxM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789364989; c=relaxed/simple; bh=m4/BXjLGVuVAlRj8pGSSsD0J60QoI9urtJmiCrZFwk0=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=F7oHhu5j8HK+2FAPuP4YvBWoBamgum/pQWyRzBWyzF10cgJ+m0uouVLCU+TNDDkpqibHSXQqAcAXFq6W0ApoukwViak+UdR58IFVOgrYm+IEbrc1hv0+lMwVECilAC9n15kKl+u+trKmEqGWA3yI/iowGURJ01RnEewKobXxCZI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=kylinos.cn; arc=none smtp.client-ip=124.126.103.232 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=kylinos.cn X-UUID: 116cb736b00011f19a56ed5b684f684d-20260914 X-CID-P-RULE: Release_Ham X-CID-O-INFO: VERSION:1.3.19,REQID:ec0ad7ee-8267-4980-8ad0-40a37e860360,IP:0,U RL:0,TC:0,Content:0,EDM:0,RT:0,SF:0,FILE:0,BULK:0,RULE:Release_Ham,ACTION: release,TS:0 X-CID-META: VersionHash:7db8b62,CLOUDID:a243d128a0b9e0e2ad890da20d224acb,BulkI D:nil,BulkQuantity:0,SF:81|82|102|850|865|898,TC:nil,Content:0|15|50|99,ED M:-3,IP:nil,URL:0,File:nil,RT:nil,Bulk:nil,QS:nil,BEC:nil,COL:0,OSI:0,OSA: 0,AV:0,LES:1,SPR:NO,DKR:0,DKP:0,BRR:0,BRE:0,ARC:0 X-CID-BVR: 2,SSN|SDN X-CID-BAS: 2,SSN|SDN,0,_ X-CID-FACTOR: TF_CID_SPAM_SNR X-CID-RHF: D41D8CD98F00B204E9800998ECF8427E X-UUID: 116cb736b00011f19a56ed5b684f684d-20260914 X-User: zenghongling@kylinos.cn Received: from localhost.localdomain [(10.44.16.150)] by mailgw.kylinos.cn (envelope-from ) (Generic MTA with TLSv1.3 TLS_AES_256_GCM_SHA384 256/256) with ESMTP id 1597423051; Mon, 14 Sep 2026 13:49:36 +0800 From: Hongling Zeng To: linkinjeon@kernel.org, hyc.lee@gmail.com Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org, zhongling0719@126.com, Hongling Zeng , Baolin Liu , stable@vger.kernel.org Subject: [PATCH v11 6/6] ntfs: check the dirty-state commit on remount and unmount Date: Mon, 14 Sep 2026 13:49:26 +0800 Message-Id: <20260914054926.78808-7-zenghongling@kylinos.cn> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260914054926.78808-1-zenghongling@kylinos.cn> References: <20260914054926.78808-1-zenghongling@kylinos.cn> 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 remount-to-read-only path commits the updated volume flags with ntfs_commit_inode(), a void wrapper around __ntfs_write_inode(), and ignores the blkdev_issue_flush() return value, so a failed commit or flush is reported as success. Once the remount has succeeded no persistence point is ever reached again: ntfs_put_super() skips read-only superblocks and the VFS never syncs one, so fail the remount unless the commit and the flush succeed. The superblock then stays read-write and ntfs_put_super() retries the persistence at unmount. The errors=3Dremount-ro downgrade does not go through ntfs_reconfigure() and is unchanged. A zero-return commit is not trusted blindly: write_mft_record() redirties the record on allocation failure and reports success, so the $Volume inode is required to be clean afterwards. ntfs_put_super() discards the same commit error. Call __ntfs_write_inode() there with the same dirty re-check and warn on failure, as put_super() cannot return an error. The commit is skipped when the dirty-state sync itself failed, as that could write back an inconsistent flag state; a record left dirty by an earlier update is still committed at evict time. NVolErrors() is deliberately not used to detect the failure: it is sticky for the lifetime of the mount, so it cannot distinguish a fresh commit failure from errors recorded before the remount. Hibernated volumes: ntfs_sync_volume_dirty_state() is a no-op for them and never dirties the $Volume inode on such a mount, since the on-disk flags are already dirty and ntfs_set_volume_flags() has nothing to change. The commit only runs if something dirtied the inode independently, as before this patch; mounting hibernated volumes read-only removes even that. Reported-by: Baolin Liu Cc: stable@vger.kernel.org Signed-off-by: Hongling Zeng --- fs/ntfs/super.c | 76 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index f155f16b252d..c547d2ca51ca 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -268,6 +268,7 @@ static int ntfs_reconfigure(struct fs_context *fc) { struct super_block *sb =3D fc->root->d_sb; struct ntfs_volume *vol =3D NTFS_SB(sb); + int err; =20 ntfs_debug("Entering with remount"); =20 @@ -324,14 +325,39 @@ static int ntfs_reconfigure(struct fs_context *fc) * and ntfs_put_super() skips them, so the only remaining * write would be the evict-time commit at unmount, which * a crash never reaches. An error recorded only after - * the remount is still never persisted. + * the remount is still never persisted; a failed commit + * or flush fails the remount, leaving the superblock + * read-write so ntfs_put_super() retries at unmount. */ - if (ntfs_sync_volume_dirty_state(vol)) { + err =3D ntfs_sync_volume_dirty_state(vol); + if (err) { ntfs_warning(sb, "Failed to update dirty bit in volume information flags. Run chkdsk."= ); - } else if (NInoDirty(NTFS_I(vol->vol_ino))) { - ntfs_commit_inode(vol->vol_ino); - blkdev_issue_flush(sb->s_bdev); + return err; + } + if (NInoDirty(NTFS_I(vol->vol_ino))) { + /* ntfs_commit_inode() would discard the error. */ + err =3D __ntfs_write_inode(vol->vol_ino, 1); + if (err) { + ntfs_warning(sb, + "Failed to commit volume information flags. Run chkdsk."); + return err; + } + /* + * write_mft_record() redirties the record on + * -ENOMEM and still reports success. + */ + if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_warning(sb, + "Volume information flags remain dirty after commit. Run chkdsk."); + return -EIO; + } + err =3D blkdev_issue_flush(sb->s_bdev); + if (err) { + ntfs_warning(sb, + "Failed to flush volume information flags. Run chkdsk."); + return err; + } } } =20 @@ -1875,26 +1901,32 @@ static void ntfs_put_super(struct super_block *sb) if (ntfs_sync_volume_dirty_state(vol)) { ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk."); - } else if (NVolErrors(vol)) { + } else { /* - * The dirty bit is on disk now; only warn when the - * sync actually succeeded, or this message would - * contradict the one above. + * __ntfs_write_inode(), not the void + * ntfs_commit_inode() wrapper: the error can only + * be warned about here. The mirror inode is only + * released below: writing the $Volume record (mft + * record number 3, below vol->mftmirr_size) mirrors + * it through ntfs_sync_mft_mirror(), which fails + * with -EIO once vol->mftmirr_ino is gone. */ - ntfs_warning(sb, - "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + if (__ntfs_write_inode(vol->vol_ino, 1)) { + ntfs_warning(sb, + "Failed to commit volume information flags. Run chkdsk."); + } else if (NInoDirty(NTFS_I(vol->vol_ino))) { + ntfs_warning(sb, + "Volume information flags remain dirty after commit. Run chkdsk."); + } else if (NVolErrors(vol)) { + /* + * Only warn once the commit has succeeded, + * or this could contradict a failure + * reported above. + */ + ntfs_warning(sb, + "Volume has errors. Leaving volume marked dirty. Run chkdsk."); + } } - /* - * Commits the updated volume flags if they were written. - * The mft mirror must still be around for this: the - * $Volume record (mft record number 3, below - * vol->mftmirr_size) is mirrored by write_mft_record() - * through ntfs_sync_mft_mirror(), which fails with -EIO - * and leaves the mirror stale once vol->mftmirr_ino is - * gone, so the mirror inode is only released after this - * commit. - */ - ntfs_commit_inode(vol->vol_ino); } =20 /* --=20 2.25.1