378 lines
13 KiB
C
378 lines
13 KiB
C
// Advisory file-lock smoke test.
|
|
// Verifies process-owned byte-range conflicts, timeout behavior, and exit cleanup.
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <ntlite/syscall.h>
|
|
|
|
#define LOCK_OFFSET 0u
|
|
#define LOCK_LENGTH 128u
|
|
#define NONOVERLAP_OFFSET 512u
|
|
#define CHILD_PREFIX_MAX 32u
|
|
#define CHILD_PATH_MAX 256u
|
|
|
|
// Prints one NUL-terminated string to the kernel console.
|
|
static void print(const char* s) {
|
|
SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) };
|
|
SysWriteConsole(&req);
|
|
}
|
|
|
|
// Prints one unsigned integer to the kernel console.
|
|
static void print_u64(uint64_t v) {
|
|
char buf[32];
|
|
int i = 0;
|
|
if (v == 0) {
|
|
print("0");
|
|
return;
|
|
}
|
|
while (v && i < (int)sizeof(buf)) {
|
|
buf[i++] = (char)('0' + (v % 10));
|
|
v /= 10;
|
|
}
|
|
while (i--) {
|
|
char c = buf[i];
|
|
SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) };
|
|
SysWriteConsole(&req);
|
|
}
|
|
}
|
|
|
|
// Terminates the current test process.
|
|
static void exit_now(int32_t code) {
|
|
while (true) SysExitProcess(code);
|
|
}
|
|
|
|
// Fails the test with one status value.
|
|
static void fail_status(const char* what, uint32_t status) {
|
|
print("file_lock_smoke: ");
|
|
print(what);
|
|
print(" status=");
|
|
print_u64(status);
|
|
print("\n");
|
|
exit_now(1);
|
|
}
|
|
|
|
// Fails the test with one plain message.
|
|
static void fail(const char* what) {
|
|
print("file_lock_smoke: ");
|
|
print(what);
|
|
print("\n");
|
|
exit_now(1);
|
|
}
|
|
|
|
// Deletes one path if it already exists.
|
|
static void delete_path(const char* path) {
|
|
SYS_PATH_REQUEST req = { SysSvFromCstr(path) };
|
|
(void)SysDeleteFile(&req);
|
|
}
|
|
|
|
// Opens one path with lock rights.
|
|
static uint32_t open_existing_lockable(const char* path, uint32_t* fileOut) {
|
|
uint32_t h = 0;
|
|
SYS_OPEN_FILE_REQUEST req = SysMakeOpenFileRequest(SysSvFromCstr(path),
|
|
SYS_FILE_ACCESS_QUERY | SYS_FILE_ACCESS_LOCK,
|
|
SYS_FILE_OPEN_EXISTING,
|
|
SYS_FILE_FLAG_NONE,
|
|
&h);
|
|
uint32_t s = SysOpenFile(&req);
|
|
if (s == NTLSTATUS_SUCCESS)
|
|
*fileOut = h;
|
|
return s;
|
|
}
|
|
|
|
// Creates a temporary file on the first writable path that works.
|
|
static uint32_t create_temp_file(uint32_t* fileOut, const char** pathOut) {
|
|
static const char* paths[] = {
|
|
"file_lock_smoke.tmp",
|
|
"/file_lock_smoke.tmp",
|
|
"1:/file_lock_smoke.tmp",
|
|
"0:/file_lock_smoke.tmp",
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
|
|
uint32_t h = 0;
|
|
delete_path(paths[i]);
|
|
SYS_OPEN_FILE_REQUEST req = SysMakeOpenFileRequest(SysSvFromCstr(paths[i]),
|
|
SYS_FILE_ACCESS_READ | SYS_FILE_ACCESS_WRITE | SYS_FILE_ACCESS_QUERY | SYS_FILE_ACCESS_LOCK,
|
|
SYS_FILE_CREATE_NEW,
|
|
SYS_FILE_FLAG_NONE,
|
|
&h);
|
|
uint32_t s = SysOpenFile(&req);
|
|
if (s == NTLSTATUS_SUCCESS) {
|
|
*fileOut = h;
|
|
*pathOut = paths[i];
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
}
|
|
return NTLSTATUS_NOT_FOUND;
|
|
}
|
|
|
|
// Locks a range through the public request ABI.
|
|
static uint32_t lock_range(uint32_t file, uint32_t mode, uint64_t offset, uint64_t length, uint64_t timeoutMs) {
|
|
SYS_LOCK_FILE_REQUEST req = {
|
|
sizeof(req),
|
|
file,
|
|
mode,
|
|
SYS_FILE_LOCK_FLAG_NONE,
|
|
offset,
|
|
length,
|
|
timeoutMs,
|
|
};
|
|
return SysLockFile(&req);
|
|
}
|
|
|
|
// Unlocks a range through the public request ABI.
|
|
static uint32_t unlock_range(uint32_t file, uint64_t offset, uint64_t length) {
|
|
SYS_UNLOCK_FILE_REQUEST req = {
|
|
sizeof(req),
|
|
file,
|
|
SYS_FILE_LOCK_FLAG_NONE,
|
|
offset,
|
|
length,
|
|
};
|
|
return SysUnlockFile(&req);
|
|
}
|
|
|
|
// Splits the child command line into mode and path.
|
|
static int parse_child_command(SYS_STRING_VIEW view, char* mode, size_t modeCap, char* path, size_t pathCap) {
|
|
if (!view.Data || view.Length == 0)
|
|
return 0;
|
|
size_t sep = 0;
|
|
while (sep < view.Length && view.Data[sep] != ':')
|
|
sep++;
|
|
if (sep == view.Length || sep + 1 >= view.Length)
|
|
return 0;
|
|
if (sep + 1 > modeCap || view.Length - sep > pathCap)
|
|
return 0;
|
|
memcpy(mode, view.Data, sep);
|
|
mode[sep] = '\0';
|
|
memcpy(path, view.Data + sep + 1, view.Length - sep - 1);
|
|
path[view.Length - sep - 1] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
// Returns whether two NUL-terminated strings are equal.
|
|
static int str_eq(const char* a, const char* b) {
|
|
return strcmp(a, b) == 0;
|
|
}
|
|
|
|
// Returns whether one mode name belongs to the child command protocol.
|
|
static int is_child_mode(const char* mode) {
|
|
return str_eq(mode, "excl-blocked") ||
|
|
str_eq(mode, "excl-timeout") ||
|
|
str_eq(mode, "excl-wait") ||
|
|
str_eq(mode, "shared-ok") ||
|
|
str_eq(mode, "excl-nonoverlap") ||
|
|
str_eq(mode, "hold-exit");
|
|
}
|
|
|
|
// Child process entry for cross-process lock checks.
|
|
static void run_child(SYS_STRING_VIEW commandLine) {
|
|
char mode[CHILD_PREFIX_MAX];
|
|
char path[CHILD_PATH_MAX];
|
|
if (!parse_child_command(commandLine, mode, sizeof(mode), path, sizeof(path)))
|
|
exit_now(2);
|
|
|
|
uint32_t file = 0;
|
|
uint32_t s = open_existing_lockable(path, &file);
|
|
if (s != NTLSTATUS_SUCCESS)
|
|
exit_now(3);
|
|
|
|
if (str_eq(mode, "excl-blocked")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_WOULD_BLOCK ? 0 : 4);
|
|
}
|
|
|
|
if (str_eq(mode, "excl-timeout")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 20);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_TIMEOUT ? 0 : 5);
|
|
}
|
|
|
|
if (str_eq(mode, "excl-wait")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, SYS_WAIT_TIMEOUT_INFINITE);
|
|
if (s == NTLSTATUS_SUCCESS)
|
|
(void)unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_SUCCESS ? 0 : 6);
|
|
}
|
|
|
|
if (str_eq(mode, "shared-ok")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_SHARED, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
if (s == NTLSTATUS_SUCCESS)
|
|
(void)unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_SUCCESS ? 0 : 7);
|
|
}
|
|
|
|
if (str_eq(mode, "excl-nonoverlap")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, NONOVERLAP_OFFSET, LOCK_LENGTH, 0);
|
|
if (s == NTLSTATUS_SUCCESS)
|
|
(void)unlock_range(file, NONOVERLAP_OFFSET, LOCK_LENGTH);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_SUCCESS ? 0 : 8);
|
|
}
|
|
|
|
if (str_eq(mode, "hold-exit")) {
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
SysCloseHandle(file);
|
|
exit_now(s == NTLSTATUS_SUCCESS ? 0 : 9);
|
|
}
|
|
|
|
SysCloseHandle(file);
|
|
exit_now(10);
|
|
}
|
|
|
|
// Builds a child command line of the form mode:path.
|
|
static void build_child_command(char* out, size_t outCap, const char* mode, const char* path) {
|
|
size_t modeLen = strlen(mode);
|
|
size_t pathLen = strlen(path);
|
|
if (modeLen + 1 + pathLen + 1 > outCap)
|
|
fail("child command too long");
|
|
memcpy(out, mode, modeLen);
|
|
out[modeLen] = ':';
|
|
memcpy(out + modeLen + 1, path, pathLen + 1);
|
|
}
|
|
|
|
// Runs this image as a child and requires a zero exit code.
|
|
static void run_child_expect_success(const char* mode, const char* path) {
|
|
SYS_PEB* peb = SysGetPeb();
|
|
if (!peb || !peb->ImagePath.Data || peb->ImagePath.Length == 0)
|
|
fail("missing image path");
|
|
|
|
char command[CHILD_PREFIX_MAX + CHILD_PATH_MAX];
|
|
build_child_command(command, sizeof(command), mode, path);
|
|
|
|
uint32_t child = 0;
|
|
SYS_CREATE_PROCESS_REQUEST req = {
|
|
peb->ImagePath,
|
|
SysSvFromCstr(command),
|
|
&child,
|
|
};
|
|
uint32_t s = SysCreateProcess(&req);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("create child", s);
|
|
|
|
s = SysWait(child, SYS_WAIT_FLAG_NONE);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("wait child", s);
|
|
|
|
int32_t exitCode = 0;
|
|
s = SysGetProcessExitCode(child, &exitCode);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("query child", s);
|
|
SysCloseHandle(child);
|
|
if (exitCode != 0) {
|
|
print("file_lock_smoke: child ");
|
|
print(mode);
|
|
print(" exited ");
|
|
print_u64((uint32_t)exitCode);
|
|
print("\n");
|
|
exit_now(1);
|
|
}
|
|
}
|
|
|
|
// Verifies validation paths that should not depend on filesystem state.
|
|
static void test_validation(uint32_t file) {
|
|
SYS_LOCK_FILE_REQUEST lockReq = {
|
|
0,
|
|
file,
|
|
SYS_FILE_LOCK_EXCLUSIVE,
|
|
SYS_FILE_LOCK_FLAG_NONE,
|
|
LOCK_OFFSET,
|
|
LOCK_LENGTH,
|
|
0,
|
|
};
|
|
uint32_t s = SysLockFile(&lockReq);
|
|
if (s != NTLSTATUS_INVALID_PARAMETER) fail_status("bad lock size", s);
|
|
|
|
lockReq.Size = sizeof(lockReq);
|
|
lockReq.Length = 0;
|
|
s = SysLockFile(&lockReq);
|
|
if (s != NTLSTATUS_INVALID_PARAMETER) fail_status("zero lock length", s);
|
|
|
|
SYS_UNLOCK_FILE_REQUEST unlockReq = {
|
|
sizeof(unlockReq),
|
|
file,
|
|
SYS_FILE_LOCK_FLAG_NONE,
|
|
LOCK_OFFSET,
|
|
LOCK_LENGTH,
|
|
};
|
|
s = SysUnlockFile(&unlockReq);
|
|
if (s != NTLSTATUS_NOT_FOUND) fail_status("unlock missing range", s);
|
|
|
|
lockReq.Length = LOCK_LENGTH;
|
|
lockReq.FileHandle = 0xeeeeeeeeu;
|
|
s = SysLockFile(&lockReq);
|
|
if (s != NTLSTATUS_INVALID_HANDLE) fail_status("bad lock handle", s);
|
|
}
|
|
|
|
// Program entry point.
|
|
int main(void) {
|
|
SYS_PEB* peb = SysGetPeb();
|
|
if (peb && peb->CommandLine.Data && peb->CommandLine.Length != 0) {
|
|
char mode[CHILD_PREFIX_MAX];
|
|
char path[CHILD_PATH_MAX];
|
|
if (parse_child_command(peb->CommandLine, mode, sizeof(mode), path, sizeof(path)) &&
|
|
is_child_mode(mode)) {
|
|
run_child(peb->CommandLine);
|
|
}
|
|
}
|
|
|
|
print("file_lock_smoke: start\n");
|
|
|
|
uint32_t file = 0;
|
|
const char* path = 0;
|
|
uint32_t s = create_temp_file(&file, &path);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("create temp", s);
|
|
|
|
test_validation(file);
|
|
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("exclusive parent lock", s);
|
|
run_child_expect_success("excl-blocked", path);
|
|
run_child_expect_success("excl-timeout", path);
|
|
run_child_expect_success("excl-nonoverlap", path);
|
|
s = unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("exclusive parent unlock", s);
|
|
|
|
s = lock_range(file, SYS_FILE_LOCK_SHARED, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("shared parent lock", s);
|
|
run_child_expect_success("shared-ok", path);
|
|
run_child_expect_success("excl-blocked", path);
|
|
s = unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("shared parent unlock", s);
|
|
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("blocking setup lock", s);
|
|
SYS_PEB* selfPeb = SysGetPeb();
|
|
if (!selfPeb || !selfPeb->ImagePath.Data || selfPeb->ImagePath.Length == 0) fail("missing self image");
|
|
char command[CHILD_PREFIX_MAX + CHILD_PATH_MAX];
|
|
build_child_command(command, sizeof(command), "excl-wait", path);
|
|
uint32_t child = 0;
|
|
SYS_CREATE_PROCESS_REQUEST createReq = { selfPeb->ImagePath, SysSvFromCstr(command), &child };
|
|
s = SysCreateProcess(&createReq);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("create blocking child", s);
|
|
SysSleep(30);
|
|
s = unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("wake unlock", s);
|
|
s = SysWait(child, SYS_WAIT_FLAG_NONE);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("wait blocking child", s);
|
|
int32_t exitCode = 0;
|
|
s = SysGetProcessExitCode(child, &exitCode);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("query blocking child", s);
|
|
SysCloseHandle(child);
|
|
if (exitCode != 0) fail("blocking child failed");
|
|
|
|
run_child_expect_success("hold-exit", path);
|
|
s = lock_range(file, SYS_FILE_LOCK_EXCLUSIVE, LOCK_OFFSET, LOCK_LENGTH, 0);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("exit released lock", s);
|
|
s = unlock_range(file, LOCK_OFFSET, LOCK_LENGTH);
|
|
if (s != NTLSTATUS_SUCCESS) fail_status("exit release cleanup unlock", s);
|
|
|
|
SysCloseHandle(file);
|
|
delete_path(path);
|
|
|
|
print("file_lock_smoke: PASS\n");
|
|
exit_now(0);
|
|
return 0;
|
|
}
|