A bounds-check omission exists in fs/ntfs3/fslog.c that leads to an attacker-controlled relative heap out-of-bounds write via an unvalidated data_off field in the log_replay() function. The issue affects both the UpdateRecordDataRoot and UpdateRecordDataAllocation code execution branches.
The flaw triggers during mount-time log recovery of an uncleanly unmounted filesystem. An attacker capable of providing a corrupted or crafted NTFS image can exploit this to corrupt adjacent kernel heap allocations.
The NTFS_DE directory entry structure contains an attacker-controlled 16-bit field read straight from on-disk filesystem metadata:
struct NTFS_DE {
struct MFT_REF ref;
__le16 size;
__le16 key_size;
__le16 flags;
__le16 reserved;
union {
struct {
__le16 data_off; /* Attacker-controlled offset */
__le16 data_size;
} view;
};
};
During log replay dispatch operations, this unvalidated data_off field is used directly in destination pointer arithmetic inside a memmove() operation without checking entry bounds:
case UpdateRecordDataRoot:
root = resident_data(attr);
hdr = &root->ihdr;
if (!check_if_index_root(rec, lrh) || !check_if_root_index(attr, hdr, lrh)) {
goto dirty_vol;
}
e = Add2Ptr(attr, le16_to_cpu(lrh->attr_off));
memmove(
Add2Ptr(e, le16_to_cpu(e->view.data_off)), /* Destination address calculation */
data,
dlen
);
mi->dirty = true;
break;
The target pointer resolves directly to e + e->view.data_off. Because data_off can be scaled up to 0xFFFF (65535), the write range completely escapes the logical boundaries of the NTFS_DE allocation frame.
The preexisting precondition filters (check_if_index_root and check_if_root_index) ensure structural alignment of record layouts but fail to check fields localized inside the targeted record.
The driver tracks the logical entry boundary using e->size, but fails to use it as a safety constraint during the memory modification path. No validation ensures that:
data_off <= e->size && dlen <= e->size - data_off
A minimal malformed replay log context to hit the sink requires the following metadata mappings:
lrh->record_off = [Valid ATTR_ROOT offset] lrh->attr_off = [Valid NTFS_DE boundary alignment] e->view.data_off = 0x7FFE dlen = [Attacker-defined byte length] Execution output: e = Add2Ptr(attr, lrh->attr_off) memmove(Add2Ptr(e, 0x7FFE), attacker_payload, dlen) -> Out-of-Bounds Heap Write
An identical secondary instance of this exact structural bypass is reachable via the UpdateRecordDataAllocation branch:
e = Add2Ptr(hdr, le32_to_cpu(aoff));
memmove(
Add2Ptr(e, le16_to_cpu(e->view.data_off)),
data,
dlen
);
Enforce rigid entry bounds validation constraints inside both functional branches before invoking memory writes:
/* UpdateRecordDataRoot Case */
e = Add2Ptr(attr, le16_to_cpu(lrh->attr_off));
{
u32 off = le16_to_cpu(e->view.data_off);
u32 size = le16_to_cpu(e->size);
if (off > size || dlen > size - off)
goto dirty_vol;
}
memmove(Add2Ptr(e, le16_to_cpu(e->view.data_off)), data, dlen);
/* UpdateRecordDataAllocation Case */
e = Add2Ptr(hdr, le32_to_cpu(aoff));
{
u32 off = le16_to_cpu(e->view.data_off);
u32 size = le16_to_cpu(e->size);
if (off > size || dlen > size - off)
goto dirty_vol;
}
memmove(Add2Ptr(e, le16_to_cpu(e->view.data_off)), data, dlen);
An unvalidated length argument in fs/ntfs3/fslog.c leads to an out-of-bounds read and write via an attacker-controlled lcns_follow field. This occurs during version 0 to version 1 conversion of the dirty page table inside log_replay().
The issue resides in on-disk filesystem metadata parsing, where an inline code warning left by the developer explicitly notes: // NOTE: Danger. Check for of boundary. but was left unhandled in production code.
When the driver encounters a version 0 $LogFile journal restart area during a mount-time log recovery sequence, it performs an in-place mutation of DIR_PAGE_ENTRY_32 structures into DIR_PAGE_ENTRY layouts inside the allocated dptbl heap buffer:
dp = NULL;
while ((dp = enum_rstbl(dptbl, dp))) {
struct DIR_PAGE_ENTRY_32 *dp0 = (struct DIR_PAGE_ENTRY_32 *)dp;
// NOTE: Danger. Check for of boundary.
memmove(&dp->vcn, &dp0->vcn_low,
2 * sizeof(u64) +
le32_to_cpu(dp->lcns_follow) * sizeof(u64));
}
The lcns_follow field dictates the size of the trailing flexible array member. Because it is read straight from the raw partition without validation, providing an inflated value like 0x1FFFFFFF yields a memmove size parameter calculation totaling 0x100000010 bytes (~4GB).
The check_rstbl() sanity checker ensures the overarching RESTART_TABLE dimensions line up with raw image bounds, but it ignores interior field bounds testing for lcns_follow.
The kernel contains an internal mathematical equation used during the native allocation path to calculate safe boundaries:
max_lcns = (le16_to_cpu(dptbl->size) - sizeof(struct DIR_PAGE_ENTRY)) / sizeof(u64);
This formula is never applied to validate the on-disk structural field boundaries prior to executing the data movement loop.
The unvalidated lcns_follow integer persists within the entry object into downstream analysis routines. In the later DeleteDirtyClusters execution phase, the driver loops using the unvalidated tracker variable directly as an array index limit:
while ((dp = enum_rstbl(dptbl, dp))) {
u32 j;
t32 = le32_to_cpu(dp->lcns_follow);
for (j = 0; j < t32; j++) {
t64 = le64_to_cpu(dp->page_lcns[j]); /* OOB Read */
if (t64 >= lcn0 && t64 <= lcn_e)
dp->page_lcns[j] = 0; /* OOB Write */
}
}
This yields a distinct, secondary out-of-bounds read and write primitive within the same functional chain without requiring additional malformed payload segments.
dptbl buffer.Validate the field using the kernel's existing structure bounds equation before triggering data block transformations:
u32 max_lcns = (le16_to_cpu(dptbl->size) - sizeof(struct DIR_PAGE_ENTRY)) / sizeof(u64);
dp = NULL;
while ((dp = enum_rstbl(dptbl, dp))) {
struct DIR_PAGE_ENTRY_32 *dp0 = (struct DIR_PAGE_ENTRY_32 *)dp;
u32 lcns = le32_to_cpu(dp->lcns_follow);
if (lcns > max_lcns)
goto dirty_vol;
memmove(&dp->vcn, &dp0->vcn_low, 2 * sizeof(u64) + lcns * sizeof(u64));
}
All Linux kernel versions from version 5.15 onwards (matching the initial integration of Paragon's ntfs3 implementation) where CONFIG_NTFS3_FS is compiled as a built-in or module component.
Both issues were assigned CVE identifiers by the Linux kernel CNA following backport into released stable trees:
data_off out-of-bounds write (UpdateRecordDataRoot / UpdateRecordDataAllocation), fix commit 3e127829e57flcns_follow out-of-bounds read/write (log_replay conversion), fix commit 6a4c53a2e26aBoth fixes are in Linus's tree and backported to stable branches 5.15, 6.1, 6.6, 6.12, 6.18, and 7.1.
The vulnerabilities were reported upstream through the Linux Kernel Mailing List (LKML), followed by patch review, maintainer discussion, and integration into the upstream kernel tree.
LKML Activity & Patch History: lore.kernel.org/all/?q=Pavitra+Jha
Pavitra Jha
Offensive Security Researcher
Kernel / Driver Exploitation Research