[PATCH 2/4] fs/ntfs3: validate restart table offsets in log records
From: Giulia Aloia
Date: Mon Sep 21 2026 - 15:25:06 EST
check_log_rec() validates transact_id and target_attr by subtracting the
24-byte restart-table header size, sizeof(struct RESTART_TABLE), and then
checking entry alignment. This is unsafe for offsets that point inside
the header. For example, offset 8 is below the header size, so the
unsigned subtraction wraps and the wrapped value can still pass the
alignment check.
The driver uses transact_id as an offset into the transaction table
when it looks up or allocates entries during journal analysis. This
happens even on read-only mounts, before replay stops for read-only
mode, so the offset must be checked at this stage too. If a forged
transact_id points into the restart-table header, analysis first reads
header bytes as tr->next. If those bytes do not look allocated, it can
then ask alloc_rsttbl_from_idx() to allocate an offset inside the
header. With crafted table metadata, that can make replay overwrite
restart-table header bytes and later treat those bytes as a
TRANSACTION_ENTRY.
The attribute-offset check is also skipped when lcns_follow is zero.
However, lcns_follow only describes page_lcns[] payload. It does not
mean target_attr is unused. OpenNonresidentAttribute can have no LCN
payload but still uses target_attr to choose or create an open-attribute
entry. Header and misaligned offsets can therefore reach the
open-attribute allocator unchecked.
For offset 8, the subtraction wraps on both 32-bit and 64-bit systems.
The wrapped value is divisible by 40, sizeof(struct TRANSACTION_ENTRY),
on both, so the transaction-ID check can accept it. The same wrapped
value is also divisible by the 40-byte v1 open-attribute entry size and,
on 64-bit systems, by 44, SIZEOF_OPENATTRIBUTEENTRY0, so the
attribute-offset check can accept it too.
For target_attr, replay can then interpret the restart table header as an
open-attribute entry. With crafted on-disk values, the interpreted entry
can contain a NULL open_attr pointer, which log_replay() later
dereferences.
This is reachable by mounting the crafted image on an x86-64 KASAN
kernel before this fix:
KASAN: null-ptr-deref in range
[0x0000000000000008-0x000000000000000f]
RIP: 0010:log_replay+0xca58/0xe690
Call Trace:
ntfs_loadlog_and_replay+0x3e0/0x500
ntfs_fill_super+0x1fd3/0x4510
...
Reject offsets that point inside the restart-table header before
subtracting the header size, so the subtraction cannot wrap. Validate
nonzero target_attr values even when lcns_follow is zero. Preserve zero
target_attr for records that require neither an attribute nor LCN work.
Do not impose a table upper bound in check_log_rec(): valid records can
require the analysis pass to grow the table.
Before OpenNonresidentAttribute grows the open attribute table or selects
an entry, validate target_attr against the actual oatbl->size as well.
Alignment to the version-specific entry size used by check_log_rec() does
not guarantee alignment to the slots used by the current table when the
on-disk table size differs. Allow aligned offsets beyond the current
table so valid records can still grow it.
Cen Zhang described the target_attr underflow in the linked patch and
proposed checks at the redo and undo lookups. Validate the offsets in
check_log_rec() itself, including transact_id and records without LCNs.
Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal")
Cc: stable@xxxxxxxxxxxxxxx
Link: https://lore.kernel.org/all/20260901174934.6275-1-cenzhang@xxxxxxxxxxxxxxxxxxx/
Assisted-by: Bynario AI
Signed-off-by: Giulia Aloia <giulia@xxxxxxxx>
---
fs/ntfs3/fslog.c | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c
index 8ac0dbd2f07f..8dd233ec7d2f 100644
--- a/fs/ntfs3/fslog.c
+++ b/fs/ntfs3/fslog.c
@@ -697,7 +697,7 @@ static bool check_log_rec(const struct LOG_REC_HDR *lr, u32 bytes, u32 tr,
if (bytes < sizeof(struct LOG_REC_HDR))
return false;
- if (!tr)
+ if (tr < sizeof(struct RESTART_TABLE))
return false;
if ((tr - sizeof(struct RESTART_TABLE)) %
@@ -711,7 +711,7 @@ static bool check_log_rec(const struct LOG_REC_HDR *lr, u32 bytes, u32 tr,
return false;
if (lr->target_attr)
- goto check_lcns;
+ goto check_target;
if (is_target_required(le16_to_cpu(lr->redo_op)))
return false;
@@ -719,12 +719,13 @@ static bool check_log_rec(const struct LOG_REC_HDR *lr, u32 bytes, u32 tr,
if (is_target_required(le16_to_cpu(lr->undo_op)))
return false;
-check_lcns:
- if (!lr->lcns_follow)
+check_target:
+ if (!lr->lcns_follow && !lr->target_attr)
goto check_length;
t16 = le16_to_cpu(lr->target_attr);
- if ((t16 - sizeof(struct RESTART_TABLE)) % bytes_per_attr_entry)
+ if (t16 < sizeof(struct RESTART_TABLE) ||
+ (t16 - sizeof(struct RESTART_TABLE)) % bytes_per_attr_entry)
return false;
check_length:
@@ -4737,6 +4738,12 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
case OpenNonresidentAttribute:
t16 = le16_to_cpu(lrh->target_attr);
+ if (t16 < sizeof(*oatbl) ||
+ (t16 - sizeof(*oatbl)) % le16_to_cpu(oatbl->size)) {
+ err = -EINVAL;
+ goto out;
+ }
+
if (t16 >= bytes_per_rt(oatbl)) {
/*
* Compute how big the table needs to be.
--
2.55.0