fs/ntfs3: fix integer overflow in find_log_rec() causing OOB read
Subsystems (2)
PATCH 1/1 fs/ntfs3: fix integer overflow in find_log_rec() causing OOB read
find_log_rec() validates the on-disk LFS_RECORD_HDR.client_data_len field (len, a raw u32 read straight from a mounted $LogFile page) against the log's total available space:
len = le32_to_cpu(rh->client_data_len);
rec_len = len + log->record_header_len;
if (rec_len >= log->total_avail)
return -EINVAL;The addition is unchecked. For len in [0xffffffd0, 0xffffffff] (i.e. log->record_header_len away from UINT_MAX), rec_len wraps to a small u32 value that trivially clears the rec_len >= log->total_avail guard, so a record whose declared length is actually ~4 GiB is reported as valid and in-page.
Because find_log_rec() wrongly accepts the record, its caller read_log_rec_lcb() hands it back to log_replay() as good. On the transaction-table replay leg, log_replay() re-derives the record length directly from the same raw, still-unclamped client_data_len field and uses it to size the restart-table blob:
rec_len = le32_to_cpu(frh->client_data_len); /* ~0xffffffff */
...
t16 = le16_to_cpu(lrh->redo_off);
rt = Add2Ptr(lrh, t16);
t32 = rec_len - t16; /* still ~4 GiB */
if (!check_rstbl(rt, t32))
return -EINVAL;check_rstbl()'s own bytes < ts bounds guard is defeated because the caller-supplied bytes (t32) is itself the poisoned ~4 GiB value, so the guard can never trigger. The subsequent entry-validation loop then walks rt->used (also attacker-controlled) entries of sizeof(struct RESTART_TABLE) == 0x18 bytes each straight past the end of the kmalloc(log->page_size) log-page buffer allocated in read_log_page(), producing an out-of-bounds read that is reachable simply by mounting a crafted $LogFile (no genuine dirty-page / crash-recovery state is required). Confirmed via KASAN as a slab-out-of-bounds read in check_rstbl(), called from log_replay() -> ntfs_loadlog_and_replay() -> ntfs_fill_super() -> mount(2).
Fix the root cause instead of hardening every downstream consumer of the poisoned length: make the addition in find_log_rec() overflow-safe with check_add_overflow(), mirroring the pattern fs/ntfs3/run.c already uses to validate other attacker-controlled on-disk length/offset arithmetic decoded from MAPPING_PAIRS runs, e.g.:
if (check_add_overflow(vcn64, len, &next_vcn))
return -EINVAL;
...
if (check_add_overflow(lcn, len, &lcn_end))
return -EINVAL;(fs/ntfs3/run.c, run_unpack()).
With the overflow rejected instead of silently wrapped, rec_len is only ever a true, non-wrapped sum, so the existing rec_len >= log->total_avail check now correctly rejects any oversized record up front. log_replay()'s transaction-table leg then never gets a chance to re-read the poisoned client_data_len, and check_rstbl() is never invoked with an attacker-inflated bytes for this path.
Verified on a v6.19 KASAN build: mounting a crafted $LogFile whose client_data_len overflows trips a KASAN slab-out-of-bounds-read report in check_rstbl() before this fix; with check_add_overflow() applied, the same image is cleanly rejected during log replay and no KASAN report fires.
--- fs/ntfs3/fslog.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)
fs/ntfs3/fslog.c +3 -1
@@ -7,6 +7,7 @@#include <linux/blkdev.h> #include <linux/fs.h> +#include <linux/overflow.h> #include <linux/random.h> #include <linux/slab.h>
@@ -2424,7 +2425,8 @@ static int find_log_rec(struct ntfs_log *log, u64 lsn, struct lcb *lcb)* Check that the length field isn't greater than the total * available space the log file. */ - rec_len = len + log->record_header_len; +if (check_add_overflow (len, log->record_header_len, &rec_len)) +return -EINVAL ;if (rec_len >= log->total_avail)return -EINVAL ;
Signature · 1 line
2.50.1 (Apple Git-155)
Konstantin Komarov
maintainerQuoted 100 lines · Ibrahim Hashimov
find_log_rec()validates the on-disk LFS_RECORD_HDR.client_data_len field (len, a raw u32 read straight from a mounted $LogFile page) against the log's total available space:len = le32_to_cpu(rh->client_data_len); rec_len = len + log->record_header_len; if (rec_len >= log->total_avail) return -EINVAL;The addition is unchecked. For len in [
0xffffffd0,0xffffffff] (i.e.log->record_header_lenaway fromUINT_MAX),rec_lenwraps to a small u32 value that trivially clears therec_len >= log->total_availguard, so a record whose declared length is actually ~4 GiB is reported as valid and in-page.Because
find_log_rec()wrongly accepts the record, its callerread_log_rec_lcb()hands it back tolog_replay()as good. On the transaction-table replay leg,log_replay()re-derives the record length directly from the same raw, still-unclampedclient_data_lenfield and uses it to size the restart-table blob:rec_len = le32_to_cpu(frh->client_data_len); /* ~0xffffffff */ ... t16 = le16_to_cpu(lrh->redo_off); rt = Add2Ptr(lrh, t16); t32 = rec_len - t16; /* still ~4 GiB */ if (!check_rstbl(rt, t32)) return -EINVAL;check_rstbl()'s own
bytes < tsbounds guard is defeated because the caller-suppliedbytes(t32) is itself the poisoned ~4 GiB value, so the guard can never trigger. The subsequent entry-validation loop then walksrt->used(also attacker-controlled) entries ofsizeof(struct RESTART_TABLE)==0x18bytes each straight past the end of thekmalloc(log->page_size)log-page buffer allocated inread_log_page(), producing an out-of-bounds read that is reachable simply by mounting a crafted $LogFile (no genuine dirty-page / crash-recovery state is required). Confirmed via KASAN as a slab-out-of-bounds read incheck_rstbl(), called fromlog_replay()->ntfs_loadlog_and_replay()->ntfs_fill_super()-> mount(2).Fix the root cause instead of hardening every downstream consumer of the poisoned length: make the addition in
find_log_rec()overflow-safe withcheck_add_overflow(), mirroring the patternfs/ntfs3/run.calready uses to validate other attacker-controlled on-disk length/offset arithmetic decoded fromMAPPING_PAIRSruns, e.g.:if (check_add_overflow(vcn64, len, &next_vcn)) return -EINVAL; ... if (check_add_overflow(lcn, len, &lcn_end)) return -EINVAL;(
fs/ntfs3/run.c,run_unpack()).With the overflow rejected instead of silently wrapped,
rec_lenis only ever a true, non-wrapped sum, so the existingrec_len >= log->total_availcheck now correctly rejects any oversized record up front. log_replay()'s transaction-table leg then never gets a chance to re-read the poisonedclient_data_len, andcheck_rstbl()is never invoked with an attacker-inflatedbytesfor this path.Verified on a v6.19 KASAN build: mounting a crafted $LogFile whose
client_data_lenoverflows trips a KASAN slab-out-of-bounds-read report incheck_rstbl()before this fix; withcheck_add_overflow()applied, the same image is cleanly rejected during log replay and no KASAN report fires.Fixes b46acd6a6a62 ("fs/ntfs3: Add NTFS journal")Signed-off-by Ibrahim Hashimov <[email protected]>Assisted-by AuditCode-AI:2026.07--- fs/ntfs3/fslog.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)fs/ntfs3/fslog.c +3
-2@@ -7,6 +7,7 @@#include <linux/blkdev.h> #include <linux/fs.h> +#include <linux/overflow.h> #include <linux/random.h> #include <linux/slab.h> @@ -2424,7 +2425,8 @@ static int find_log_rec(struct ntfs_log *log, u64 lsn, struct lcb *lcb)* Check that the length field isn't greater than the total * available space the log file. */ - rec_len = len + log->record_header_len; +if (check_add_overflow (len, log->record_header_len, &rec_len)) +return -EINVAL ;if (rec_len >= log->total_avail)return -EINVAL ; --
Quoted 1 line
2.50.1 (Apple Git-155)
Hello,
Your patch was applied, thanks.
Regards,
Konstantin