* [PATCH] ntfs: validate the full MST update sequence array
@ 2026-05-08 14:08 DaeMyung Kang
0 siblings, 0 replies; only message in thread
From: DaeMyung Kang @ 2026-05-08 14:08 UTC (permalink / raw)
To: Namjae Jeon; +Cc: Hyunchul Lee, linux-fsdevel, linux-kernel
The NTFS update sequence array contains the update sequence number
itself followed by one fixup entry for each protected sector. The
current MST validation subtracts one from usa_count before checking
the array bounds, so it only accounts for the fixup entries and not
the leading update sequence number.
A malformed record can therefore pass validation with the last fixup
entry outside the record buffer. post_read_mst_fixup() then reads past
the supplied buffer, while pre_write_mst_fixup() can write past it.
The USA also must end before the last word of the first 512-byte sector,
otherwise applying the fixups can overlap the sector trailer that the
array is supposed to protect.
This matches the documented NTFS MULTI_SECTOR_HEADER layout, where the
update sequence array is required to end before the last USHORT in the
first sector, and the sequence number stride is 512 bytes.
Validate the on-disk usa_count before converting it to a fixup count,
check the full USA size, and share this validation between the MST
helpers. Pass the record size to post_write_mst_fixup() as well so
error paths do not restore data from a malformed update sequence array.
A small userspace ASAN reproducer with size=512, usa_ofs=510 and
usa_count=2 triggers a heap-buffer-overflow with the old validation.
With this change the same malformed record is rejected before the
fixup array is dereferenced.
Fixes: d3ad708fecaa ("ntfs: Initial commit")
Signed-off-by: DaeMyung Kang <charsyam@gmail.com>
---
fs/ntfs/index.c | 3 ++-
fs/ntfs/mst.c | 68 ++++++++++++++++++++++++++++++++++++++---------------------------
fs/ntfs/ntfs.h | 2 +-
3 files changed, 43 insertions(+), 30 deletions(-)
diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index 413a1425d640..c2289550bc8f 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -178,7 +178,8 @@ int ntfs_icx_ib_sync_write(struct ntfs_index_context *icx)
icx->ib = NULL;
icx->ib_dirty = false;
} else {
- post_write_mst_fixup((struct ntfs_record *)icx->ib);
+ post_write_mst_fixup((struct ntfs_record *)icx->ib,
+ icx->block_size);
icx->sync_write = false;
}
diff --git a/fs/ntfs/mst.c b/fs/ntfs/mst.c
index 7f9faad924ad..d1b9ed0703bd 100644
--- a/fs/ntfs/mst.c
+++ b/fs/ntfs/mst.c
@@ -9,6 +9,30 @@
#include "ntfs.h"
+static bool mst_fixup_valid(const struct ntfs_record *b, const u32 size,
+ u16 *usa_ofs, u16 *fixup_count)
+{
+ u16 usa_count;
+ u32 usa_end;
+
+ if (!b)
+ return false;
+
+ *usa_ofs = le16_to_cpu(b->usa_ofs);
+ usa_count = le16_to_cpu(b->usa_count);
+
+ if (!size || (size & (NTFS_BLOCK_SIZE - 1)) || (*usa_ofs & 1) ||
+ usa_count != (size >> NTFS_BLOCK_SIZE_BITS) + 1)
+ return false;
+
+ usa_end = *usa_ofs + usa_count * sizeof(__le16);
+ if (usa_end > size || usa_end > NTFS_BLOCK_SIZE - sizeof(__le16))
+ return false;
+
+ *fixup_count = usa_count - 1;
+ return true;
+}
+
/*
* post_read_mst_fixup - deprotect multi sector transfer protected data
* @b: pointer to the data to deprotect
@@ -28,18 +52,12 @@
*/
int post_read_mst_fixup(struct ntfs_record *b, const u32 size)
{
- u16 usa_ofs, usa_count, usn;
+ u16 usa_ofs, usa_count, fixup_count, usn;
u16 *usa_pos, *data_pos;
- /* Setup the variables. */
- usa_ofs = le16_to_cpu(b->usa_ofs);
- /* Decrement usa_count to get number of fixups. */
- usa_count = le16_to_cpu(b->usa_count) - 1;
- /* Size and alignment checks. */
- if (size & (NTFS_BLOCK_SIZE - 1) || usa_ofs & 1 ||
- usa_ofs + (usa_count * 2) > size ||
- (size >> NTFS_BLOCK_SIZE_BITS) != usa_count)
+ if (!mst_fixup_valid(b, size, &usa_ofs, &fixup_count))
return 0;
+ usa_count = fixup_count;
/* Position of usn in update sequence array. */
usa_pos = (u16 *)b + usa_ofs/sizeof(u16);
/*
@@ -75,8 +93,7 @@ int post_read_mst_fixup(struct ntfs_record *b, const u32 size)
}
data_pos += NTFS_BLOCK_SIZE / sizeof(u16);
}
- /* Re-setup the variables. */
- usa_count = le16_to_cpu(b->usa_count) - 1;
+ usa_count = fixup_count;
data_pos = (u16 *)b + NTFS_BLOCK_SIZE / sizeof(u16) - 1;
/* Fixup all sectors. */
while (usa_count--) {
@@ -115,21 +132,14 @@ int post_read_mst_fixup(struct ntfs_record *b, const u32 size)
int pre_write_mst_fixup(struct ntfs_record *b, const u32 size)
{
__le16 *usa_pos, *data_pos;
- u16 usa_ofs, usa_count, usn;
+ u16 usa_ofs, fixup_count, usn;
__le16 le_usn;
/* Sanity check + only fixup if it makes sense. */
if (!b || ntfs_is_baad_record(b->magic) ||
ntfs_is_hole_record(b->magic))
return -EINVAL;
- /* Setup the variables. */
- usa_ofs = le16_to_cpu(b->usa_ofs);
- /* Decrement usa_count to get number of fixups. */
- usa_count = le16_to_cpu(b->usa_count) - 1;
- /* Size and alignment checks. */
- if (size & (NTFS_BLOCK_SIZE - 1) || usa_ofs & 1 ||
- usa_ofs + (usa_count * 2) > size ||
- (size >> NTFS_BLOCK_SIZE_BITS) != usa_count)
+ if (!mst_fixup_valid(b, size, &usa_ofs, &fixup_count))
return -EINVAL;
/* Position of usn in update sequence array. */
usa_pos = (__le16 *)((u8 *)b + usa_ofs);
@@ -145,7 +155,7 @@ int pre_write_mst_fixup(struct ntfs_record *b, const u32 size)
/* Position in data of first u16 that needs fixing up. */
data_pos = (__le16 *)b + NTFS_BLOCK_SIZE/sizeof(__le16) - 1;
/* Fixup all sectors. */
- while (usa_count--) {
+ while (fixup_count--) {
/*
* Increment the position in the usa and save the
* original data from the data buffer into the usa.
@@ -162,17 +172,19 @@ int pre_write_mst_fixup(struct ntfs_record *b, const u32 size)
/*
* post_write_mst_fixup - fast deprotect multi sector transfer protected data
* @b: pointer to the data to deprotect
+ * @size: size in bytes of @b
*
- * Perform the necessary post write multi sector transfer fixup, not checking
- * for any errors, because we assume we have just used pre_write_mst_fixup(),
- * thus the data will be fine or we would never have gotten here.
+ * Perform the necessary post write multi sector transfer fixup. This is only
+ * expected after pre_write_mst_fixup() has applied fixups, but keep the bounds
+ * checks so error paths cannot restore from a malformed update sequence array.
*/
-void post_write_mst_fixup(struct ntfs_record *b)
+void post_write_mst_fixup(struct ntfs_record *b, const u32 size)
{
__le16 *usa_pos, *data_pos;
+ u16 usa_ofs, fixup_count;
- u16 usa_ofs = le16_to_cpu(b->usa_ofs);
- u16 usa_count = le16_to_cpu(b->usa_count) - 1;
+ if (!mst_fixup_valid(b, size, &usa_ofs, &fixup_count))
+ return;
/* Position of usn in update sequence array. */
usa_pos = (__le16 *)b + usa_ofs/sizeof(__le16);
@@ -181,7 +193,7 @@ void post_write_mst_fixup(struct ntfs_record *b)
data_pos = (__le16 *)b + NTFS_BLOCK_SIZE/sizeof(__le16) - 1;
/* Fixup all sectors. */
- while (usa_count--) {
+ while (fixup_count--) {
/*
* Increment position in usa and restore original data from
* the usa into the data buffer.
diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
index e301c68c780b..445101c06ef9 100644
--- a/fs/ntfs/ntfs.h
+++ b/fs/ntfs/ntfs.h
@@ -230,7 +230,7 @@ int ntfs_write_volume_label(struct ntfs_volume *vol, char *label);
/* From fs/ntfs/mst.c */
int post_read_mst_fixup(struct ntfs_record *b, const u32 size);
int pre_write_mst_fixup(struct ntfs_record *b, const u32 size);
-void post_write_mst_fixup(struct ntfs_record *b);
+void post_write_mst_fixup(struct ntfs_record *b, const u32 size);
/* From fs/ntfs/unistr.c */
bool ntfs_are_names_equal(const __le16 *s1, size_t s1_len,
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-05-08 14:08 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-08 14:08 [PATCH] ntfs: validate the full MST update sequence array DaeMyung Kang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox