From: Giulia Aloia <giulia@bynar.io>
To: almaz.alexandrovich@paragon-software.com
Cc: ntfs3@lists.linux.dev, linux-kernel@vger.kernel.org,
cenzhang@linux.microsoft.com
Subject: [PATCH 2/4] fs/ntfs3: validate restart table offsets in log records
Date: Mon, 21 Sep 2026 21:21:37 +0200 [thread overview]
Message-ID: <20260921192157.102738-3-giulia@bynar.io> (raw)
In-Reply-To: <20260921192157.102738-1-giulia@bynar.io>
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@vger.kernel.org
Link: https://lore.kernel.org/all/20260901174934.6275-1-cenzhang@linux.microsoft.com/
Assisted-by: Bynario AI
Signed-off-by: Giulia Aloia <giulia@bynar.io>
---
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
next prev parent reply other threads:[~2026-09-21 19:22 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-21 19:21 [PATCH 0/4] fs/ntfs3: tighten restart-table offset validation Giulia Aloia
2026-09-21 19:21 ` [PATCH 1/4] fs/ntfs3: validate dirty page open attribute offsets Giulia Aloia
2026-09-21 19:21 ` Giulia Aloia [this message]
2026-09-21 19:21 ` [PATCH 3/4] fs/ntfs3: validate on-disk restart tables before use Giulia Aloia
2026-09-21 19:21 ` [PATCH 4/4] fs/ntfs3: fix out-of-bounds access in alloc_rsttbl_from_idx() Giulia Aloia
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260921192157.102738-3-giulia@bynar.io \
--to=giulia@bynar.io \
--cc=almaz.alexandrovich@paragon-software.com \
--cc=cenzhang@linux.microsoft.com \
--cc=linux-kernel@vger.kernel.org \
--cc=ntfs3@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®