#include // Native read-only NTFS filesystem driver for NTLite. // Portions are based on ntfs-efi by Mark Harmstone (2023), GPLv2-or-later. // The port removes UEFI protocol/header dependencies and targets NTLite's IO_VOLUME_PROVIDER ABI. #include #include #include #include #include #include #include #include #include #include #include #include #define NTFS_SECTOR_SIZE 512u #define NTFS_FS_NAME "NTFS " #define NTFS_UPCASE_TABLE_CHARS 0x10000u #define NTFS_UPCASE_TABLE_BYTES (NTFS_UPCASE_TABLE_CHARS * sizeof(uint16_t)) #ifndef NTFS_TRACE_ENABLED #define NTFS_TRACE_ENABLED 0 #endif #if NTFS_TRACE_ENABLED #define NtfsTrace DbgPrint #else #define NtfsTrace(...) ((void)0) #endif #define NTFS_MFT_INODE 0ull #define NTFS_ROOT_DIR_INODE 5ull #define NTFS_UPCASE_INODE 10ull #define NTFS_FILE_SIGNATURE 0x454c4946u /* FILE */ #define NTFS_INDEX_SIGNATURE 0x58444e49u /* INDX */ #define NTFS_ATTR_STANDARD_INFORMATION 0x10u #define NTFS_ATTR_ATTRIBUTE_LIST 0x20u #define NTFS_ATTR_FILE_NAME 0x30u #define NTFS_ATTR_DATA 0x80u #define NTFS_ATTR_INDEX_ROOT 0x90u #define NTFS_ATTR_INDEX_ALLOCATION 0xA0u #define NTFS_ATTR_END 0xffffffffu #define NTFS_FORM_RESIDENT 0u #define NTFS_FORM_NONRESIDENT 1u #define NTFS_ATTRIBUTE_FLAG_COMPRESSION_MASK 0x00ffu #define NTFS_ATTRIBUTE_FLAG_ENCRYPTED 0x4000u #define NTFS_FILE_ATTRIBUTE_READONLY 0x00000001u #define NTFS_FILE_ATTRIBUTE_HIDDEN 0x00000002u #define NTFS_FILE_ATTRIBUTE_SYSTEM 0x00000004u #define NTFS_FILE_ATTRIBUTE_DIRECTORY 0x00000010u #define NTFS_FILE_ATTRIBUTE_ARCHIVE 0x00000020u #define NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT 0x10000000u #define NTFS_INDEX_ENTRY_SUBNODE 1u #define NTFS_INDEX_ENTRY_LAST 2u #define NTFS_FILE_NAME_POSIX 0u #define NTFS_FILE_NAME_WINDOWS 1u #define NTFS_FILE_NAME_DOS 2u #define NTFS_FILE_NAME_WINDOWS_AND_DOS 3u #pragma pack(push, 1) typedef struct NTFS_BOOT_SECTOR { uint8_t Jmp[3]; uint8_t FsName[8]; uint16_t BytesPerSector; uint8_t SectorsPerCluster; uint16_t ReservedSectors; uint8_t Unused1[5]; uint8_t Media; uint8_t Unused2[2]; uint16_t SectorsPerTrack; uint16_t Heads; uint32_t HiddenSectors; uint32_t Unused3; uint32_t Unknown; uint64_t TotalSectors; uint64_t MFT; uint64_t MFTMirr; int8_t ClustersPerMFTRecord; uint8_t Padding1[3]; int8_t ClustersPerIndexRecord; uint8_t Padding2[3]; uint64_t SerialNumber; uint32_t Checksum; } NTFS_BOOT_SECTOR; typedef struct NTFS_MULTI_SECTOR_HEADER { uint32_t Signature; uint16_t UpdateSequenceArrayOffset; uint16_t UpdateSequenceArraySize; } NTFS_MULTI_SECTOR_HEADER; typedef struct NTFS_FILE_RECORD_SEGMENT_HEADER { NTFS_MULTI_SECTOR_HEADER MultiSectorHeader; uint64_t LogFileSequenceNumber; uint16_t SequenceNumber; uint16_t HardLinkCount; uint16_t FirstAttributeOffset; uint16_t Flags; uint32_t EntryUsedSize; uint32_t EntryAllocatedSize; uint64_t BaseFileRecordSegment; uint16_t NextAttributeID; } NTFS_FILE_RECORD_SEGMENT_HEADER; typedef struct NTFS_ATTRIBUTE_RECORD_HEADER { uint32_t TypeCode; uint16_t RecordLength; uint16_t RecordLengthHigh; uint8_t FormCode; uint8_t NameLength; uint16_t NameOffset; uint16_t Flags; uint16_t Instance; union { struct { uint32_t ValueLength; uint16_t ValueOffset; uint8_t Reserved[2]; } Resident; struct { uint64_t LowestVcn; uint64_t HighestVcn; uint16_t MappingPairsOffset; uint16_t CompressionUnit; uint32_t Padding; uint64_t AllocatedLength; uint64_t FileSize; uint64_t ValidDataLength; uint64_t TotalAllocated; } Nonresident; } Form; } NTFS_ATTRIBUTE_RECORD_HEADER; typedef struct NTFS_STANDARD_INFORMATION { int64_t CreationTime; int64_t LastAccessTime; int64_t LastWriteTime; int64_t ChangeTime; uint32_t FileAttributes; uint32_t MaximumVersions; uint32_t VersionNumber; uint32_t ClassId; uint32_t OwnerId; uint32_t SecurityId; uint64_t QuotaCharged; uint64_t USN; } NTFS_STANDARD_INFORMATION; typedef struct NTFS_INDEX_NODE_HEADER { uint32_t FirstEntry; uint32_t TotalSize; uint32_t AllocatedSize; uint32_t Flags; } NTFS_INDEX_NODE_HEADER; typedef struct NTFS_INDEX_ENTRY { uint64_t FileReference; uint16_t EntryLength; uint16_t StreamLength; uint32_t Flags; } NTFS_INDEX_ENTRY; typedef struct NTFS_INDEX_ROOT { uint32_t AttributeType; uint32_t CollationRule; uint32_t BytesPerIndexRecord; uint8_t ClustersPerIndexRecord; uint8_t Padding[3]; NTFS_INDEX_NODE_HEADER NodeHeader; } NTFS_INDEX_ROOT; typedef struct NTFS_INDEX_RECORD { NTFS_MULTI_SECTOR_HEADER MultiSectorHeader; uint64_t SequenceNumber; uint64_t Vcn; NTFS_INDEX_NODE_HEADER Header; uint16_t UpdateSequence; } NTFS_INDEX_RECORD; typedef struct NTFS_FILE_NAME_ATTR { uint64_t Parent; int64_t CreationTime; int64_t LastAccessTime; int64_t LastWriteTime; int64_t ChangeTime; uint64_t AllocationSize; uint64_t EndOfFile; uint32_t FileAttributes; uint32_t EaSize; uint8_t FileNameLength; uint8_t Namespace; uint16_t FileName[1]; } NTFS_FILE_NAME_ATTR; #pragma pack(pop) typedef struct NTFS_RUN { LIST_ENTRY Link; uint64_t Vcn; uint64_t Lcn; uint64_t Length; bool Sparse; } NTFS_RUN; typedef struct NTFS_VOLUME { uint8_t DriveIndex; uint64_t PartitionLba; uint64_t PartitionSize; uint64_t SerialNumber; uint32_t BytesPerSector; uint32_t SectorsPerCluster; uint32_t BytesPerCluster; uint64_t TotalSectors; uint64_t MftStartCluster; uint64_t FileRecordSize; LIST_ENTRY MftRuns; uint16_t* Upcase; IO_VOLUME* Volume; // Active mount-list reference. LIST_ENTRY MountLink; } NTFS_VOLUME; typedef struct NTFS_FILE { NTFS_VOLUME* Vol; uint64_t Inode; uint64_t Size; uint64_t ValidDataLength; uint64_t PhysicalSize; uint32_t Attributes; bool IsDirectory; NTFS_STANDARD_INFORMATION StandardInfo; LIST_ENTRY DataRuns; LIST_ENTRY IndexRuns; uint8_t* ResidentData; size_t ResidentDataSize; NTFS_INDEX_ROOT* IndexRoot; size_t IndexRootSize; } NTFS_FILE; typedef struct NTFS_DIRENT_CACHE { uint64_t Inode; uint64_t FileSize; uint64_t AllocSize; uint32_t Attributes; int64_t CreationTime; int64_t LastAccessTime; int64_t LastWriteTime; char Name[SYS_DIR_NAME_MAX]; } NTFS_DIRENT_CACHE; typedef struct NTFS_DIR_CTX { NTFS_DIRENT_CACHE* Entries; uint32_t Count; uint32_t Capacity; uint32_t Position; } NTFS_DIR_CTX; typedef struct NTFS_ATTR_VIEW { const NTFS_ATTRIBUTE_RECORD_HEADER* Attr; const uint8_t* ResidentData; size_t ResidentSize; } NTFS_ATTR_VIEW; typedef struct NTFS_FIND_CTX { NTFS_VOLUME* Vol; const uint16_t* Name; size_t NameLength; uint64_t Inode; bool Found; } NTFS_FIND_CTX; typedef struct NTFS_COLLECT_CTX { NTFS_VOLUME* Vol; NTFS_DIR_CTX* Dir; KSTATUS Status; bool HideNtfsMetadata; } NTFS_COLLECT_CTX; static LIST_ENTRY gNtfsMounts; static uint32_t gMountedCount; static MUTEX gNtfsMountMutex; static SPINLOCK gNtfsMountMutexInitLock = SPINLOCK_INIT; static int gNtfsMountMutexReady; static uint64_t NtfsAlignUp(uint64_t value, uint64_t alignment) { if (alignment == 0) return value; uint64_t rem = value % alignment; return rem ? value + (alignment - rem) : value; } static bool NtfsIsPowerOfTwo32(uint32_t value) { return value != 0 && (value & (value - 1u)) == 0; } static uint64_t NtfsFileReferenceNumber(uint64_t reference) { return reference & 0x0000ffffffffffffull; } static void NtfsInitRunList(LIST_ENTRY* list) { LIST_INIT(list); } static void NtfsFreeRuns(LIST_ENTRY* list) { while (!LIST_IS_EMPTY(list)) { LIST_ENTRY* node = ListPopHead(list); NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link); MmFreePool(run); } } static void NtfsDebugDumpRuns(const char* tag, const LIST_ENTRY* runs) { if (!tag || !runs) return; uint32_t i = 0; for (LIST_ENTRY* node = runs->Fwd; node != runs && i < 16u; node = node->Fwd, i++) { NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link); NtfsTrace("NTFS: %s run[%u]: vcn=%llu lcn=%llu len=%llu sparse=%u\n", tag, i, (unsigned long long)run->Vcn, (unsigned long long)run->Lcn, (unsigned long long)run->Length, run->Sparse ? 1u : 0u); } if (i == 0) NtfsTrace("NTFS: %s runlist empty\n", tag); } static KSTATUS NtfsReadSectors(NTFS_VOLUME* vol, uint64_t lba, uint32_t count, void* buffer) { if (!vol || !buffer || count == 0) return NTLSTATUS_INVALID_PARAMETER; if (vol->PartitionSize && lba >= vol->PartitionSize) return NTLSTATUS_DEVICE_ERROR; if (vol->PartitionSize && (uint64_t)count > vol->PartitionSize - lba) return NTLSTATUS_DEVICE_ERROR; return IoReadDiskSectors(vol->DriveIndex, vol->PartitionLba + lba, count, buffer); } static KSTATUS NtfsReadBytesDirect(NTFS_VOLUME* vol, uint64_t byteOffset, uint64_t byteCount, void* buffer) { if (!vol || !buffer) return NTLSTATUS_INVALID_PARAMETER; if ((byteOffset % vol->BytesPerSector) != 0 || (byteCount % vol->BytesPerSector) != 0) return NTLSTATUS_INVALID_PARAMETER; uint64_t lba = byteOffset / vol->BytesPerSector; uint64_t sectors = byteCount / vol->BytesPerSector; if (sectors > 0xffffffffull) return NTLSTATUS_INVALID_PARAMETER; return NtfsReadSectors(vol, lba, (uint32_t)sectors, buffer); } static KSTATUS NtfsProcessFixups(NTFS_MULTI_SECTOR_HEADER* header, uint64_t length, uint32_t sectorSize) { if (!header || sectorSize == 0 || length < sectorSize) return NTLSTATUS_INVALID_PARAMETER; uint64_t sectors = length / sectorSize; if (header->UpdateSequenceArraySize < sectors + 1) return NTLSTATUS_INVALID_PARAMETER; if (header->UpdateSequenceArrayOffset + (uint64_t)header->UpdateSequenceArraySize * sizeof(uint16_t) > length) return NTLSTATUS_INVALID_PARAMETER; uint16_t* seq = (uint16_t*)((uint8_t*)header + header->UpdateSequenceArrayOffset); uint8_t* ptr = (uint8_t*)header + sectorSize - sizeof(uint16_t); for (uint64_t i = 0; i < sectors; i++) { if (*(uint16_t*)ptr != seq[0]) return NTLSTATUS_CHECKSUM_MISMATCH; *(uint16_t*)ptr = seq[i + 1u]; ptr += sectorSize; } return NTLSTATUS_SUCCESS; } static int64_t NtfsReadSignedLe(const uint8_t* p, uint8_t bytes) { uint64_t value = 0; for (uint8_t i = 0; i < bytes; i++) value |= ((uint64_t)p[i]) << (8u * i); if (bytes && (p[bytes - 1u] & 0x80u)) { for (uint8_t i = bytes; i < 8u; i++) value |= 0xffull << (8u * i); } return (int64_t)value; } static uint32_t NtfsAttributeRecordLength(const NTFS_ATTRIBUTE_RECORD_HEADER* attr) { if (!attr) return 0; return (uint32_t)attr->RecordLength | ((uint32_t)attr->RecordLengthHigh << 16); } static uint32_t NtfsAttributeMinimumLength(uint8_t formCode) { if (formCode == NTFS_FORM_RESIDENT) { return (uint32_t)(offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form.Resident.Reserved) + sizeof(((NTFS_ATTRIBUTE_RECORD_HEADER*)0)->Form.Resident.Reserved)); } if (formCode == NTFS_FORM_NONRESIDENT) { return (uint32_t)(offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form.Nonresident.ValidDataLength) + sizeof(((NTFS_ATTRIBUTE_RECORD_HEADER*)0)->Form.Nonresident.ValidDataLength)); } return (uint32_t)offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form); } static KSTATUS NtfsReadMappings(NTFS_VOLUME* vol, const NTFS_ATTRIBUTE_RECORD_HEADER* attr, LIST_ENTRY* runs) { if (!vol || !attr || !runs) return NTLSTATUS_INVALID_PARAMETER; if (attr->FormCode != NTFS_FORM_NONRESIDENT) return NTLSTATUS_INVALID_PARAMETER; if (attr->Flags & NTFS_ATTRIBUTE_FLAG_ENCRYPTED) return NTLSTATUS_NOT_SUPPORTED; if (attr->Flags & NTFS_ATTRIBUTE_FLAG_COMPRESSION_MASK) return NTLSTATUS_NOT_SUPPORTED; uint32_t attrLen = NtfsAttributeRecordLength(attr); if (attrLen < NtfsAttributeMinimumLength(NTFS_FORM_NONRESIDENT)) return NTLSTATUS_INVALID_PARAMETER; if (attr->Form.Nonresident.MappingPairsOffset >= attrLen) return NTLSTATUS_INVALID_PARAMETER; uint64_t limitBytes = attr->Form.Nonresident.AllocatedLength; if (limitBytes == 0) limitBytes = attr->Form.Nonresident.FileSize; if (limitBytes == 0) limitBytes = attr->Form.Nonresident.ValidDataLength; uint64_t maxCluster = NtfsAlignUp(limitBytes, vol->BytesPerCluster) / vol->BytesPerCluster; if (maxCluster == 0) return NTLSTATUS_SUCCESS; const uint8_t* stream = (const uint8_t*)attr + attr->Form.Nonresident.MappingPairsOffset; const uint8_t* end = (const uint8_t*)attr + attrLen; uint64_t nextVcn = attr->Form.Nonresident.LowestVcn; int64_t currentLcn = 0; while (stream < end && *stream != 0) { uint8_t header = *stream++; uint8_t lenBytes = header & 0x0fu; uint8_t lcnBytes = header >> 4; if (lenBytes == 0 || lenBytes > 8 || lcnBytes > 8) return NTLSTATUS_INVALID_PARAMETER; if ((size_t)(end - stream) < (size_t)lenBytes + (size_t)lcnBytes) return NTLSTATUS_INVALID_PARAMETER; int64_t runClustersSigned = NtfsReadSignedLe(stream, lenBytes); stream += lenBytes; if (runClustersSigned <= 0) return NTLSTATUS_INVALID_PARAMETER; uint64_t currentVcn = nextVcn; nextVcn += (uint64_t)runClustersSigned; if (nextVcn > maxCluster) nextVcn = maxCluster; NTFS_RUN* run = (NTFS_RUN*)MmAllocatePool(sizeof(NTFS_RUN)); if (!run) return NTLSTATUS_NO_MEMORY; memset(run, 0, sizeof(*run)); run->Vcn = currentVcn; run->Length = nextVcn - currentVcn; if (lcnBytes == 0) { run->Sparse = true; run->Lcn = 0; } else { currentLcn += NtfsReadSignedLe(stream, lcnBytes); stream += lcnBytes; if (currentLcn < 0) { MmFreePool(run); return NTLSTATUS_INVALID_PARAMETER; } run->Sparse = false; run->Lcn = (uint64_t)currentLcn; } ListInsertTail(runs, &run->Link); if (nextVcn >= maxCluster) break; } return NTLSTATUS_SUCCESS; } static KSTATUS NtfsReadFromRuns(NTFS_VOLUME* vol, const LIST_ENTRY* runs, uint64_t offset, void* buffer, uint64_t size) { if (!vol || !runs || (!buffer && size)) return NTLSTATUS_INVALID_PARAMETER; if (size == 0) return NTLSTATUS_SUCCESS; uint8_t* out = (uint8_t*)buffer; uint64_t clusterSize = vol->BytesPerCluster; uint64_t vcn = offset / clusterSize; uint64_t endOffset = offset + size; uint64_t lastVcn = NtfsAlignUp(endOffset, clusterSize) / clusterSize; for (LIST_ENTRY* node = runs->Fwd; node != runs && size; node = node->Fwd) { NTFS_RUN* run = CONTAINING_RECORD(node, NTFS_RUN, Link); if (run->Vcn >= lastVcn || run->Vcn + run->Length <= vcn) continue; uint64_t runByteStart = run->Vcn * clusterSize; uint64_t runByteEnd = (run->Vcn + run->Length) * clusterSize; if (offset < runByteStart) { uint64_t gap = runByteStart - offset; if (gap > size) gap = size; memset(out, 0, (size_t)gap); out += gap; offset += gap; size -= gap; vcn = offset / clusterSize; if (!size) break; } uint64_t chunkStart = offset > runByteStart ? offset : runByteStart; uint64_t chunkEnd = endOffset < runByteEnd ? endOffset : runByteEnd; if (chunkEnd <= chunkStart) continue; uint64_t chunkSize = chunkEnd - chunkStart; uint64_t mappingOffset = chunkStart - runByteStart; if (run->Sparse) { memset(out, 0, (size_t)chunkSize); } else { uint64_t byteOnVolume = run->Lcn * clusterSize + mappingOffset; KSTATUS s = NtfsReadBytesDirect(vol, byteOnVolume, chunkSize, out); if (!KSUCCESS(s)) return s; } out += chunkSize; offset += chunkSize; size -= chunkSize; vcn = offset / clusterSize; } if (size) memset(out, 0, (size_t)size); return NTLSTATUS_SUCCESS; } static bool NtfsAttrNameEqualsAscii(const NTFS_ATTRIBUTE_RECORD_HEADER* attr, const char* ascii) { if (!attr || !ascii) return false; size_t len = strlen(ascii); if (attr->NameLength != len) return false; if (attr->NameLength == 0) return len == 0; uint32_t attrLen = NtfsAttributeRecordLength(attr); uint32_t nameBytes = (uint32_t)attr->NameLength * sizeof(uint16_t); if (attr->NameOffset < offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form) || attr->NameOffset + nameBytes > attrLen) { return false; } const uint16_t* name = (const uint16_t*)((const uint8_t*)attr + attr->NameOffset); for (size_t i = 0; i < len; i++) { if (name[i] != (uint16_t)(uint8_t)ascii[i]) return false; } return true; } static KSTATUS NtfsForEachRecordAttribute(NTFS_VOLUME* vol, uint64_t inode, const NTFS_FILE_RECORD_SEGMENT_HEADER* record, bool (*callback)(const NTFS_ATTR_VIEW* view, void* ctx), void* ctx) { if (!vol || !record || !callback) return NTLSTATUS_INVALID_PARAMETER; uint32_t offset = record->FirstAttributeOffset; uint32_t recordBytes = record->EntryUsedSize ? record->EntryUsedSize : record->EntryAllocatedSize; if (recordBytes == 0 || recordBytes > vol->FileRecordSize) recordBytes = (uint32_t)vol->FileRecordSize; if (offset >= recordBytes || offset < sizeof(NTFS_FILE_RECORD_SEGMENT_HEADER)) { NtfsTrace("NTFS: attrs: inode=%llu bad firstAttr=%u recordBytes=%u fileRecordSize=%llu\n", (unsigned long long)inode, offset, recordBytes, (unsigned long long)vol->FileRecordSize); return NTLSTATUS_INVALID_PARAMETER; } while (offset + offsetof(NTFS_ATTRIBUTE_RECORD_HEADER, Form) <= recordBytes) { const NTFS_ATTRIBUTE_RECORD_HEADER* attr = (const NTFS_ATTRIBUTE_RECORD_HEADER*)((const uint8_t*)record + offset); if (attr->TypeCode == NTFS_ATTR_END) break; uint32_t attrLen = NtfsAttributeRecordLength(attr); uint32_t minAttrLen = NtfsAttributeMinimumLength(attr->FormCode); if (attrLen < minAttrLen || attrLen == 0) { NtfsTrace("NTFS: attrs: inode=%llu bad attr off=%u type=0x%x len=%u form=%u min=%u\n", (unsigned long long)inode, offset, attr->TypeCode, attrLen, attr->FormCode, minAttrLen); return NTLSTATUS_INVALID_PARAMETER; } if (offset + attrLen > recordBytes) { NtfsTrace("NTFS: attrs: inode=%llu attr overruns off=%u type=0x%x len=%u recordBytes=%u used=%u alloc=%u\n", (unsigned long long)inode, offset, attr->TypeCode, attrLen, recordBytes, record->EntryUsedSize, record->EntryAllocatedSize); return NTLSTATUS_INVALID_PARAMETER; } NTFS_ATTR_VIEW view; memset(&view, 0, sizeof(view)); view.Attr = attr; if (attr->FormCode == NTFS_FORM_RESIDENT) { if ((uint32_t)attr->Form.Resident.ValueOffset < minAttrLen || (uint32_t)attr->Form.Resident.ValueOffset + attr->Form.Resident.ValueLength > attrLen) { NtfsTrace("NTFS: attrs: inode=%llu resident bounds off=%u type=0x%x attrLen=%u valueOff=%u valueLen=%u\n", (unsigned long long)inode, offset, attr->TypeCode, attrLen, attr->Form.Resident.ValueOffset, attr->Form.Resident.ValueLength); return NTLSTATUS_INVALID_PARAMETER; } view.ResidentData = (const uint8_t*)attr + attr->Form.Resident.ValueOffset; view.ResidentSize = attr->Form.Resident.ValueLength; } else if (attr->FormCode != NTFS_FORM_NONRESIDENT) { NtfsTrace("NTFS: attrs: inode=%llu unsupported form=%u off=%u type=0x%x\n", (unsigned long long)inode, attr->FormCode, offset, attr->TypeCode); return NTLSTATUS_INVALID_PARAMETER; } if (!callback(&view, ctx)) break; offset += attrLen; } return NTLSTATUS_SUCCESS; } static KSTATUS NtfsValidateFileRecord(NTFS_VOLUME* vol, uint64_t inode, void* record, const char* source) { NTFS_FILE_RECORD_SEGMENT_HEADER* fr = (NTFS_FILE_RECORD_SEGMENT_HEADER*)record; if (fr->MultiSectorHeader.Signature != NTFS_FILE_SIGNATURE) { const uint8_t* b = (const uint8_t*)record; NtfsTrace("NTFS: record: inode=%llu %s bad signature=0x%x bytes=%02x %02x %02x %02x mftOff=%llu\n", (unsigned long long)inode, source ? source : "?", fr->MultiSectorHeader.Signature, b[0], b[1], b[2], b[3], (unsigned long long)(inode * vol->FileRecordSize)); return NTLSTATUS_INVALID_PARAMETER; } KSTATUS s = NtfsProcessFixups(&fr->MultiSectorHeader, vol->FileRecordSize, vol->BytesPerSector); if (!KSUCCESS(s)) { NtfsTrace("NTFS: record: inode=%llu %s fixup failed st=0x%x sig=0x%x usaOff=%u usaCount=%u recordSize=%llu sector=%u\n", (unsigned long long)inode, source ? source : "?", s, fr->MultiSectorHeader.Signature, fr->MultiSectorHeader.UpdateSequenceArrayOffset, fr->MultiSectorHeader.UpdateSequenceArraySize, (unsigned long long)vol->FileRecordSize, vol->BytesPerSector); return s; } return NTLSTATUS_SUCCESS; } static KSTATUS NtfsReadRecord(NTFS_VOLUME* vol, uint64_t inode, void* record) { if (!vol || !record) return NTLSTATUS_INVALID_PARAMETER; uint64_t mftOffset = inode * vol->FileRecordSize; KSTATUS s = NtfsReadFromRuns(vol, &vol->MftRuns, mftOffset, record, vol->FileRecordSize); if (!KSUCCESS(s)) { NtfsTrace("NTFS: record: inode=%llu runlist read failed st=0x%x mftOff=%llu size=%llu\n", (unsigned long long)inode, s, (unsigned long long)mftOffset, (unsigned long long)vol->FileRecordSize); NtfsDebugDumpRuns("mft", &vol->MftRuns); return s; } s = NtfsValidateFileRecord(vol, inode, record, "runlist"); if (KSUCCESS(s)) return s; /* Diagnostic and conservative rescue path: on a normal NTFS volume, early MFT records are contiguous from the boot-sector $MFT LCN. If the decoded runlist gives garbage, try that direct address and log whether it works. This also keeps the system usable while we narrow the runlist bug down. */ uint8_t* direct = (uint8_t*)MmAllocatePool((size_t)vol->FileRecordSize); if (!direct) return s; uint64_t directByte = vol->MftStartCluster * vol->BytesPerCluster + mftOffset; KSTATUS ds = NtfsReadBytesDirect(vol, directByte, vol->FileRecordSize, direct); if (KSUCCESS(ds)) ds = NtfsValidateFileRecord(vol, inode, direct, "boot-mft-direct"); else NtfsTrace("NTFS: record: inode=%llu direct read failed st=0x%x byte=%llu\n", (unsigned long long)inode, ds, (unsigned long long)directByte); if (KSUCCESS(ds)) { memcpy(record, direct, (size_t)vol->FileRecordSize); NtfsTrace("NTFS: record: inode=%llu used boot-sector $MFT direct fallback; decoded MFT runlist is suspect\n", (unsigned long long)inode); NtfsDebugDumpRuns("mft", &vol->MftRuns); MmFreePool(direct); return NTLSTATUS_SUCCESS; } MmFreePool(direct); return s; } static void NtfsInitFile(NTFS_FILE* file, NTFS_VOLUME* vol, uint64_t inode) { memset(file, 0, sizeof(*file)); file->Vol = vol; file->Inode = inode; NtfsInitRunList(&file->DataRuns); NtfsInitRunList(&file->IndexRuns); } static void NtfsCleanupFile(NTFS_FILE* file) { if (!file) return; NtfsFreeRuns(&file->DataRuns); NtfsFreeRuns(&file->IndexRuns); if (file->ResidentData) MmFreePool(file->ResidentData); if (file->IndexRoot) MmFreePool(file->IndexRoot); memset(file, 0, sizeof(*file)); } typedef struct NTFS_LOAD_FILE_CTX { NTFS_FILE* File; KSTATUS Status; } NTFS_LOAD_FILE_CTX; static bool NtfsLoadFileAttributeCallback(const NTFS_ATTR_VIEW* view, void* rawCtx) { NTFS_LOAD_FILE_CTX* ctx = (NTFS_LOAD_FILE_CTX*)rawCtx; NTFS_FILE* file = ctx->File; const NTFS_ATTRIBUTE_RECORD_HEADER* attr = view->Attr; switch (attr->TypeCode) { case NTFS_ATTR_STANDARD_INFORMATION: { if (attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentData) { size_t toCopy = view->ResidentSize < sizeof(file->StandardInfo) ? view->ResidentSize : sizeof(file->StandardInfo); memcpy(&file->StandardInfo, view->ResidentData, toCopy); file->Attributes = file->StandardInfo.FileAttributes; } break; } case NTFS_ATTR_FILE_NAME: { if (attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentSize >= offsetof(NTFS_FILE_NAME_ATTR, FileName)) { const NTFS_FILE_NAME_ATTR* fn = (const NTFS_FILE_NAME_ATTR*)view->ResidentData; file->Attributes = fn->FileAttributes; file->IsDirectory = (fn->FileAttributes & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT) != 0 || (fn->FileAttributes & NTFS_FILE_ATTRIBUTE_DIRECTORY) != 0; if (file->Size == 0) file->Size = fn->EndOfFile; if (file->PhysicalSize == 0) file->PhysicalSize = fn->AllocationSize; } break; } case NTFS_ATTR_DATA: { if (attr->NameLength != 0) break; if (attr->FormCode == NTFS_FORM_RESIDENT) { file->Size = view->ResidentSize; file->ValidDataLength = view->ResidentSize; file->PhysicalSize = view->ResidentSize; if (view->ResidentSize) { file->ResidentData = (uint8_t*)MmAllocatePool(view->ResidentSize); if (!file->ResidentData) { ctx->Status = NTLSTATUS_NO_MEMORY; return false; } memcpy(file->ResidentData, view->ResidentData, view->ResidentSize); file->ResidentDataSize = view->ResidentSize; } } else if (attr->FormCode == NTFS_FORM_NONRESIDENT) { file->Size = attr->Form.Nonresident.FileSize; file->ValidDataLength = attr->Form.Nonresident.ValidDataLength; file->PhysicalSize = attr->Form.Nonresident.AllocatedLength; ctx->Status = NtfsReadMappings(file->Vol, attr, &file->DataRuns); if (!KSUCCESS(ctx->Status)) return false; } break; } case NTFS_ATTR_INDEX_ROOT: { if (NtfsAttrNameEqualsAscii(attr, "$I30") && attr->FormCode == NTFS_FORM_RESIDENT && view->ResidentSize) { file->IndexRoot = (NTFS_INDEX_ROOT*)MmAllocatePool(view->ResidentSize); if (!file->IndexRoot) { ctx->Status = NTLSTATUS_NO_MEMORY; return false; } memcpy(file->IndexRoot, view->ResidentData, view->ResidentSize); file->IndexRootSize = view->ResidentSize; file->IsDirectory = true; } break; } case NTFS_ATTR_INDEX_ALLOCATION: { if (NtfsAttrNameEqualsAscii(attr, "$I30") && attr->FormCode == NTFS_FORM_NONRESIDENT) { ctx->Status = NtfsReadMappings(file->Vol, attr, &file->IndexRuns); if (!KSUCCESS(ctx->Status)) return false; } break; } case NTFS_ATTR_ATTRIBUTE_LIST: /* Complex attribute-list continuations are not expanded in this first native port. */ break; default: break; } return true; } static KSTATUS NtfsLoadFile(NTFS_VOLUME* vol, uint64_t inode, NTFS_FILE* file) { if (!vol || !file) return NTLSTATUS_INVALID_PARAMETER; NtfsInitFile(file, vol, inode); void* record = MmAllocatePool((size_t)vol->FileRecordSize); if (!record) return NTLSTATUS_NO_MEMORY; KSTATUS s = NtfsReadRecord(vol, inode, record); if (!KSUCCESS(s)) { MmFreePool(record); NtfsCleanupFile(file); return s; } NTFS_LOAD_FILE_CTX ctx; ctx.File = file; ctx.Status = NTLSTATUS_SUCCESS; s = NtfsForEachRecordAttribute(vol, inode, (const NTFS_FILE_RECORD_SEGMENT_HEADER*)record, NtfsLoadFileAttributeCallback, &ctx); MmFreePool(record); if (!KSUCCESS(s)) { NtfsCleanupFile(file); return s; } if (!KSUCCESS(ctx.Status)) { NtfsCleanupFile(file); return ctx.Status; } if (file->ValidDataLength == 0 && file->ResidentDataSize) file->ValidDataLength = file->ResidentDataSize; return NTLSTATUS_SUCCESS; } static KSTATUS NtfsReadFileData(NTFS_FILE* file, uint64_t offset, void* buffer, size_t size, size_t* bytesRead) { if (bytesRead) *bytesRead = 0; if (!file || !buffer || !bytesRead) return NTLSTATUS_INVALID_PARAMETER; if (file->IsDirectory) return NTLSTATUS_ACCESS_DENIED; if (offset >= file->Size || size == 0) return NTLSTATUS_SUCCESS; uint64_t remaining = file->Size - offset; if ((uint64_t)size > remaining) size = (size_t)remaining; if (file->ResidentData) { if (offset >= file->ResidentDataSize) { memset(buffer, 0, size); } else { size_t avail = file->ResidentDataSize - (size_t)offset; size_t copy = size < avail ? size : avail; memcpy(buffer, file->ResidentData + offset, copy); if (copy < size) memset((uint8_t*)buffer + copy, 0, size - copy); } *bytesRead = size; return NTLSTATUS_SUCCESS; } uint64_t validEnd = offset + size; if (validEnd > file->ValidDataLength) validEnd = file->ValidDataLength; if (offset >= file->ValidDataLength) { memset(buffer, 0, size); *bytesRead = size; return NTLSTATUS_SUCCESS; } uint64_t sectorSize = file->Vol->BytesPerSector; uint64_t startAligned = offset & ~(sectorSize - 1u); uint64_t endAligned = NtfsAlignUp(validEnd, sectorSize); uint8_t* tmp = NULL; uint8_t* ioBuffer = (uint8_t*)buffer; uint64_t ioSize = endAligned - startAligned; if (startAligned != offset || endAligned != validEnd) { tmp = (uint8_t*)MmAllocatePool((size_t)ioSize); if (!tmp) return NTLSTATUS_NO_MEMORY; ioBuffer = tmp; } KSTATUS s = NtfsReadFromRuns(file->Vol, &file->DataRuns, startAligned, ioBuffer, ioSize); if (!KSUCCESS(s)) { if (tmp) MmFreePool(tmp); return s; } if (tmp) { memcpy(buffer, tmp + (offset - startAligned), (size_t)(validEnd - offset)); MmFreePool(tmp); } if (validEnd < offset + size) { memset((uint8_t*)buffer + (validEnd - offset), 0, (size_t)(offset + size - validEnd)); } *bytesRead = size; return NTLSTATUS_SUCCESS; } static uint16_t NtfsUpcaseChar(NTFS_VOLUME* vol, uint16_t c) { if (vol && vol->Upcase) return vol->Upcase[c]; if (c >= (uint16_t)'a' && c <= (uint16_t)'z') return (uint16_t)(c - 32u); return c; } static int NtfsCompareUtf16Name(NTFS_VOLUME* vol, const uint16_t* a, size_t alen, const uint16_t* b, size_t blen) { size_t n = alen < blen ? alen : blen; for (size_t i = 0; i < n; i++) { uint16_t ca = NtfsUpcaseChar(vol, a[i]); uint16_t cb = NtfsUpcaseChar(vol, b[i]); if (ca < cb) return -1; if (ca > cb) return 1; } if (alen < blen) return -1; if (alen > blen) return 1; return 0; } static size_t NtfsUtf16ToAscii(const uint16_t* src, size_t len, char* dst, size_t dstSize) { if (!dst || dstSize == 0) return 0; size_t out = 0; for (size_t i = 0; i < len && out + 1 < dstSize; i++) { uint16_t c = src[i]; dst[out++] = (c >= 0x20u && c < 0x7fu) ? (char)c : '?'; } dst[out] = '\0'; return out; } static void NtfsWinTimeToSystemTime(int64_t win, SYS_SYSTEM_TIME* out) { memset(out, 0, sizeof(*out)); if (win <= 0) return; uint64_t unixSeconds = (uint64_t)(win / 10000000ll); if (unixSeconds >= 11644473600ull) unixSeconds -= 11644473600ull; else unixSeconds = 0; out->EpochSeconds = unixSeconds; out->Nanosecond = (uint32_t)((win % 10000000ll) * 100ll); out->Flags = SYS_TIME_FLAG_VALID; out->TimeZoneMinutes = SYS_TIME_UNSPECIFIED_TZ; uint64_t days = unixSeconds / 86400ull; uint32_t rem = (uint32_t)(unixSeconds % 86400ull); out->Hour = (uint8_t)(rem / 3600u); out->Minute = (uint8_t)((rem % 3600u) / 60u); out->Second = (uint8_t)(rem % 60u); out->DayOfWeek = (uint8_t)((days + 4u) % 7u); int64_t z = (int64_t)days + 719468ll; int64_t era = (z >= 0 ? z : z - 146096ll) / 146097ll; uint32_t doe = (uint32_t)(z - era * 146097ll); uint32_t yoe = (doe - doe / 1460u + doe / 36524u - doe / 146096u) / 365u; int64_t y = (int64_t)yoe + era * 400ll; uint32_t doy = doe - (365u * yoe + yoe / 4u - yoe / 100u); uint32_t mp = (5u * doy + 2u) / 153u; uint32_t d = doy - (153u * mp + 2u) / 5u + 1u; uint32_t m = mp + (mp < 10u ? 3u : (uint32_t)-9); y += (m <= 2u); out->Year = (uint16_t)y; out->Month = (uint8_t)m; out->Day = (uint8_t)d; } static uint32_t NtfsSysAttrsFromNtfs(uint32_t attr, bool isDir) { uint32_t out = 0; if (isDir || (attr & NTFS_FILE_ATTRIBUTE_DIRECTORY) || (attr & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT)) out |= SYS_FILE_ATTR_DIRECTORY; if (attr & NTFS_FILE_ATTRIBUTE_READONLY) out |= SYS_FILE_ATTR_READ_ONLY; if (attr & NTFS_FILE_ATTRIBUTE_HIDDEN) out |= SYS_FILE_ATTR_HIDDEN; if (attr & NTFS_FILE_ATTRIBUTE_SYSTEM) out |= SYS_FILE_ATTR_SYSTEM; if (attr & NTFS_FILE_ATTRIBUTE_ARCHIVE) out |= SYS_FILE_ATTR_ARCHIVE; return out; } static void NtfsFillStat(NTFS_FILE* file, SYS_FILE_STAT* out) { memset(out, 0, sizeof(*out)); out->Size = sizeof(*out); out->Type = file->IsDirectory ? SYS_FILE_TYPE_DIRECTORY : SYS_FILE_TYPE_REGULAR; out->Attributes = NtfsSysAttrsFromNtfs(file->Attributes, file->IsDirectory); out->LinkCount = 1; out->FileSize = file->Size; out->FileId = file->Inode; NtfsWinTimeToSystemTime(file->StandardInfo.CreationTime, &out->CreationTime); NtfsWinTimeToSystemTime(file->StandardInfo.LastAccessTime, &out->LastAccessTime); NtfsWinTimeToSystemTime(file->StandardInfo.LastWriteTime, &out->LastWriteTime); NtfsWinTimeToSystemTime(file->StandardInfo.ChangeTime, &out->ChangeTime); } typedef bool (*NTFS_INDEX_ENTRY_CALLBACK)(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* ctx); static KSTATUS NtfsWalkIndexBuffer(NTFS_VOLUME* vol, NTFS_INDEX_ROOT* root, const LIST_ENTRY* indexRuns, const NTFS_INDEX_NODE_HEADER* nodeHeader, const uint8_t* nodeBase, size_t nodeBytes, NTFS_INDEX_ENTRY_CALLBACK callback, void* ctx, uint32_t depth) { if (!vol || !root || !nodeHeader || !nodeBase || !callback) return NTLSTATUS_INVALID_PARAMETER; if (depth > 16u) return NTLSTATUS_INVALID_PARAMETER; if (nodeHeader->FirstEntry > nodeBytes) { NtfsTrace("NTFS: index walk: firstEntry=%u nodeBytes=%llu depth=%u\n", nodeHeader->FirstEntry, (unsigned long long)nodeBytes, depth); return NTLSTATUS_INVALID_PARAMETER; } uint32_t offset = nodeHeader->FirstEntry; uint32_t limit = nodeHeader->TotalSize; if (limit == 0 || limit > nodeBytes) limit = (uint32_t)nodeBytes; while (offset + sizeof(NTFS_INDEX_ENTRY) <= limit) { const NTFS_INDEX_ENTRY* ent = (const NTFS_INDEX_ENTRY*)(nodeBase + offset); if (ent->EntryLength < sizeof(NTFS_INDEX_ENTRY) || offset + ent->EntryLength > limit) { NtfsTrace("NTFS: index walk: bad entry off=%u len=%u limit=%u flags=0x%x depth=%u\n", offset, ent->EntryLength, limit, ent->Flags, depth); return NTLSTATUS_INVALID_PARAMETER; } if (!(ent->Flags & NTFS_INDEX_ENTRY_LAST) && ent->StreamLength >= offsetof(NTFS_FILE_NAME_ATTR, FileName)) { const NTFS_FILE_NAME_ATTR* fn = (const NTFS_FILE_NAME_ATTR*)((const uint8_t*)ent + sizeof(NTFS_INDEX_ENTRY)); size_t needed = offsetof(NTFS_FILE_NAME_ATTR, FileName) + (size_t)fn->FileNameLength * sizeof(uint16_t); if (needed <= ent->StreamLength && fn->Namespace != NTFS_FILE_NAME_DOS) { if (!callback(fn, NtfsFileReferenceNumber(ent->FileReference), ctx)) return NTLSTATUS_SUCCESS; } } if (ent->Flags & NTFS_INDEX_ENTRY_SUBNODE) { if (ent->EntryLength < sizeof(NTFS_INDEX_ENTRY) + sizeof(uint64_t)) return NTLSTATUS_INVALID_PARAMETER; if (!indexRuns || LIST_IS_EMPTY(indexRuns)) { NtfsTrace("NTFS: index walk: subnode present but no $INDEX_ALLOCATION runs depth=%u off=%u\n", depth, offset); return NTLSTATUS_NOT_SUPPORTED; } uint64_t childVcn = *(const uint64_t*)((const uint8_t*)ent + ent->EntryLength - sizeof(uint64_t)); uint64_t childOffset; if (root->BytesPerIndexRecord < vol->BytesPerCluster) childOffset = childVcn * vol->BytesPerSector; else childOffset = childVcn * (uint64_t)vol->BytesPerCluster; uint8_t* record = (uint8_t*)MmAllocatePool(root->BytesPerIndexRecord); if (!record) return NTLSTATUS_NO_MEMORY; KSTATUS s = NtfsReadFromRuns(vol, indexRuns, childOffset, record, root->BytesPerIndexRecord); if (KSUCCESS(s)) { NTFS_INDEX_RECORD* ir = (NTFS_INDEX_RECORD*)record; if (ir->MultiSectorHeader.Signature != NTFS_INDEX_SIGNATURE) { NtfsTrace("NTFS: index walk: child VCN=%llu offset=%llu signature=0x%x, expected INDX\n", (unsigned long long)childVcn, (unsigned long long)childOffset, ir->MultiSectorHeader.Signature); s = NTLSTATUS_INVALID_PARAMETER; } else s = NtfsProcessFixups(&ir->MultiSectorHeader, root->BytesPerIndexRecord, vol->BytesPerSector); if (KSUCCESS(s)) { const uint8_t* recNodeBase = (const uint8_t*)&ir->Header; size_t recNodeBytes = root->BytesPerIndexRecord - offsetof(NTFS_INDEX_RECORD, Header); s = NtfsWalkIndexBuffer(vol, root, indexRuns, &ir->Header, recNodeBase, recNodeBytes, callback, ctx, depth + 1u); } } MmFreePool(record); if (!KSUCCESS(s)) { NtfsTrace("NTFS: index walk: child VCN=%llu failed st=0x%x depth=%u\n", (unsigned long long)childVcn, s, depth); return s; } } if (ent->Flags & NTFS_INDEX_ENTRY_LAST) break; offset += ent->EntryLength; } return NTLSTATUS_SUCCESS; } static KSTATUS NtfsWalkDirectory(NTFS_FILE* dir, NTFS_INDEX_ENTRY_CALLBACK callback, void* ctx) { if (!dir || !dir->IsDirectory || !dir->IndexRoot || !callback) return NTLSTATUS_INVALID_PARAMETER; if (dir->IndexRootSize < sizeof(NTFS_INDEX_ROOT)) return NTLSTATUS_INVALID_PARAMETER; const uint8_t* nodeBase = (const uint8_t*)&dir->IndexRoot->NodeHeader; size_t nodeBytes = dir->IndexRootSize - offsetof(NTFS_INDEX_ROOT, NodeHeader); return NtfsWalkIndexBuffer(dir->Vol, dir->IndexRoot, &dir->IndexRuns, &dir->IndexRoot->NodeHeader, nodeBase, nodeBytes, callback, ctx, 0u); } static bool NtfsFindEntryCallback(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* rawCtx) { NTFS_FIND_CTX* ctx = (NTFS_FIND_CTX*)rawCtx; if (NtfsCompareUtf16Name(ctx->Vol, ctx->Name, ctx->NameLength, fn->FileName, fn->FileNameLength) == 0) { ctx->Inode = inode; ctx->Found = true; return false; } return true; } static KSTATUS NtfsFindInDirectory(NTFS_VOLUME* vol, uint64_t dirInode, const uint16_t* name, size_t nameLength, uint64_t* outInode) { if (!vol || !name || !nameLength || !outInode) return NTLSTATUS_INVALID_PARAMETER; NTFS_FILE dir; KSTATUS s = NtfsLoadFile(vol, dirInode, &dir); if (!KSUCCESS(s)) return s; if (!dir.IsDirectory) { NtfsCleanupFile(&dir); return NTLSTATUS_NOT_FOUND; } NTFS_FIND_CTX ctx; memset(&ctx, 0, sizeof(ctx)); ctx.Vol = vol; ctx.Name = name; ctx.NameLength = nameLength; s = NtfsWalkDirectory(&dir, NtfsFindEntryCallback, &ctx); NtfsCleanupFile(&dir); if (!KSUCCESS(s)) return s; if (!ctx.Found) return NTLSTATUS_NOT_FOUND; *outInode = ctx.Inode; return NTLSTATUS_SUCCESS; } static KSTATUS NtfsLookupPath(NTFS_VOLUME* vol, SYS_STRING_VIEW path, uint64_t* outInode) { if (!vol || !outInode || !SysSvIsValid(path)) return NTLSTATUS_INVALID_PARAMETER; uint64_t inode = NTFS_ROOT_DIR_INODE; size_t pos = 0; while (pos < path.Length && (path.Data[pos] == '\\' || path.Data[pos] == '/')) pos++; if (pos >= path.Length) { *outInode = inode; return NTLSTATUS_SUCCESS; } while (pos < path.Length) { while (pos < path.Length && (path.Data[pos] == '\\' || path.Data[pos] == '/')) pos++; if (pos >= path.Length) break; size_t start = pos; while (pos < path.Length && path.Data[pos] != '\\' && path.Data[pos] != '/') pos++; size_t len = pos - start; if (len == 0) continue; if (len == 1 && path.Data[start] == '.') continue; if (len == 2 && path.Data[start] == '.' && path.Data[start + 1u] == '.') return NTLSTATUS_INVALID_PARAMETER; if (len > 255u) return NTLSTATUS_INVALID_PARAMETER; uint16_t segment[260]; for (size_t i = 0; i < len; i++) segment[i] = (uint16_t)(uint8_t)path.Data[start + i]; KSTATUS s = NtfsFindInDirectory(vol, inode, segment, len, &inode); if (!KSUCCESS(s)) return s; } *outInode = inode; return NTLSTATUS_SUCCESS; } static bool NtfsShouldHideDirectoryEntry(const NTFS_FILE_NAME_ATTR* fn, bool hideNtfsMetadata) { if (!fn) return true; if (fn->FileNameLength == 0) return true; if (fn->FileNameLength == 1 && fn->FileName[0] == (uint16_t)'.') return true; /* The NTFS root directory exposes metadata files such as $MFT, $Bitmap, $LogFile, and $Extend. They are filesystem implementation details, not normal user directory entries, so keep them out of directory listings. Lookup still sees the raw index when the driver needs metadata internally. */ if (hideNtfsMetadata && fn->FileName[0] == (uint16_t)'$') return true; return false; } static bool NtfsCollectEntryCallback(const NTFS_FILE_NAME_ATTR* fn, uint64_t inode, void* rawCtx) { NTFS_COLLECT_CTX* ctx = (NTFS_COLLECT_CTX*)rawCtx; NTFS_DIR_CTX* dir = ctx->Dir; if (NtfsShouldHideDirectoryEntry(fn, ctx->HideNtfsMetadata)) return true; if (dir->Count == dir->Capacity) { uint32_t newCap = dir->Capacity ? dir->Capacity * 2u : 32u; if (newCap < dir->Capacity) { ctx->Status = NTLSTATUS_NO_MEMORY; return false; } NTFS_DIRENT_CACHE* newer = (NTFS_DIRENT_CACHE*)MmReallocatePool(dir->Entries, (size_t)newCap * sizeof(NTFS_DIRENT_CACHE)); if (!newer) { ctx->Status = NTLSTATUS_NO_MEMORY; return false; } dir->Entries = newer; dir->Capacity = newCap; } NTFS_DIRENT_CACHE* e = &dir->Entries[dir->Count++]; memset(e, 0, sizeof(*e)); e->Inode = inode; e->FileSize = fn->EndOfFile; e->AllocSize = fn->AllocationSize; e->Attributes = fn->FileAttributes; e->CreationTime = fn->CreationTime; e->LastAccessTime = fn->LastAccessTime; e->LastWriteTime = fn->LastWriteTime; NtfsUtf16ToAscii(fn->FileName, fn->FileNameLength, e->Name, sizeof(e->Name)); ctx->Status = NTLSTATUS_SUCCESS; return true; } static KSTATUS NtfsOpen(IO_VOLUME_PROVIDER* fs, SYS_STRING_VIEW path, void** fileCtx) { if (!fs || !fileCtx || !SysSvIsValid(path)) return NTLSTATUS_INVALID_PARAMETER; NTFS_VOLUME* vol = (NTFS_VOLUME*)fs->Context; if (!vol) return NTLSTATUS_INVALID_PARAMETER; uint64_t inode; KSTATUS s = NtfsLookupPath(vol, path, &inode); if (!KSUCCESS(s)) return s; NTFS_FILE* file = (NTFS_FILE*)MmAllocatePool(sizeof(NTFS_FILE)); if (!file) return NTLSTATUS_NO_MEMORY; s = NtfsLoadFile(vol, inode, file); if (!KSUCCESS(s)) { MmFreePool(file); return s; } *fileCtx = file; return NTLSTATUS_SUCCESS; } static void NtfsClose(IO_VOLUME_PROVIDER* fs, void* fileCtx) { (void)fs; NTFS_FILE* file = (NTFS_FILE*)fileCtx; if (!file) return; NtfsCleanupFile(file); MmFreePool(file); } static KSTATUS NtfsRead(IO_VOLUME_PROVIDER* fs, void* fileCtx, uint64_t offset, void* buf, size_t size, size_t* bytesRead) { (void)fs; return NtfsReadFileData((NTFS_FILE*)fileCtx, offset, buf, size, bytesRead); } static KSTATUS NtfsWrite(IO_VOLUME_PROVIDER* fs, void* fileCtx, uint64_t offset, const void* buf, size_t size, size_t* bytesWritten) { (void)fs; (void)fileCtx; (void)offset; (void)buf; (void)size; if (bytesWritten) *bytesWritten = 0; return NTLSTATUS_NOT_SUPPORTED; } static KSTATUS NtfsCreate(IO_VOLUME_PROVIDER* fs, SYS_STRING_VIEW path, void** fileCtx) { (void)fs; (void)path; (void)fileCtx; return NTLSTATUS_NOT_SUPPORTED; } static KSTATUS NtfsDelete(IO_VOLUME_PROVIDER* fs, SYS_STRING_VIEW path) { (void)fs; (void)path; return NTLSTATUS_NOT_SUPPORTED; } static KSTATUS NtfsTruncate(IO_VOLUME_PROVIDER* fs, void* fileCtx, uint64_t newSize, uint32_t fileFlags) { (void)fs; (void)fileCtx; (void)newSize; (void)fileFlags; return NTLSTATUS_NOT_SUPPORTED; } static uint64_t NtfsGetSize(IO_VOLUME_PROVIDER* fs, void* fileCtx) { (void)fs; NTFS_FILE* file = (NTFS_FILE*)fileCtx; return file ? file->Size : 0; } static KSTATUS NtfsQueryInfo(IO_VOLUME_PROVIDER* fs, void* fileCtx, SYS_FILE_STAT* out) { (void)fs; if (!fileCtx || !out) return NTLSTATUS_INVALID_PARAMETER; NtfsFillStat((NTFS_FILE*)fileCtx, out); return NTLSTATUS_SUCCESS; } static KSTATUS NtfsStat(IO_VOLUME_PROVIDER* fs, SYS_STRING_VIEW path, SYS_FILE_STAT* out) { if (!fs || !out || !SysSvIsValid(path)) return NTLSTATUS_INVALID_PARAMETER; void* ctx = NULL; KSTATUS s = NtfsOpen(fs, path, &ctx); if (!KSUCCESS(s)) return s; s = NtfsQueryInfo(fs, ctx, out); NtfsClose(fs, ctx); return s; } static KSTATUS NtfsQueryIdentity(IO_VOLUME_PROVIDER* fs, void* fileCtx, IO_FILE_IDENTITY* out) { (void)fs; if (!fileCtx || !out) return NTLSTATUS_INVALID_PARAMETER; NTFS_FILE* file = (NTFS_FILE*)fileCtx; out->FileId = file->Inode; out->Generation = (uint64_t)file->StandardInfo.LastWriteTime ^ ((uint64_t)file->StandardInfo.ChangeTime << 1u); out->Size = file->Size; return NTLSTATUS_SUCCESS; } static KSTATUS NtfsOpenDir(IO_VOLUME_PROVIDER* fs, SYS_STRING_VIEW dirPath, void** dirCtx) { if (!fs || !dirCtx || !SysSvIsValid(dirPath)) return NTLSTATUS_INVALID_PARAMETER; NTFS_VOLUME* vol = (NTFS_VOLUME*)fs->Context; if (!vol) return NTLSTATUS_INVALID_PARAMETER; uint64_t inode; KSTATUS s = NtfsLookupPath(vol, dirPath, &inode); if (!KSUCCESS(s)) { NtfsTrace("NTFS: opendir: lookup failed disk=%u lba=%llu pathLen=%llu st=0x%x\n", vol->DriveIndex, (unsigned long long)vol->PartitionLba, (unsigned long long)dirPath.Length, s); return s; } NTFS_FILE dir; s = NtfsLoadFile(vol, inode, &dir); if (!KSUCCESS(s)) { NtfsTrace("NTFS: opendir: load inode=%llu failed st=0x%x\n", (unsigned long long)inode, s); return s; } if (!dir.IsDirectory) { NtfsTrace("NTFS: opendir: inode=%llu is not directory attr=0x%x indexRoot=0x%016llX\n", (unsigned long long)inode, dir.Attributes, dir.IndexRoot); NtfsCleanupFile(&dir); return NTLSTATUS_NOT_FOUND; } if (!dir.IndexRoot) { NtfsTrace("NTFS: opendir: inode=%llu has no $I30 $INDEX_ROOT\n", (unsigned long long)inode); } else { NtfsTrace("NTFS: opendir: inode=%llu indexRootSize=%llu indexRecordBytes=%u indexRunsEmpty=%u\n", (unsigned long long)inode, (unsigned long long)dir.IndexRootSize, dir.IndexRoot->BytesPerIndexRecord, LIST_IS_EMPTY(&dir.IndexRuns) ? 1u : 0u); } NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)MmAllocatePool(sizeof(NTFS_DIR_CTX)); if (!ctx) { NtfsCleanupFile(&dir); return NTLSTATUS_NO_MEMORY; } memset(ctx, 0, sizeof(*ctx)); NTFS_COLLECT_CTX collect; memset(&collect, 0, sizeof(collect)); collect.Vol = vol; collect.Dir = ctx; collect.Status = NTLSTATUS_SUCCESS; collect.HideNtfsMetadata = (inode == NTFS_ROOT_DIR_INODE); s = NtfsWalkDirectory(&dir, NtfsCollectEntryCallback, &collect); NtfsCleanupFile(&dir); if (!KSUCCESS(s)) { NtfsTrace("NTFS: opendir: walk inode=%llu failed st=0x%x collected=%u\n", (unsigned long long)inode, s, ctx->Count); if (ctx->Entries) MmFreePool(ctx->Entries); MmFreePool(ctx); return s; } if (!KSUCCESS(collect.Status)) { NtfsTrace("NTFS: opendir: collect inode=%llu failed st=0x%x collected=%u\n", (unsigned long long)inode, collect.Status, ctx->Count); if (ctx->Entries) MmFreePool(ctx->Entries); MmFreePool(ctx); return collect.Status; } NtfsTrace("NTFS: opendir: inode=%llu OK entries=%u\n", (unsigned long long)inode, ctx->Count); *dirCtx = ctx; return NTLSTATUS_SUCCESS; } static KSTATUS NtfsReadDir(IO_VOLUME_PROVIDER* fs, void* dirCtx, KDIR_ENTRY* out) { (void)fs; if (!dirCtx || !out) return NTLSTATUS_INVALID_PARAMETER; NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)dirCtx; if (ctx->Position >= ctx->Count) return NTLSTATUS_NO_MORE_FILES; NTFS_DIRENT_CACHE* e = &ctx->Entries[ctx->Position++]; memset(out, 0, sizeof(*out)); strncpy(out->Name, e->Name, sizeof(out->Name) - 1u); out->Attr = (uint8_t)NtfsSysAttrsFromNtfs(e->Attributes, (e->Attributes & NTFS_FILE_ATTRIBUTE_DIRECTORY_MFT) != 0); out->FileSize = e->FileSize > 0xffffffffull ? 0xffffffffu : (uint32_t)e->FileSize; NtfsWinTimeToSystemTime(e->CreationTime, &out->CreateTime); NtfsWinTimeToSystemTime(e->LastAccessTime, &out->LastAccessTime); NtfsWinTimeToSystemTime(e->LastWriteTime, &out->LastWriteTime); return NTLSTATUS_SUCCESS; } static void NtfsCloseDir(IO_VOLUME_PROVIDER* fs, void* dirCtx) { (void)fs; NTFS_DIR_CTX* ctx = (NTFS_DIR_CTX*)dirCtx; if (!ctx) return; if (ctx->Entries) MmFreePool(ctx->Entries); MmFreePool(ctx); } static void NtfsQueryStats(IO_VOLUME_PROVIDER* fs, IO_FILESYSTEM_STATS* out) { (void)fs; if (out) memset(out, 0, sizeof(*out)); } static void NtfsVolumeDead(IO_VOLUME_PROVIDER* provider, IO_VOLUME_REMOVAL_REASON reason); static void NtfsDestroyVolumeProvider(IO_VOLUME_PROVIDER* provider); static void NtfsInitProvider(IO_VOLUME_PROVIDER* fs, NTFS_VOLUME* vol) { memset(fs, 0, sizeof(*fs)); fs->Open = NtfsOpen; fs->Read = NtfsRead; fs->Write = NtfsWrite; fs->Create = NtfsCreate; fs->Delete = NtfsDelete; fs->Truncate = NtfsTruncate; fs->GetSize = NtfsGetSize; fs->QueryInfo = NtfsQueryInfo; fs->Stat = NtfsStat; fs->QueryIdentity = NtfsQueryIdentity; fs->Close = NtfsClose; fs->QueryStats = NtfsQueryStats; fs->Context = vol; fs->OpenDir = NtfsOpenDir; fs->ReadDir = NtfsReadDir; fs->CloseDir = NtfsCloseDir; fs->VolumeDead = NtfsVolumeDead; fs->Destroy = NtfsDestroyVolumeProvider; } static bool NtfsIsValidBootSector(const NTFS_BOOT_SECTOR* bs) { if (!bs) return false; if (memcmp(bs->FsName, NTFS_FS_NAME, 8) != 0) return false; if (!NtfsIsPowerOfTwo32(bs->BytesPerSector)) return false; if (bs->BytesPerSector != NTFS_SECTOR_SIZE) return false; if (!NtfsIsPowerOfTwo32(bs->SectorsPerCluster)) return false; if (bs->TotalSectors == 0 || bs->MFT == 0) return false; return true; } static uint64_t NtfsDecodeRecordSize(int8_t clusters, uint32_t bytesPerCluster) { if (clusters > 0) return (uint64_t)(uint8_t)clusters * bytesPerCluster; if (clusters < 0 && clusters > -64) return 1ull << (uint8_t)(-clusters); return 0; } typedef struct NTFS_MFT_CTX { NTFS_VOLUME* Vol; KSTATUS Status; bool Found; } NTFS_MFT_CTX; static bool NtfsMftAttributeCallback(const NTFS_ATTR_VIEW* view, void* rawCtx) { NTFS_MFT_CTX* ctx = (NTFS_MFT_CTX*)rawCtx; const NTFS_ATTRIBUTE_RECORD_HEADER* attr = view->Attr; if (attr->TypeCode == NTFS_ATTR_DATA && attr->NameLength == 0 && attr->FormCode == NTFS_FORM_NONRESIDENT) { ctx->Status = NtfsReadMappings(ctx->Vol, attr, &ctx->Vol->MftRuns); ctx->Found = KSUCCESS(ctx->Status); return false; } return true; } static KSTATUS NtfsReadMftRuns(NTFS_VOLUME* vol) { void* record = MmAllocatePool((size_t)vol->FileRecordSize); if (!record) return NTLSTATUS_NO_MEMORY; uint64_t mftByte = vol->MftStartCluster * vol->BytesPerCluster; KSTATUS s = NtfsReadBytesDirect(vol, mftByte, vol->FileRecordSize, record); if (KSUCCESS(s)) { NTFS_FILE_RECORD_SEGMENT_HEADER* fr = (NTFS_FILE_RECORD_SEGMENT_HEADER*)record; if (fr->MultiSectorHeader.Signature != NTFS_FILE_SIGNATURE) s = NTLSTATUS_INVALID_PARAMETER; else s = NtfsProcessFixups(&fr->MultiSectorHeader, vol->FileRecordSize, vol->BytesPerSector); } if (!KSUCCESS(s)) { MmFreePool(record); return s; } NTFS_MFT_CTX ctx; ctx.Vol = vol; ctx.Status = NTLSTATUS_NOT_FOUND; ctx.Found = false; s = NtfsForEachRecordAttribute(vol, NTFS_MFT_INODE, (const NTFS_FILE_RECORD_SEGMENT_HEADER*)record, NtfsMftAttributeCallback, &ctx); MmFreePool(record); if (!KSUCCESS(s)) return s; if (!ctx.Found) return NTLSTATUS_NOT_FOUND; if (KSUCCESS(ctx.Status)) NtfsDebugDumpRuns("mft", &vol->MftRuns); return ctx.Status; } static KSTATUS NtfsInitUpcaseIdentity(NTFS_VOLUME* vol) { if (!vol) return NTLSTATUS_INVALID_PARAMETER; vol->Upcase = (uint16_t*)MmAllocatePool(NTFS_UPCASE_TABLE_BYTES); if (!vol->Upcase) return NTLSTATUS_NO_MEMORY; for (uint32_t i = 0; i < NTFS_UPCASE_TABLE_CHARS; i++) vol->Upcase[i] = (uint16_t)i; for (uint16_t c = (uint16_t)'a'; c <= (uint16_t)'z'; c++) vol->Upcase[c] = (uint16_t)(c - 32u); return NTLSTATUS_SUCCESS; } static void NtfsCleanupVolume(NTFS_VOLUME* vol) { if (!vol) return; if (vol->MftRuns.Fwd) NtfsFreeRuns(&vol->MftRuns); if (vol->Upcase) { MmFreePool(vol->Upcase); vol->Upcase = (uint16_t*)0; } } // Provider death blocks new I/O in the I/O core; NTFS state is retained for safe close teardown. static void NtfsVolumeDead(IO_VOLUME_PROVIDER* provider, IO_VOLUME_REMOVAL_REASON reason) { (void)provider; (void)reason; } // Releases one dynamically allocated NTFS mount after every volume reference has drained. static void NtfsDestroyVolumeProvider(IO_VOLUME_PROVIDER* provider) { if (!provider || !provider->Context) return; NTFS_VOLUME* volume = (NTFS_VOLUME*)provider->Context; NtfsCleanupVolume(volume); memset(volume, 0, sizeof(*volume)); MmFreePool(volume); } static void NtfsTryLoadUpcase(NTFS_VOLUME* vol) { NTFS_FILE upcase; if (!KSUCCESS(NtfsLoadFile(vol, NTFS_UPCASE_INODE, &upcase))) return; if (vol->Upcase && upcase.Size >= NTFS_UPCASE_TABLE_BYTES) { size_t br = 0; (void)NtfsReadFileData(&upcase, 0, vol->Upcase, NTFS_UPCASE_TABLE_BYTES, &br); } NtfsCleanupFile(&upcase); } static KSTATUS NtfsProbeAndPrepareVolume(NTFS_VOLUME* vol, uint8_t disk, uint64_t startLba, uint64_t sectors) { if (!vol) return NTLSTATUS_INVALID_PARAMETER; memset(vol, 0, sizeof(*vol)); vol->DriveIndex = disk; vol->PartitionLba = startLba; vol->PartitionSize = sectors; vol->BytesPerSector = NTFS_SECTOR_SIZE; NtfsInitRunList(&vol->MftRuns); LIST_INIT(&vol->MountLink); uint8_t sector[NTFS_SECTOR_SIZE]; memset(sector, 0, sizeof(sector)); KSTATUS s = NtfsReadSectors(vol, 0, 1, sector); if (!KSUCCESS(s)) return s; const NTFS_BOOT_SECTOR* bs = (const NTFS_BOOT_SECTOR*)sector; if (!NtfsIsValidBootSector(bs)) return NTLSTATUS_NOT_SUPPORTED; vol->BytesPerSector = bs->BytesPerSector; vol->SectorsPerCluster = bs->SectorsPerCluster; vol->BytesPerCluster = vol->BytesPerSector * vol->SectorsPerCluster; vol->TotalSectors = bs->TotalSectors; vol->PartitionSize = sectors ? sectors : bs->TotalSectors; if (vol->PartitionSize > bs->TotalSectors) vol->PartitionSize = bs->TotalSectors; vol->SerialNumber = bs->SerialNumber; vol->MftStartCluster = bs->MFT; vol->FileRecordSize = NtfsDecodeRecordSize(bs->ClustersPerMFTRecord, vol->BytesPerCluster); if (vol->FileRecordSize == 0 || vol->FileRecordSize > 65536ull || (vol->FileRecordSize % vol->BytesPerSector) != 0) return NTLSTATUS_INVALID_PARAMETER; s = NtfsInitUpcaseIdentity(vol); if (!KSUCCESS(s)) return s; s = NtfsReadMftRuns(vol); if (!KSUCCESS(s)) { NtfsCleanupVolume(vol); return s; } NtfsTryLoadUpcase(vol); return NTLSTATUS_SUCCESS; } // Initializes the dynamic NTFS mount-list mutex without allocating kernel objects. static void NtfsMountEnsureMutex(void) { if (gNtfsMountMutexReady) return; uintptr_t flags = KeAcquireSpinlock(&gNtfsMountMutexInitLock); if (!gNtfsMountMutexReady) { memset(&gNtfsMountMutex, 0, sizeof(gNtfsMountMutex)); ObInitializeObjectHeader(&gNtfsMountMutex.Header, ObjectTypeMutex); gNtfsMountMutex.Lock = (SPINLOCK)SPINLOCK_INIT; gNtfsMountMutex.Owner = (THREAD*)0; LIST_INIT(&gNtfsMountMutex.WaitList); LIST_INIT(&gNtfsMountMutex.OwnerLink); gNtfsMountMutex.DonationClass = KeSchedulingNormal; gNtfsMountMutex.DonationPriority = PriorityLow; gNtfsMountMutex.HasDonation = 0; gNtfsMountMutex.Abandoned = 0; gNtfsMountMutexReady = 1; } KeReleaseSpinlock(&gNtfsMountMutexInitLock, flags); } // Returns one active NTFS mount for an exact partition extent while the mount mutex is held. static NTFS_VOLUME* NtfsFindMountLocked(const IO_PARTITION_INFO* partition) { if (!partition) return (NTFS_VOLUME*)0; for (LIST_ENTRY* entry = gNtfsMounts.Fwd; entry != &gNtfsMounts; entry = entry->Fwd) { NTFS_VOLUME* volume = CONTAINING_RECORD(entry, NTFS_VOLUME, MountLink); if (volume->DriveIndex == partition->DiskIndex && volume->PartitionLba == partition->StartLba) { return volume; } } return (NTFS_VOLUME*)0; } // Probes one I/O-core partition extent and mounts it only when its VBR is valid NTFS. static bool NtfsTryMountPartition(const IO_PARTITION_INFO* partition) { if (!partition || partition->SectorCount == 0 || !IoDiskDevicePresent(partition->DiskIndex)) return false; NtfsMountEnsureMutex(); if (!KE_MUTEX_ACQUIRE_SUCCEEDED(KeAcquireMutex(&gNtfsMountMutex, KE_WAIT_INFINITE))) return false; if (NtfsFindMountLocked(partition)) { KeReleaseMutex(&gNtfsMountMutex); return false; } NTFS_VOLUME* mount = (NTFS_VOLUME*)MmAllocatePool(sizeof(NTFS_VOLUME)); if (!mount) { KeReleaseMutex(&gNtfsMountMutex); return false; } KSTATUS status = NtfsProbeAndPrepareVolume(mount, partition->DiskIndex, partition->StartLba, partition->SectorCount); if (!KSUCCESS(status)) { NtfsCleanupVolume(mount); MmFreePool(mount); KeReleaseMutex(&gNtfsMountMutex); return false; } IO_VOLUME_PROVIDER provider; NtfsInitProvider(&provider, mount); IO_VOLUME_SOURCE params; params.Type = IoVolumeSourcePartition; params.DiskIndex = partition->DiskIndex; memset(params.Reserved, 0, sizeof(params.Reserved)); params.StartLba = partition->StartLba; params.SectorCount = mount->PartitionSize; IO_VOLUME* volume = (IO_VOLUME*)0; status = IoCreateVolume(&provider, ¶ms, &volume); if (!KSUCCESS(status)) { if (volume) { (void)IoRemoveVolume(volume, IoVolumeUnmounted); IoReleaseVolume(volume); } else { NtfsCleanupVolume(mount); MmFreePool(mount); } DbgPrint("NTFS: disk=%u lba=%llu mount failed status=0x%x\n", partition->DiskIndex, (unsigned long long)partition->StartLba, status); KeReleaseMutex(&gNtfsMountMutex); return false; } // Retain the IoCreateVolume caller reference while this provider remains on the active mount list. mount->Volume = volume; ListInsertTail(&gNtfsMounts, &mount->MountLink); gMountedCount++; uint32_t mountedCount = gMountedCount; uint32_t instanceId = IoGetVolumeInstanceId(volume); uint64_t recordSize = mount->FileRecordSize; DbgPrint("NTFS: disk=%u lba=%llu mounted volume=%u (mft_record=%llu); total=%u\n", partition->DiskIndex, (unsigned long long)partition->StartLba, instanceId, (unsigned long long)recordSize, mountedCount); KeReleaseMutex(&gNtfsMountMutex); return true; } // Unmounts one exact dynamic NTFS provider instance when its backing partition disappears. static void NtfsUnmountPartition(const IO_PARTITION_INFO* partition, IO_PARTITION_CHANGE change) { if (!partition) return; NtfsMountEnsureMutex(); if (!KE_MUTEX_ACQUIRE_SUCCEEDED(KeAcquireMutex(&gNtfsMountMutex, KE_WAIT_INFINITE))) return; NTFS_VOLUME* mount = NtfsFindMountLocked(partition); if (!mount) { KeReleaseMutex(&gNtfsMountMutex); return; } IO_VOLUME* volume = mount->Volume; uint32_t instanceId = IoGetVolumeInstanceId(volume); ListRemove(&mount->MountLink); mount->Volume = (IO_VOLUME*)0; if (gMountedCount) gMountedCount--; uint32_t mountedCount = gMountedCount; KeReleaseMutex(&gNtfsMountMutex); (void)IoRemoveVolume(volume, IoVolumeBackingRemoved); IoReleaseVolume(volume); // Drop the active mount-list reference. DbgPrint("NTFS: disk=%u lba=%llu %s; unmounted volume=%u; total=%u\n", partition->DiskIndex, (unsigned long long)partition->StartLba, change == IoPartitionSurpriseRemoved ? "surprise removed" : "removed", instanceId, mountedCount); } // Receives immutable partition extents from the I/O core; NTFS never parses partition tables. static void NtfsPartitionChange(const IO_PARTITION_INFO* partition, IO_PARTITION_CHANGE change, void* context) { (void)context; switch (change) { case IoPartitionArrived: (void)NtfsTryMountPartition(partition); break; case IoPartitionSurpriseRemoved: case IoPartitionRemoved: NtfsUnmountPartition(partition, change); break; default: break; } } // Initializes NTFS and subscribes to the authoritative kernel partition stream. static KSTATUS DriverInitialize(NTL_MODULE* module) { (void)module; DbgPrint("NTFS: Init\n"); LIST_INIT(&gNtfsMounts); gMountedCount = 0; NtfsMountEnsureMutex(); KSTATUS status = IoRegisterPartitionChangeCallback(NtfsPartitionChange, NULL); if (!KSUCCESS(status)) { DbgPrint("NTFS: partition callback registration failed status=0x%x\n", status); return status; } if (!gMountedCount) DbgPrint("NTFS: no mountable NTFS partitions found; driver resident for partition changes.\n"); DbgPrint("NTFS: Init complete; mounted %u volume(s).\n", gMountedCount); return NTLSTATUS_SUCCESS; } NTL_DECLARE_MODULE(DriverInitialize);