Files
nt-lite/code/kernel/io_file.c
T

1484 lines
53 KiB
C

// Kernel file I/O layer. This file owns file handles, filesystem dispatch, and asynchronous request plumbing.
#include <string.h>
#include <kernel/ke_list.h>
#include <kernel/ke_process.h>
#include <kernel/io_file.h>
#include <kernel/mm_file_cache.h>
#include <kernel/mm_pfn.h>
#include <kernel/mm_virt.h>
#include <kernel/ob_object.h>
#include <kernel/ke_userenv.h>
#include <kernel/ke_irql.h>
#include <kernel/ke_event.h>
#include <kernel/ke_apc.h>
#include <kernel/ke_semaphore.h>
#include <kernel/ke_thread.h>
#include <kernel/ke_sched.h>
#include <ntlite/syscall.h>
#define MAX_DRIVES 8
typedef struct {
IO_FILESYSTEM *Fs;
IO_VOLUME_PARAMS Vol;
} VOLUME_ENTRY;
static VOLUME_ENTRY gVolumes[MAX_DRIVES];
// Async file I/O worker.
typedef enum IO_ASYNC_FILE_OP {
IoAsyncFileRead = 0,
IoAsyncFileWrite = 1,
} IO_ASYNC_FILE_OP;
struct IO_ASYNC_FILE_REQUEST_OBJECT {
IO_ASYNC_REQUEST_OBJECT Async;
IO_ASYNC_FILE_OP Op;
FILE_OBJECT* File;
void* Buffer;
size_t Size;
uint64_t Offset;
};
static SPINLOCK gAsyncFileInitLock = SPINLOCK_INIT;
static int gAsyncFileInitState; // 0=off, 1=starting, 2=ready
static SPINLOCK gAsyncFileQueueLock = SPINLOCK_INIT;
static LIST_ENTRY gAsyncFileQueue;
static SEMAPHORE* gAsyncFileSemaphore;
typedef struct IO_FILE_LOCK_ENTRY {
LIST_ENTRY Link;
IO_FILE_IDENTITY Identity;
PROCESS* Owner;
uint64_t Offset;
uint64_t Length;
uint32_t Mode;
} IO_FILE_LOCK_ENTRY;
typedef struct IO_FILE_LOCK_WAITER {
LIST_ENTRY Link;
IO_FILE_IDENTITY Identity;
PROCESS* Owner;
uint64_t Offset;
uint64_t Length;
uint32_t Mode;
EVENT* Event;
uint8_t Armed;
} IO_FILE_LOCK_WAITER;
static SPINLOCK gFileLockLock = SPINLOCK_INIT;
static LIST_ENTRY gFileLocks;
static LIST_ENTRY gFileLockWaiters;
static uint8_t gFileLocksInitialized;
// Initializes the global advisory file-lock lists on first use.
static void KiIoEnsureFileLockState(void) {
uintptr_t flags = KeAcquireSpinlock(&gFileLockLock);
if (!gFileLocksInitialized) {
LIST_INIT(&gFileLocks);
LIST_INIT(&gFileLockWaiters);
gFileLocksInitialized = 1;
}
KeReleaseSpinlock(&gFileLockLock, flags);
}
// Compares the stable filesystem identity fields used for lock ownership.
static bool KiIoSameFileIdentity(const IO_FILE_IDENTITY* a, const IO_FILE_IDENTITY* b) {
if (!a || !b) return false;
return a->VolumeId == b->VolumeId &&
a->FileId == b->FileId &&
a->Generation == b->Generation;
}
// Returns the exclusive end offset for a public lock range.
static uint64_t KiIoFileLockEnd(uint64_t offset, uint64_t length) {
if (length == SYS_FILE_LOCK_TO_EOF)
return UINT64_MAX;
uint64_t end = offset + length;
if (end < offset)
return UINT64_MAX;
return end;
}
// Tests whether two byte ranges overlap.
static bool KiIoFileLockRangesOverlap(uint64_t aOffset, uint64_t aLength,
uint64_t bOffset, uint64_t bLength) {
uint64_t aEnd = KiIoFileLockEnd(aOffset, aLength);
uint64_t bEnd = KiIoFileLockEnd(bOffset, bLength);
return aOffset < bEnd && bOffset < aEnd;
}
// Tests whether one existing lock conflicts with a requested lock.
static bool KiIoFileLockConflicts(const IO_FILE_LOCK_ENTRY* lock,
const IO_FILE_IDENTITY* identity,
PROCESS* owner,
uint32_t mode,
uint64_t offset,
uint64_t length) {
if (!lock || !identity || !owner)
return false;
if (lock->Owner == owner)
return false;
if (!KiIoSameFileIdentity(&lock->Identity, identity))
return false;
if (!KiIoFileLockRangesOverlap(lock->Offset, lock->Length, offset, length))
return false;
if (lock->Mode == SYS_FILE_LOCK_EXCLUSIVE || mode == SYS_FILE_LOCK_EXCLUSIVE)
return true;
return false;
}
// Tests whether the requested lock is currently blocked by another process.
static bool KiIoFileLockHasConflictLocked(const IO_FILE_IDENTITY* identity,
PROCESS* owner,
uint32_t mode,
uint64_t offset,
uint64_t length) {
LIST_ENTRY* head = &gFileLocks;
for (LIST_ENTRY* node = head->Fwd; node != head; node = node->Fwd) {
IO_FILE_LOCK_ENTRY* lock = CONTAINING_RECORD(node, IO_FILE_LOCK_ENTRY, Link);
if (KiIoFileLockConflicts(lock, identity, owner, mode, offset, length))
return true;
}
return false;
}
// Returns the scheduler tick value used by finite file-lock waits.
static uint64_t KiIoFileLockReadSchedulerTick(void) {
KE_SCHED_STATS stats;
KeGetSchedulerStats(&stats);
return stats.Ticks;
}
// Converts a millisecond timeout to at least one scheduler tick.
static uint64_t KiIoFileLockTimeoutTicks(uint64_t timeoutMilliseconds) {
uint64_t ticks = timeoutMilliseconds / SCHED_TICK_MS;
if ((timeoutMilliseconds % SCHED_TICK_MS) != 0)
ticks++;
return ticks ? ticks : 1;
}
// Computes an absolute scheduler-tick deadline for a finite timeout.
static uint64_t KiIoFileLockTimeoutDeadline(uint64_t timeoutMilliseconds) {
uint64_t now = KiIoFileLockReadSchedulerTick();
uint64_t ticks = KiIoFileLockTimeoutTicks(timeoutMilliseconds);
uint64_t deadline = now + ticks;
return deadline < now ? UINT64_MAX : deadline;
}
// Computes the remaining wait time for a finite timeout deadline.
static uint64_t KiIoFileLockRemainingMilliseconds(uint64_t deadlineTick) {
uint64_t now = KiIoFileLockReadSchedulerTick();
if (deadlineTick <= now)
return 0;
uint64_t ticks = deadlineTick - now;
if (ticks > UINT64_MAX / SCHED_TICK_MS)
return UINT64_MAX;
return ticks * SCHED_TICK_MS;
}
// Waits for a file-lock conflict to change.
static KSTATUS KiIoWaitForFileLockChange(EVENT* event, uint64_t timeoutMilliseconds, uint64_t deadlineTick) {
if (!event)
return STATUS_INVALID_PARAMETER;
uint64_t waitMilliseconds = timeoutMilliseconds;
if (timeoutMilliseconds != SYS_WAIT_TIMEOUT_INFINITE) {
waitMilliseconds = KiIoFileLockRemainingMilliseconds(deadlineTick);
if (waitMilliseconds == 0)
return STATUS_TIMEOUT;
}
EVENT* events[1] = { event };
return KeWaitForMultipleEvents(events, 1, 0, waitMilliseconds, (uint32_t*)0);
}
// Wakes waiters whose requested file identity may have become available.
static void KiIoSignalFileLockWaitersLocked(const IO_FILE_IDENTITY* identity) {
if (!identity || !gFileLocksInitialized)
return;
LIST_ENTRY* head = &gFileLockWaiters;
for (LIST_ENTRY* node = head->Fwd; node != head; node = node->Fwd) {
IO_FILE_LOCK_WAITER* waiter = CONTAINING_RECORD(node, IO_FILE_LOCK_WAITER, Link);
if (waiter->Armed && waiter->Event && KiIoSameFileIdentity(&waiter->Identity, identity))
KeSignalEvent(waiter->Event);
}
}
// Releases file-specific references owned by an async file request.
static void KiIoAsyncFileRequestCleanup(IO_ASYNC_REQUEST_OBJECT* async, void* context) {
(void)context;
IO_ASYNC_FILE_REQUEST_OBJECT* req =
(IO_ASYNC_FILE_REQUEST_OBJECT*)async;
if (req->File) {
ObDereferenceObject(&req->File->Header);
req->File = (FILE_OBJECT*)0;
}
}
// Cancels an async file request from the common async request surface.
static KSTATUS KiIoCancelAsyncFileRequest(IO_ASYNC_REQUEST_OBJECT* async, void* context) {
(void)context;
if (!async) return STATUS_INVALID_PARAMETER;
uintptr_t flags = KeAcquireSpinlock(&gAsyncFileQueueLock);
IO_ASYNC_REQUEST_STATE state = IoAsyncRequestGetState(async);
if (state == IoAsyncRequestQueued) {
ListRemove(&async->Link);
IoAsyncRequestRequestCancel(async);
IoAsyncRequestSetState(async, IoAsyncRequestCancelled);
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
IoCompleteAsyncRequest(async, STATUS_CANCELLED, 0);
ObDereferenceObject(&async->Header);
return STATUS_SUCCESS;
}
if (state == IoAsyncRequestRunning) {
IoAsyncRequestRequestCancel(async);
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
return STATUS_PENDING;
}
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
return STATUS_OBJECT_DEAD;
}
// Implements the io Async File Dequeue operation.
static IO_ASYNC_FILE_REQUEST_OBJECT* KiIoAsyncFileDequeue(void) {
uintptr_t flags = KeAcquireSpinlock(&gAsyncFileQueueLock);
LIST_ENTRY* node = ListPopHead(&gAsyncFileQueue);
if (!node) {
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
return (IO_ASYNC_FILE_REQUEST_OBJECT*)0;
}
IO_ASYNC_FILE_REQUEST_OBJECT* req =
(IO_ASYNC_FILE_REQUEST_OBJECT*)CONTAINING_RECORD(node, IO_ASYNC_REQUEST_OBJECT, Link);
if (IoAsyncRequestIsCancelRequested(&req->Async)) {
IoAsyncRequestSetState(&req->Async, IoAsyncRequestCancelled);
} else {
IoAsyncRequestSetState(&req->Async, IoAsyncRequestRunning);
}
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
return req;
}
// Implements the io Async File Worker operation.
static void KiIoAsyncFileWorker(void* arg) {
(void)arg;
for (;;) {
KSTATUS s = KeWaitSemaphore(gAsyncFileSemaphore);
if (!KSUCCESS(s)) {
KeYield();
continue;
}
IO_ASYNC_FILE_REQUEST_OBJECT* req = KiIoAsyncFileDequeue();
if (!req) continue;
if (IoAsyncRequestIsCancelRequested(&req->Async) || IoAsyncRequestGetState(&req->Async) == IoAsyncRequestCancelled) {
IoCompleteAsyncRequest(&req->Async, STATUS_CANCELLED, 0);
ObDereferenceObject(&req->Async.Header); // queue/worker reference
continue;
}
size_t done = 0;
if (req->Op == IoAsyncFileRead) {
s = IoReadFileAt(req->File, req->Offset, req->Buffer,
req->Size, &done);
} else {
s = IoWriteFileAt(req->File, req->Offset, req->Buffer,
req->Size, &done);
}
if (IoAsyncRequestIsCancelRequested(&req->Async)) {
// The operation may already have reached the filesystem. The
// completion contract still reports cancellation so waiters/APCs do
// not mistake a cancelled request for useful data.
s = STATUS_CANCELLED;
done = 0;
}
IoCompleteAsyncRequest(&req->Async, s, done);
ObDereferenceObject(&req->Async.Header); // queue/worker reference
}
}
// Implements the io Ensure Async File Io Started operation.
static KSTATUS KiIoEnsureAsyncFileIoStarted(void) {
uintptr_t flags = KeAcquireSpinlock(&gAsyncFileInitLock);
if (gAsyncFileInitState == 2) {
KeReleaseSpinlock(&gAsyncFileInitLock, flags);
return STATUS_SUCCESS;
}
if (gAsyncFileInitState == 1) {
KeReleaseSpinlock(&gAsyncFileInitLock, flags);
while (gAsyncFileInitState == 1)
KeYield();
return (gAsyncFileInitState == 2) ? STATUS_SUCCESS : STATUS_FAILURE;
}
gAsyncFileInitState = 1;
KeReleaseSpinlock(&gAsyncFileInitLock, flags);
LIST_INIT(&gAsyncFileQueue);
KSTATUS s = KeSemaphoreCreate(0, 0x7fffffff, &gAsyncFileSemaphore);
if (KSUCCESS(s)) {
THREAD* worker = (THREAD*)0;
s = KeCreateKernelThread((uintptr_t)KiIoAsyncFileWorker,
0,
PriorityNormal,
&worker);
// Keep the creator reference: this worker is intentionally persistent
// for the lifetime of the kernel.
(void)worker;
}
flags = KeAcquireSpinlock(&gAsyncFileInitLock);
gAsyncFileInitState = KSUCCESS(s) ? 2 : 0;
KeReleaseSpinlock(&gAsyncFileInitLock, flags);
return s;
}
static void KiIoNativeFsAsyncComplete(void* completeContext,
KSTATUS status,
size_t information) {
IO_ASYNC_FILE_REQUEST_OBJECT* req =
(IO_ASYNC_FILE_REQUEST_OBJECT*)completeContext;
if (!req) return;
if (IoAsyncRequestIsCancelRequested(&req->Async)) {
status = STATUS_CANCELLED;
information = 0;
}
IoCompleteAsyncRequest(&req->Async, status, information);
ObDereferenceObject(&req->Async.Header); // native filesystem provider reference
}
static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op,
FILE_OBJECT* file,
void* buffer,
size_t size,
uint64_t offset,
IO_STATUS_BLOCK* iosb,
EVENT* completionEvent,
KAPC* completionApc,
MM_LOCKED_USER_BUFFER* lockedBuffer,
MM_LOCKED_USER_BUFFER* lockedIosb,
IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) {
if (!file || !iosb) {
MmUnlockUserBuffer(lockedBuffer);
MmUnlockUserBuffer(lockedIosb);
return STATUS_INVALID_PARAMETER;
}
if (!buffer && size) {
MmUnlockUserBuffer(lockedBuffer);
MmUnlockUserBuffer(lockedIosb);
return STATUS_INVALID_PARAMETER;
}
if (!KeCanWait()) {
MmUnlockUserBuffer(lockedBuffer);
MmUnlockUserBuffer(lockedIosb);
return STATUS_INVALID_IRQL;
}
iosb->Status = STATUS_PENDING;
iosb->Information = 0;
IO_ASYNC_REQUEST_OBJECT* async = (IO_ASYNC_REQUEST_OBJECT*)0;
KSTATUS createStatus = IoCreateAsyncRequest(sizeof(IO_ASYNC_FILE_REQUEST_OBJECT),
KiIoAsyncFileRequestCleanup,
(void*)0,
KiIoCancelAsyncFileRequest,
(void*)0,
&async);
if (!KSUCCESS(createStatus)) {
iosb->Status = createStatus;
MmUnlockUserBuffer(lockedBuffer);
MmUnlockUserBuffer(lockedIosb);
return createStatus;
}
IO_ASYNC_FILE_REQUEST_OBJECT* req = (IO_ASYNC_FILE_REQUEST_OBJECT*)async;
req->Op = op;
req->File = file;
req->Buffer = buffer;
req->Size = size;
req->Offset = offset;
IoInitializeAsyncRequestCompletion(&req->Async, iosb, completionEvent,
completionApc, lockedBuffer, lockedIosb);
ObReferenceObject(&file->Header);
IO_FS_ASYNC_COMPLETE complete = KiIoNativeFsAsyncComplete;
KSTATUS ns = STATUS_NOT_SUPPORTED;
bool hasNative = (op == IoAsyncFileRead) ? (file->Fs->ReadAsync != 0)
: (file->Fs->WriteAsync != 0);
if (hasNative) {
uintptr_t flags = KeAcquireSpinlock(&gAsyncFileQueueLock);
IoAsyncRequestSetState(&req->Async, IoAsyncRequestRunning);
KeReleaseSpinlock(&gAsyncFileQueueLock, flags);
ObReferenceObject(&req->Async.Header); // native filesystem provider reference
if (op == IoAsyncFileRead) {
ns = file->Fs->ReadAsync(file->Fs, file->FileCtx, offset, buffer,
size, complete, req);
} else {
ns = file->Fs->WriteAsync(file->Fs, file->FileCtx, offset, buffer,
size, complete, req);
}
if (ns == STATUS_PENDING) {
if (requestOut) {
*requestOut = req;
} else {
ObDereferenceObject(&req->Async.Header);
}
return STATUS_PENDING;
}
ObDereferenceObject(&req->Async.Header); // provider rejected synchronously
iosb->Status = ns;
req->Async.IosbSnapshot.Status = ns;
req->Async.IosbSnapshot.Information = 0;
ObDereferenceObject(&req->Async.Header); // creator reference
return ns;
}
KSTATUS s = KiIoEnsureAsyncFileIoStarted();
if (!KSUCCESS(s)) {
iosb->Status = s;
req->Async.IosbSnapshot.Status = s;
req->Async.IosbSnapshot.Information = 0;
ObDereferenceObject(&req->Async.Header);
return s;
}
// Queue ownership keeps the request alive until the worker completes or a
// successful queued cancellation completes it from IoCancelAsyncFileRequest.
ObReferenceObject(&req->Async.Header);
uintptr_t qflags = KeAcquireSpinlock(&gAsyncFileQueueLock);
ListInsertTail(&gAsyncFileQueue, &req->Async.Link);
KeReleaseSpinlock(&gAsyncFileQueueLock, qflags);
KeSignalSemaphore(gAsyncFileSemaphore);
if (requestOut) {
*requestOut = req; // creator reference returned to caller
} else {
ObDereferenceObject(&req->Async.Header);
}
return STATUS_PENDING;
}
__declspec(dllexport) KSTATUS IoReadFileAsyncKernel(FILE_OBJECT* file, void* buf,
size_t size, uint64_t offset,
IO_STATUS_BLOCK* iosb,
EVENT* completionEvent,
KAPC* completionApc,
MM_LOCKED_USER_BUFFER* lockedBuffer,
MM_LOCKED_USER_BUFFER* lockedIosb,
IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) {
return KiIoSubmitAsyncFileRequest(IoAsyncFileRead, file, buf, size, offset,
iosb, completionEvent, completionApc,
lockedBuffer, lockedIosb, requestOut);
}
__declspec(dllexport) KSTATUS IoWriteFileAsyncKernel(FILE_OBJECT* file, const void* buf,
size_t size, uint64_t offset,
IO_STATUS_BLOCK* iosb,
EVENT* completionEvent,
KAPC* completionApc,
MM_LOCKED_USER_BUFFER* lockedBuffer,
MM_LOCKED_USER_BUFFER* lockedIosb,
IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) {
return KiIoSubmitAsyncFileRequest(IoAsyncFileWrite, file, (void*)buf, size, offset,
iosb, completionEvent, completionApc,
lockedBuffer, lockedIosb, requestOut);
}
// Implements the async File Request Object operation.
__declspec(dllexport) OBJECT* IoAsyncFileRequestObject(IO_ASYNC_FILE_REQUEST_OBJECT* req) {
return req ? &req->Async.Header : (OBJECT*)0;
}
// Returns the wait event owned by an async file request.
__declspec(dllexport) EVENT* IoGetAsyncFileRequestWaitEvent(IO_ASYNC_FILE_REQUEST_OBJECT* req) {
return req ? IoGetAsyncRequestWaitEvent(&req->Async) : (EVENT*)0;
}
// Waits for an async file request to complete according to wait flags.
__declspec(dllexport) KSTATUS IoWaitAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, uint32_t flags) {
return IoWaitAsyncRequest(req ? &req->Async : (IO_ASYNC_REQUEST_OBJECT*)0, flags);
}
// Cancels an async file request through the common async I/O object.
__declspec(dllexport) KSTATUS IoCancelAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req) {
return IoCancelAsyncRequest(req ? &req->Async : (IO_ASYNC_REQUEST_OBJECT*)0);
}
__declspec(dllexport) KSTATUS IoQueryAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req,
IO_STATUS_BLOCK* iosbOut) {
return IoQueryAsyncRequest(req ? &req->Async : (IO_ASYNC_REQUEST_OBJECT*)0,
iosbOut);
}
// Current Working Directory
// Stored as (CurrentDrive, drive-relative path with no trailing backslash).
// Exported as native "N:/path" so personalities can round-trip volumes.
#define CWD_MAX PROCESS_CWD_MAX
typedef struct KRESOLVED_PATH {
uint8_t Drive;
char Storage[CWD_MAX];
SYS_STRING_VIEW Path;
} KRESOLVED_PATH;
// Implements the format Cwd operation.
static size_t KiFormatCwd(uint8_t drive, SYS_STRING_VIEW rel, char* buf, size_t size) {
if (!buf || size == 0 || !SysSvIsValid(rel)) return 0;
if (drive > 7) drive = 0;
size_t needed = 3 + rel.Length + 1;
if (needed > size) return needed;
buf[0] = (char)('0' + drive);
buf[1] = ':';
buf[2] = '/';
for (size_t i = 0; i < rel.Length; i++) {
char c = rel.Data[i];
buf[3 + i] = (c == '\\') ? '/' : c;
}
buf[3 + rel.Length] = '\0';
return needed;
}
// Implements the sv Has Drive Root operation.
static int KiSvHasDriveRoot(SYS_STRING_VIEW path) {
return path.Length >= 3 &&
path.Data[0] >= '0' && path.Data[0] <= '7' &&
path.Data[1] == ':' &&
(path.Data[2] == '\\' || path.Data[2] == '/');
}
static int KiAppendBytes(char* work, size_t* workLen, size_t cap,
const char* data, size_t len) {
if (!work || !workLen || (!data && len)) return 0;
if (*workLen + len >= cap) return 0;
if (len) memcpy(work + *workLen, data, len);
*workLen += len;
work[*workLen] = '\0';
return 1;
}
static int KiAppendComponent(char* work, size_t* workLen, size_t cap,
const char* data, size_t len) {
if (!work || !workLen || (!data && len)) return 0;
if (*workLen && work[*workLen - 1] != '\\' && work[*workLen - 1] != '/') {
if (!KiAppendBytes(work, workLen, cap, "\\", 1)) return 0;
}
return KiAppendBytes(work, workLen, cap, data, len);
}
// Implements the pop Component operation.
static void KiPopComponent(char* work, size_t* workLen) {
if (!work || !workLen || *workLen == 0) return;
size_t i = *workLen;
while (i > 0 && work[i - 1] != '\\' && work[i - 1] != '/') i--;
if (i > 0) i--;
*workLen = i;
work[i] = '\0';
}
// Resolve a possibly-relative string view against the CWD.
// The returned view is backed by out->Storage and remains valid while 'out' lives.
static KSTATUS KiResolvePathV(SYS_STRING_VIEW input, KRESOLVED_PATH* out) {
if (!out) return STATUS_INVALID_PARAMETER;
memset(out, 0, sizeof(*out));
if (!SysSvIsValid(input)) return STATUS_INVALID_PARAMETER;
if (SysSvContainsByte(input, '\0')) return STATUS_INVALID_PARAMETER;
if (input.Length >= CWD_MAX) return STATUS_BUFFER_TOO_SMALL;
uint8_t drive = 0;
PROCESS* proc = KeGetCurrentProcess();
if (proc) {
uintptr_t flags = KeAcquireSpinlock(&proc->CwdLock);
drive = proc->CurrentDrive;
KeReleaseSpinlock(&proc->CwdLock, flags);
}
size_t pos = 0;
bool fromDriveRoot = false;
if (KiSvHasDriveRoot(input)) {
drive = (uint8_t)(input.Data[0] - '0');
pos = 3;
fromDriveRoot = true;
}
char work[CWD_MAX * 2];
size_t workLen = 0;
work[0] = '\0';
bool absolute = fromDriveRoot ||
(pos < input.Length && (input.Data[pos] == '\\' || input.Data[pos] == '/'));
if (!absolute && proc) {
uintptr_t flags = KeAcquireSpinlock(&proc->CwdLock);
size_t cwdLen = proc->CurrentDirectoryLength;
if (cwdLen >= sizeof(work)) cwdLen = sizeof(work) - 1;
memcpy(work, proc->CurrentDirectory, cwdLen);
work[cwdLen] = '\0';
workLen = cwdLen;
KeReleaseSpinlock(&proc->CwdLock, flags);
}
while (pos < input.Length) {
while (pos < input.Length && (input.Data[pos] == '\\' || input.Data[pos] == '/')) pos++;
if (pos >= input.Length) break;
size_t compStart = pos;
while (pos < input.Length && input.Data[pos] != '\\' && input.Data[pos] != '/') pos++;
size_t compLen = pos - compStart;
if (compLen == 0) continue;
if (compLen == 1 && input.Data[compStart] == '.') {
continue;
}
if (compLen == 2 && input.Data[compStart] == '.' && input.Data[compStart + 1] == '.') {
KiPopComponent(work, &workLen);
continue;
}
if (compLen >= 256) return STATUS_BUFFER_TOO_SMALL;
if (!KiAppendComponent(work, &workLen, sizeof(work), input.Data + compStart, compLen))
return STATUS_BUFFER_TOO_SMALL;
}
if (workLen + 1 > sizeof(out->Storage)) return STATUS_BUFFER_TOO_SMALL;
memcpy(out->Storage, work, workLen);
out->Storage[workLen] = '\0';
out->Drive = drive;
out->Path = SysSvFromParts(out->Storage, workLen);
return STATUS_SUCCESS;
}
// Builds a canonical native path using the kernel-owned process CWD.
__declspec(dllexport) KSTATUS IoRealPath(SYS_STRING_VIEW path, char* buffer, size_t bufferSize, size_t* requiredSizeOut) {
if (requiredSizeOut) *requiredSizeOut = 0;
if (path.Length && !path.Data) return STATUS_INVALID_PARAMETER;
if (path.Length == 0) return STATUS_INVALID_PARAMETER;
if (!buffer && bufferSize != 0) return STATUS_INVALID_PARAMETER;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
char formatted[SYS_CWD_MAX];
size_t required = KiFormatCwd(resolved.Drive, resolved.Path, formatted, sizeof(formatted));
if (!required || required > sizeof(formatted)) return STATUS_BUFFER_TOO_SMALL;
if (requiredSizeOut) *requiredSizeOut = required;
if (!buffer) return STATUS_SUCCESS;
if (bufferSize < required) return STATUS_BUFFER_TOO_SMALL;
memcpy(buffer, formatted, required);
return STATUS_SUCCESS;
}
// Updates cwd.
__declspec(dllexport) KSTATUS IoSetCwd(SYS_STRING_VIEW path) {
if (path.Length && !path.Data) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
PROCESS* proc = KeGetCurrentProcess();
if (!proc) return STATUS_ACCESS_DENIED;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->OpenDir) return STATUS_NOT_SUPPORTED;
void* dirCtx = NULL;
s = fs->OpenDir(fs, resolved.Path, &dirCtx);
if (!KSUCCESS(s)) return s;
fs->CloseDir(fs, dirCtx);
if (resolved.Path.Length + 1 > sizeof(proc->CurrentDirectory)) return STATUS_BUFFER_TOO_SMALL;
uintptr_t flags = KeAcquireSpinlock(&proc->CwdLock);
proc->CurrentDrive = resolved.Drive;
proc->CurrentDirectoryLength = resolved.Path.Length;
memcpy(proc->CurrentDirectory, resolved.Path.Data, resolved.Path.Length);
proc->CurrentDirectory[resolved.Path.Length] = '\0';
if (proc->UserCwdBase && proc->UserCwdSize) {
size_t formatted = KiFormatCwd(resolved.Drive, resolved.Path, (char*)proc->UserCwdBase, proc->UserCwdSize);
size_t copyLen = (formatted && formatted <= proc->UserCwdSize) ? formatted - 1 : proc->UserCwdSize - 1;
SYS_PEB* peb = (SYS_PEB*)proc->UserPebBase;
if (peb && peb->Magic == SYS_PEB_MAGIC) {
peb->CurrentDirectory = SysSvFromParts((const char*)proc->UserCwdBase, copyLen);
}
}
KeReleaseSpinlock(&proc->CwdLock, flags);
return STATUS_SUCCESS;
}
// Registration.
__declspec(dllexport) KSTATUS IoRegisterFilesystem(uint8_t volumeIndex,
IO_FILESYSTEM *fs,
const IO_VOLUME_PARAMS *vol) {
if (volumeIndex >= MAX_DRIVES || !fs || !vol)
return STATUS_INVALID_PARAMETER;
if (gVolumes[volumeIndex].Fs)
return STATUS_ALREADY_EXISTS;
gVolumes[volumeIndex].Fs = fs;
gVolumes[volumeIndex].Vol = *vol;
return STATUS_SUCCESS;
}
// Implements the unregister Filesystem operation.
__declspec(dllexport) KSTATUS IoUnregisterFilesystem(uint8_t volumeIndex) {
if (volumeIndex >= MAX_DRIVES)
return STATUS_INVALID_PARAMETER;
if (!gVolumes[volumeIndex].Fs)
return STATUS_NOT_FOUND;
memset(&gVolumes[volumeIndex], 0, sizeof(gVolumes[volumeIndex]));
return STATUS_SUCCESS;
}
// Implements the unregister Filesystems For Disk operation.
__declspec(dllexport) KSTATUS IoUnregisterFilesystemsForDisk(uint8_t diskIndex) {
KSTATUS last = STATUS_NOT_FOUND;
for (uint8_t i = 0; i < MAX_DRIVES; i++) {
if (gVolumes[i].Fs && gVolumes[i].Vol.DiskIndex == diskIndex) {
memset(&gVolumes[i], 0, sizeof(gVolumes[i]));
last = STATUS_SUCCESS;
}
}
return last;
}
// Implements the filesystem Present operation.
__declspec(dllexport) bool IoFilesystemPresent(uint8_t volumeIndex) {
if (volumeIndex >= MAX_DRIVES)
return false;
return gVolumes[volumeIndex].Fs != (IO_FILESYSTEM*)0;
}
// Aggregates filesystem traversal statistics across mounted volumes.
__declspec(dllexport) void IoQueryFilesystemStats(IO_FILESYSTEM_STATS* out) {
if (!out) return;
memset(out, 0, sizeof(*out));
for (uint8_t i = 0; i < MAX_DRIVES; i++) {
IO_FILESYSTEM* fs = gVolumes[i].Fs;
if (!fs || !fs->QueryStats) continue;
IO_FILESYSTEM_STATS one;
memset(&one, 0, sizeof(one));
fs->QueryStats(fs, &one);
out->PathLookups += one.PathLookups;
out->PathCacheHits += one.PathCacheHits;
out->PathCacheMisses += one.PathCacheMisses;
out->PathCacheStores += one.PathCacheStores;
out->PathCacheInvalidations += one.PathCacheInvalidations;
if (one.PathCacheGeneration > out->PathCacheGeneration)
out->PathCacheGeneration = one.PathCacheGeneration;
out->DirectorySearches += one.DirectorySearches;
out->DirectoryBlocksRead += one.DirectoryBlocksRead;
out->DirectoryEntriesScanned += one.DirectoryEntriesScanned;
out->DataReadRuns += one.DataReadRuns;
out->DataReadSectors += one.DataReadSectors;
out->DataReadRunMerges += one.DataReadRunMerges;
if (one.MaxDataReadSpanSectors > out->MaxDataReadSpanSectors)
out->MaxDataReadSpanSectors = one.MaxDataReadSpanSectors;
}
}
// Destructor.
static void KiFileDestroy(OBJECT* obj) {
FILE_OBJECT* f = CONTAINING_RECORD(obj, FILE_OBJECT, Header);
if (f->Fs && f->FileCtx) {
(void)IoFlushFile(f);
MmFileCacheInvalidateAll(f);
f->Fs->Close(f->Fs, f->FileCtx);
}
}
// IoOpenFile.
__declspec(dllexport) KSTATUS IoOpenFile(SYS_STRING_VIEW path, FILE_OBJECT **out) {
if ((!path.Data && path.Length) || !out) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
if (path.Length == 0) return STATUS_INVALID_PARAMETER;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
void* fileCtx;
s = fs->Open(fs, resolved.Path, &fileCtx);
if (!KSUCCESS(s)) return s;
// File objects reuse the generic object slot because the handle table only needs the shared header contract here.
FILE_OBJECT* f = (FILE_OBJECT *)ObAllocateObject(ObjectTypeFile,
sizeof(FILE_OBJECT));
if (!f) { fs->Close(fs, fileCtx); return STATUS_NO_MEMORY; }
f->Header.Destroy = KiFileDestroy;
f->Fs = fs;
f->FileCtx = fileCtx;
f->Lock = (SPINLOCK)SPINLOCK_INIT;
f->Size = fs->GetSize(fs, fileCtx);
f->Offset = 0;
f->Flags = 0;
f->ReadaheadScore = 0;
f->ReadaheadNextOffset = 0;
f->ReadaheadIssuedUntil = 0;
f->ReadaheadSuppressedUntil = 0;
*out = f;
return STATUS_SUCCESS;
}
static uint64_t KiAlignUpPage64(uint64_t value) {
return (value + PAGE_SIZE - 1u) & ~(uint64_t)(PAGE_SIZE - 1u);
}
static void KiIoMaybeScheduleReadAhead(FILE_OBJECT* file, uint64_t offset, size_t bytesRead, uint64_t fileSize) {
if (!file || bytesRead < 512)
return;
if ((file->Flags & IO_FILE_FLAG_NO_CACHE) != 0)
return;
uint64_t end = offset + bytesRead;
uint64_t start = 0;
uint32_t pages = 0;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
if (offset == file->ReadaheadNextOffset) {
if (file->ReadaheadScore < 32)
file->ReadaheadScore++;
} else {
file->ReadaheadScore = 1;
file->ReadaheadIssuedUntil = end;
}
file->ReadaheadNextOffset = end;
if (end < file->ReadaheadSuppressedUntil) {
pages = 0;
} else if (file->ReadaheadScore >= 2 || bytesRead >= PAGE_SIZE) {
pages = file->ReadaheadScore < 4 ? 8u : (file->ReadaheadScore < 8 ? 16u : 32u);
start = KiAlignUpPage64(end);
if (start < file->ReadaheadIssuedUntil)
start = KiAlignUpPage64(file->ReadaheadIssuedUntil);
if (start >= fileSize) {
pages = 0;
} else {
uint64_t maxEnd = start + ((uint64_t)pages * PAGE_SIZE);
if (maxEnd > fileSize)
pages = (uint32_t)((KiAlignUpPage64(fileSize) - start) / PAGE_SIZE);
file->ReadaheadIssuedUntil = start + ((uint64_t)pages * PAGE_SIZE);
}
}
KeReleaseSpinlock(&file->Lock, flags);
if (pages) {
KSTATUS ras = MmFileCacheScheduleReadAhead(file, start, pages);
if (ras == STATUS_QUEUE_FULL) {
flags = KeAcquireSpinlock(&file->Lock);
uint64_t suppressUntil = end + (1024u * 1024u);
if (suppressUntil < end || suppressUntil > fileSize)
suppressUntil = fileSize;
if (file->ReadaheadSuppressedUntil < suppressUntil)
file->ReadaheadSuppressedUntil = suppressUntil;
KeReleaseSpinlock(&file->Lock, flags);
}
}
}
// Explicit-offset file I/O. Async I/O is built on these routines so it does
// not race the sequential FILE_OBJECT.Offset used by legacy read/write calls.
__declspec(dllexport) KSTATUS IoReadFileAt(FILE_OBJECT* file, uint64_t offset,
void* buf, size_t size,
size_t* bytesRead) {
if (bytesRead) *bytesRead = 0;
if (!file || !buf || !bytesRead) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
if (size == 0) return STATUS_SUCCESS;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
uint64_t fileSize = file->Size;
KeReleaseSpinlock(&file->Lock, flags);
if (offset >= fileSize) return STATUS_SUCCESS;
size_t remaining = (size_t)(fileSize - offset);
if (size > remaining) size = remaining;
if ((file->Flags & IO_FILE_FLAG_NO_CACHE) || size < 512)
return file->Fs->Read(file->Fs, file->FileCtx, offset, buf, size, bytesRead);
size_t total = 0;
uint8_t* dst = (uint8_t*)buf;
while (total < size) {
uint64_t absolute = offset + total;
uint64_t pageOffset = absolute & ~(uint64_t)(PAGE_SIZE - 1);
size_t inPage = (size_t)(absolute - pageOffset);
size_t chunk = PAGE_SIZE - inPage;
if (chunk > size - total) chunk = size - total;
MM_FILE_CACHE_PAGE page;
KSTATUS s = MmFileCacheGetPage(file, pageOffset, &page);
if (!KSUCCESS(s)) {
if (total) {
*bytesRead = total;
return STATUS_SUCCESS;
}
return s;
}
if (inPage >= page.ValidBytes) {
MmFileCacheReleasePage(&page);
break;
}
size_t available = page.ValidBytes - inPage;
if (chunk > available) chunk = available;
void* srcPage = MmPfnToVirtual(page.Frame);
if (!srcPage) {
MmFileCacheReleasePage(&page);
return total ? STATUS_SUCCESS : STATUS_FAILURE;
}
memcpy(dst + total, (const uint8_t*)srcPage + inPage, chunk);
MmFileCacheReleasePage(&page);
total += chunk;
if (chunk == 0) break;
}
*bytesRead = total;
KiIoMaybeScheduleReadAhead(file, offset, total, fileSize);
return STATUS_SUCCESS;
}
__declspec(dllexport) KSTATUS IoWriteFileAt(FILE_OBJECT* file, uint64_t offset,
const void* buf, size_t size,
size_t* bytesWritten) {
if (bytesWritten) *bytesWritten = 0;
if (!file || !buf || !bytesWritten) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
if (!file->Fs->Write) return STATUS_NOT_SUPPORTED;
if (size == 0) return STATUS_SUCCESS;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
uint64_t oldSize = file->Size;
KeReleaseSpinlock(&file->Lock, flags);
size_t written = 0;
KSTATUS s;
if (file->Flags & IO_FILE_FLAG_NO_CACHE) {
s = file->Fs->Write(file->Fs, file->FileCtx, offset, buf, size, &written);
if (KSUCCESS(s) && written)
MmFileCacheInvalidate(file, offset, written);
} else {
s = MmFileCacheWrite(file, offset, buf, size, oldSize, &written);
}
if (KSUCCESS(s)) {
flags = KeAcquireSpinlock(&file->Lock);
uint64_t end = offset + written;
if (end > file->Size) file->Size = end;
KeReleaseSpinlock(&file->Lock, flags);
}
*bytesWritten = written;
return s;
}
// Flushes dirty mapped and cached pages for one file object.
__declspec(dllexport) KSTATUS IoFlushFile(FILE_OBJECT* file) {
if (!file) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KSTATUS sync = MmVadSyncFileMappings(file);
KSTATUS flush = MmFileCacheFlushFile(file);
return KSUCCESS(sync) ? flush : sync;
}
// IoReadFile - sequential read at current offset.
__declspec(dllexport) KSTATUS IoReadFile(FILE_OBJECT* file, void* buf,
size_t size, size_t* bytesRead) {
if (bytesRead) *bytesRead = 0;
if (!file || !buf || !bytesRead) return STATUS_INVALID_PARAMETER;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
uint64_t offset = file->Offset;
KeReleaseSpinlock(&file->Lock, flags);
KSTATUS s = IoReadFileAt(file, offset, buf, size, bytesRead);
if (KSUCCESS(s)) {
flags = KeAcquireSpinlock(&file->Lock);
file->Offset = offset + *bytesRead;
KeReleaseSpinlock(&file->Lock, flags);
}
return s;
}
// IoWriteFile - sequential write at current offset.
__declspec(dllexport) KSTATUS IoWriteFile(FILE_OBJECT* file, const void* buf,
size_t size, size_t* bytesWritten) {
if (bytesWritten) *bytesWritten = 0;
if (!file || !buf || !bytesWritten) return STATUS_INVALID_PARAMETER;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
uint64_t offset = (file->Flags & IO_FILE_FLAG_APPEND) ? file->Size : file->Offset;
KeReleaseSpinlock(&file->Lock, flags);
KSTATUS s = IoWriteFileAt(file, offset, buf, size, bytesWritten);
if (KSUCCESS(s)) {
flags = KeAcquireSpinlock(&file->Lock);
file->Offset = offset + *bytesWritten;
KeReleaseSpinlock(&file->Lock, flags);
}
return s;
}
// IoCreateFile - create a new file and return it open for writing.
__declspec(dllexport) KSTATUS IoCreateFile(SYS_STRING_VIEW path, FILE_OBJECT** out) {
if ((!path.Data && path.Length) || !out || path.Length == 0) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->Create) return STATUS_NOT_SUPPORTED;
void* fileCtx;
s = fs->Create(fs, resolved.Path, &fileCtx);
if (!KSUCCESS(s)) return s;
FILE_OBJECT* f = (FILE_OBJECT*)ObAllocateObject(ObjectTypeFile,
sizeof(FILE_OBJECT));
if (!f) { fs->Close(fs, fileCtx); return STATUS_NO_MEMORY; }
f->Header.Destroy = KiFileDestroy;
f->Fs = fs;
f->FileCtx = fileCtx;
f->Lock = (SPINLOCK)SPINLOCK_INIT;
f->Size = 0;
f->Offset = 0;
f->Flags = 0;
f->ReadaheadScore = 0;
f->ReadaheadNextOffset = 0;
f->ReadaheadIssuedUntil = 0;
f->ReadaheadSuppressedUntil = 0;
*out = f;
return STATUS_SUCCESS;
}
// IoDeleteFile - delete a file by path (does not require an open handle).
__declspec(dllexport) KSTATUS IoDeleteFile(SYS_STRING_VIEW path) {
if ((!path.Data && path.Length) || path.Length == 0) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->Delete) return STATUS_NOT_SUPPORTED;
return fs->Delete(fs, resolved.Path);
}
// Implements the rename File operation.
__declspec(dllexport) KSTATUS IoRenameFile(SYS_STRING_VIEW oldPath, SYS_STRING_VIEW newPath) {
if ((!oldPath.Data && oldPath.Length) || (!newPath.Data && newPath.Length) ||
oldPath.Length == 0 || newPath.Length == 0) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH oldResolved;
KRESOLVED_PATH newResolved;
KSTATUS s = KiResolvePathV(oldPath, &oldResolved);
if (!KSUCCESS(s)) return s;
s = KiResolvePathV(newPath, &newResolved);
if (!KSUCCESS(s)) return s;
if (oldResolved.Drive != newResolved.Drive) return STATUS_NOT_SUPPORTED;
IO_FILESYSTEM* fs = gVolumes[oldResolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->Rename) return STATUS_NOT_SUPPORTED;
return fs->Rename(fs, oldResolved.Path, newResolved.Path);
}
// Creates dir.
__declspec(dllexport) KSTATUS IoCreateDir(SYS_STRING_VIEW path) {
if ((!path.Data && path.Length) || path.Length == 0) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->CreateDir) return STATUS_NOT_SUPPORTED;
return fs->CreateDir(fs, resolved.Path);
}
// Releases dir.
__declspec(dllexport) KSTATUS IoRemoveDir(SYS_STRING_VIEW path) {
if ((!path.Data && path.Length) || path.Length == 0) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->RemoveDir) return STATUS_NOT_SUPPORTED;
return fs->RemoveDir(fs, resolved.Path);
}
// IoTruncateFile - resize the file; updates the cached Size on success.
__declspec(dllexport) KSTATUS IoTruncateFile(FILE_OBJECT* file, uint64_t newSize) {
if (!file) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
if (!file->Fs->Truncate) return STATUS_NOT_SUPPORTED;
KSTATUS flush = IoFlushFile(file);
if (!KSUCCESS(flush)) return flush;
KSTATUS s = file->Fs->Truncate(file->Fs, file->FileCtx, newSize, file->Flags);
if (KSUCCESS(s)) {
MmFileCacheInvalidate(file, newSize, (size_t)-1);
file->Size = newSize;
// Pull the file pointer back if it now sits past EOF.
if (file->Offset > file->Size)
file->Offset = file->Size;
}
return s;
}
__declspec(dllexport) KSTATUS IoSeekFile(FILE_OBJECT* file, uint64_t offset) {
if (!file) return STATUS_INVALID_PARAMETER;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
file->Offset = offset;
KeReleaseSpinlock(&file->Lock, flags);
return STATUS_SUCCESS;
}
// Returns file Size.
__declspec(dllexport) uint64_t IoGetFileSize(FILE_OBJECT* file) {
if (!file) return 0;
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
uint64_t size = file->Size;
KeReleaseSpinlock(&file->Lock, flags);
return size;
}
// Queries metadata for an open file object.
__declspec(dllexport) KSTATUS IoGetFileStat(FILE_OBJECT* file, SYS_FILE_STAT* out) {
if (!file || !out) return STATUS_INVALID_PARAMETER;
memset(out, 0, sizeof(*out));
out->Size = sizeof(*out);
out->Type = SYS_FILE_TYPE_REGULAR;
out->LinkCount = 1;
out->FileSize = IoGetFileSize(file);
if (!file->Fs || !file->Fs->QueryInfo)
return STATUS_SUCCESS;
KSTATUS s = file->Fs->QueryInfo(file->Fs, file->FileCtx, out);
if (KSUCCESS(s)) {
uintptr_t flags = KeAcquireSpinlock(&file->Lock);
file->Size = out->FileSize;
KeReleaseSpinlock(&file->Lock, flags);
}
return s;
}
// Queries metadata for a filesystem path.
__declspec(dllexport) KSTATUS IoStatFile(SYS_STRING_VIEW path, SYS_FILE_STAT* out) {
if ((!path.Data && path.Length) || path.Length == 0 || !out) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s = KiResolvePathV(path, &resolved);
if (!KSUCCESS(s)) return s;
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->Stat) return STATUS_NOT_SUPPORTED;
memset(out, 0, sizeof(*out));
out->Size = sizeof(*out);
return fs->Stat(fs, resolved.Path, out);
}
// Queries the filesystem identity for a file object.
__declspec(dllexport) KSTATUS IoQueryFileIdentity(FILE_OBJECT* file, IO_FILE_IDENTITY* out) {
if (!file || !out)
return STATUS_INVALID_PARAMETER;
memset(out, 0, sizeof(*out));
if (!file->Fs || !file->Fs->QueryIdentity)
return STATUS_NOT_SUPPORTED;
return file->Fs->QueryIdentity(file->Fs, file->FileCtx, out);
}
// Acquires an advisory process-owned byte-range lock for one file identity.
__declspec(dllexport) KSTATUS IoLockFile(FILE_OBJECT* file, PROCESS* owner, uint32_t mode,
uint64_t offset, uint64_t length,
uint64_t timeoutMilliseconds) {
if (!file || !owner) return STATUS_INVALID_PARAMETER;
if (mode != SYS_FILE_LOCK_SHARED && mode != SYS_FILE_LOCK_EXCLUSIVE) return STATUS_INVALID_PARAMETER;
if (length == 0) return STATUS_INVALID_PARAMETER;
if (timeoutMilliseconds != 0 && !KeCanWait()) return STATUS_INVALID_IRQL;
IO_FILE_IDENTITY identity;
KSTATUS s = IoQueryFileIdentity(file, &identity);
if (!KSUCCESS(s)) return s;
KiIoEnsureFileLockState();
uint64_t deadlineTick = 0;
if (timeoutMilliseconds != 0 && timeoutMilliseconds != SYS_WAIT_TIMEOUT_INFINITE)
deadlineTick = KiIoFileLockTimeoutDeadline(timeoutMilliseconds);
EVENT* waitEvent = (EVENT*)0;
IO_FILE_LOCK_WAITER waiter;
memset(&waiter, 0, sizeof(waiter));
for (;;) {
IO_FILE_LOCK_ENTRY* entry = (IO_FILE_LOCK_ENTRY*)MmAllocatePoolTag(sizeof(*entry), MM_POOL_TAG('f', 'l', 'c', 'k'));
if (!entry) {
if (waitEvent) ObDereferenceObject(&waitEvent->Header);
return STATUS_NO_MEMORY;
}
uintptr_t flags = KeAcquireSpinlock(&gFileLockLock);
if (!KiIoFileLockHasConflictLocked(&identity, owner, mode, offset, length)) {
entry->Identity = identity;
entry->Owner = owner;
entry->Offset = offset;
entry->Length = length;
entry->Mode = mode;
LIST_INIT(&entry->Link);
ListInsertTail(&gFileLocks, &entry->Link);
KeReleaseSpinlock(&gFileLockLock, flags);
if (waitEvent) ObDereferenceObject(&waitEvent->Header);
return STATUS_SUCCESS;
}
KeReleaseSpinlock(&gFileLockLock, flags);
MmFreePool(entry);
if (timeoutMilliseconds == 0) {
if (waitEvent) ObDereferenceObject(&waitEvent->Header);
return STATUS_WOULD_BLOCK;
}
if (!waitEvent) {
s = KeEventCreate(EventAutoReset, 0, &waitEvent);
if (!KSUCCESS(s)) return s;
waiter.Identity = identity;
waiter.Owner = owner;
waiter.Offset = offset;
waiter.Length = length;
waiter.Mode = mode;
waiter.Event = waitEvent;
LIST_INIT(&waiter.Link);
} else {
KeClearEvent(waitEvent);
}
flags = KeAcquireSpinlock(&gFileLockLock);
if (!KiIoFileLockHasConflictLocked(&identity, owner, mode, offset, length)) {
KeReleaseSpinlock(&gFileLockLock, flags);
continue;
}
waiter.Armed = 1;
ListInsertTail(&gFileLockWaiters, &waiter.Link);
KeReleaseSpinlock(&gFileLockLock, flags);
s = KiIoWaitForFileLockChange(waitEvent, timeoutMilliseconds, deadlineTick);
flags = KeAcquireSpinlock(&gFileLockLock);
if (waiter.Armed) {
ListRemove(&waiter.Link);
waiter.Armed = 0;
}
KeReleaseSpinlock(&gFileLockLock, flags);
if (!KSUCCESS(s)) {
ObDereferenceObject(&waitEvent->Header);
return s;
}
}
}
// Releases this process's overlapping advisory locks for one file identity.
__declspec(dllexport) KSTATUS IoUnlockFile(FILE_OBJECT* file, PROCESS* owner, uint64_t offset, uint64_t length) {
if (!file || !owner || length == 0) return STATUS_INVALID_PARAMETER;
IO_FILE_IDENTITY identity;
KSTATUS s = IoQueryFileIdentity(file, &identity);
if (!KSUCCESS(s)) return s;
KiIoEnsureFileLockState();
bool released = false;
uintptr_t flags = KeAcquireSpinlock(&gFileLockLock);
LIST_ENTRY* head = &gFileLocks;
LIST_ENTRY* node = head->Fwd;
while (node != head) {
LIST_ENTRY* next = node->Fwd;
IO_FILE_LOCK_ENTRY* lock = CONTAINING_RECORD(node, IO_FILE_LOCK_ENTRY, Link);
if (lock->Owner == owner && KiIoSameFileIdentity(&lock->Identity, &identity) &&
KiIoFileLockRangesOverlap(lock->Offset, lock->Length, offset, length)) {
ListRemove(&lock->Link);
MmFreePool(lock);
released = true;
}
node = next;
}
if (released)
KiIoSignalFileLockWaitersLocked(&identity);
KeReleaseSpinlock(&gFileLockLock, flags);
return released ? STATUS_SUCCESS : STATUS_NOT_FOUND;
}
// Releases every advisory file lock owned by one exiting process.
__declspec(dllexport) void IoReleaseFileLocksForProcess(PROCESS* owner) {
if (!owner) return;
KiIoEnsureFileLockState();
bool released = false;
uintptr_t flags = KeAcquireSpinlock(&gFileLockLock);
LIST_ENTRY* head = &gFileLocks;
LIST_ENTRY* node = head->Fwd;
while (node != head) {
LIST_ENTRY* next = node->Fwd;
IO_FILE_LOCK_ENTRY* lock = CONTAINING_RECORD(node, IO_FILE_LOCK_ENTRY, Link);
if (lock->Owner == owner) {
ListRemove(&lock->Link);
MmFreePool(lock);
released = true;
}
node = next;
}
if (released) {
LIST_ENTRY* waiterHead = &gFileLockWaiters;
for (LIST_ENTRY* waiterNode = waiterHead->Fwd; waiterNode != waiterHead; waiterNode = waiterNode->Fwd) {
IO_FILE_LOCK_WAITER* waiter = CONTAINING_RECORD(waiterNode, IO_FILE_LOCK_WAITER, Link);
if (waiter->Armed && waiter->Event)
KeSignalEvent(waiter->Event);
}
}
KeReleaseSpinlock(&gFileLockLock, flags);
}
// Releases file.
__declspec(dllexport) void IoCloseFile(FILE_OBJECT* file) {
if (file) ObDereferenceObject(&file->Header);
}
// Directory enumeration.
static void KiDirDestroy(OBJECT* obj) {
DIR_OBJECT* d = (DIR_OBJECT*)obj; // Header is first member
if (d->Fs && d->DirCtx) d->Fs->CloseDir(d->Fs, d->DirCtx);
}
// Opens dir.
__declspec(dllexport) KSTATUS IoOpenDir(SYS_STRING_VIEW dirPath, DIR_OBJECT** out) {
if (!out) return STATUS_INVALID_PARAMETER;
if (dirPath.Length && !dirPath.Data) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
KRESOLVED_PATH resolved;
KSTATUS s;
if (dirPath.Length == 0) {
PROCESS* proc = KeGetCurrentProcess();
if (!proc) return STATUS_ACCESS_DENIED;
uintptr_t flags = KeAcquireSpinlock(&proc->CwdLock);
size_t cwdLen = proc->CurrentDirectoryLength;
if (cwdLen + 1 > sizeof(resolved.Storage)) {
KeReleaseSpinlock(&proc->CwdLock, flags);
return STATUS_BUFFER_TOO_SMALL;
}
memcpy(resolved.Storage, proc->CurrentDirectory, cwdLen + 1);
resolved.Drive = proc->CurrentDrive;
resolved.Path = SysSvFromParts(resolved.Storage, cwdLen);
KeReleaseSpinlock(&proc->CwdLock, flags);
} else {
s = KiResolvePathV(dirPath, &resolved);
if (!KSUCCESS(s)) return s;
}
IO_FILESYSTEM* fs = gVolumes[resolved.Drive].Fs;
if (!fs) return STATUS_NOT_FOUND;
if (!fs->OpenDir) return STATUS_NOT_SUPPORTED;
void* dirCtx = NULL;
s = fs->OpenDir(fs, resolved.Path, &dirCtx);
if (!KSUCCESS(s)) return s;
DIR_OBJECT* d = (DIR_OBJECT*)ObAllocateObject(ObjectTypeDir, sizeof(DIR_OBJECT));
if (!d) { fs->CloseDir(fs, dirCtx); return STATUS_NO_MEMORY; }
d->Header.Destroy = KiDirDestroy;
d->Fs = fs;
d->DirCtx = dirCtx;
*out = d;
return STATUS_SUCCESS;
}
// Returns dir.
__declspec(dllexport) KSTATUS IoReadDir(DIR_OBJECT* dir, KDIR_ENTRY* entryOut) {
if (!dir || !entryOut) return STATUS_INVALID_PARAMETER;
if (!KeCanWait()) return STATUS_INVALID_IRQL;
if (!dir->Fs->ReadDir) return STATUS_NOT_SUPPORTED;
return dir->Fs->ReadDir(dir->Fs, dir->DirCtx, entryOut);
}
// Releases dir.
__declspec(dllexport) void IoCloseDir(DIR_OBJECT* dir) {
if (dir) ObDereferenceObject(&dir->Header);
}