Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ if (WIBO_ENABLE_FIXTURE_TESTS)
wibo_add_fixture_bin(NAME test_fls SOURCES test/test_fls.c)
wibo_add_fixture_bin(NAME test_fpu_state SOURCES test/test_fpu_state.c)
wibo_add_fixture_bin(NAME test_handleapi SOURCES test/test_handleapi.c)
wibo_add_fixture_bin(NAME test_movefileex SOURCES test/test_movefileex.c)
wibo_add_fixture_bin(NAME test_findfile SOURCES test/test_findfile.c)
wibo_add_fixture_bin(NAME test_getfileattributesex SOURCES test/test_getfileattributesex.c)
wibo_add_fixture_bin(NAME test_locale SOURCES test/test_locale.c)
Expand Down
114 changes: 114 additions & 0 deletions dll/kernel32/fileapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,38 @@
#include <unordered_map>
#include <vector>

#if defined(__linux__)
#include <linux/fs.h>
#include <sys/syscall.h>
#endif

namespace {

// Keep no-replace moves atomic even if another process creates the destination.
int renameFile(const std::filesystem::path &from, const std::filesystem::path &to, bool replace) {
if (replace)
return rename(from.c_str(), to.c_str());
#if defined(__APPLE__)
return renamex_np(from.c_str(), to.c_str(), RENAME_EXCL);
#else
return syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE);
#endif
}

DWORD moveFileError(int error, const std::filesystem::path &path) {
if (error == EXDEV)
return ERROR_NOT_SAME_DEVICE;
if (error == EISDIR || error == ENOTEMPTY || error == EPERM)
return ERROR_ACCESS_DENIED;
if (error == ENOENT) {
auto parent = path.parent_path();
std::error_code ec;
if (!parent.empty() && !std::filesystem::is_directory(parent, ec))
return ERROR_PATH_NOT_FOUND;
}
return wibo::winErrorFromErrno(error);
}

using random_shorts_engine =
std::independent_bits_engine<std::default_random_engine, sizeof(unsigned short) * 8, unsigned short>;

Expand Down Expand Up @@ -1241,6 +1271,90 @@ BOOL WINAPI MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName) {
return MoveFileA(from.c_str(), to.c_str());
}

BOOL WINAPI MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, DWORD dwFlags) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("MoveFileExA(%s, %s, 0x%x)\n", lpExistingFileName ? lpExistingFileName : "(null)",
lpNewFileName ? lpNewFileName : "(null)", dwFlags);
if ((dwFlags & MOVEFILE_DELAY_UNTIL_REBOOT) && (dwFlags & MOVEFILE_COPY_ALLOWED)) {
setLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
// There is no reboot queue or Windows link-tracking service in wibo.
if (dwFlags & (MOVEFILE_DELAY_UNTIL_REBOOT | MOVEFILE_CREATE_HARDLINK | MOVEFILE_FAIL_IF_NOT_TRACKABLE)) {
setLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
if (!lpExistingFileName || !lpNewFileName ||
(dwFlags & ~(MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH))) {
setLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
auto from = files::pathFromWindows(lpExistingFileName);
auto to = files::pathFromWindows(lpNewFileName);
struct stat source{}, destination{};
if (stat(from.c_str(), &source) != 0) {
setLastError(moveFileError(errno, from));
return FALSE;
}
bool replace = (dwFlags & MOVEFILE_REPLACE_EXISTING) != 0;
if (stat(to.c_str(), &destination) == 0) {
if (source.st_dev == destination.st_dev && source.st_ino == destination.st_ino)
return TRUE;
if (!replace) {
setLastError(ERROR_ALREADY_EXISTS);
return FALSE;
}
if (S_ISDIR(destination.st_mode) || !(destination.st_mode & S_IWUSR)) {
setLastError(ERROR_ACCESS_DENIED);
return FALSE;
}
} else if (errno != ENOENT) {
setLastError(moveFileError(errno, to));
return FALSE;
}
if (renameFile(from, to, replace) == 0)
return TRUE;
int error = errno;
if (error != EXDEV || !(dwFlags & MOVEFILE_COPY_ALLOWED) || !S_ISREG(source.st_mode)) {
setLastError(moveFileError(error, to));
return FALSE;
}

std::error_code ec;
auto options = replace ? std::filesystem::copy_options::overwrite_existing : std::filesystem::copy_options::none;
if (!std::filesystem::copy_file(from, to, options, ec)) {
setLastError(moveFileError(ec.value(), to));
return FALSE;
}
// WRITE_THROUGH requires flushing the copy before deleting the original.
if (dwFlags & MOVEFILE_WRITE_THROUGH) {
int fd = open(to.c_str(), O_RDONLY);
if (fd < 0) {
setLastError(moveFileError(errno, to));
return FALSE;
}
int result = fsync(fd);
error = errno;
close(fd);
if (result != 0) {
setLastError(moveFileError(error, to));
return FALSE;
}
}
// Windows reports success after a successful copy even if deletion fails.
if (unlink(from.c_str()) != 0)
DEBUG_LOG("MoveFileExA: copied but could not delete source (errno %d)\n", errno);
return TRUE;
}

BOOL WINAPI MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, DWORD dwFlags) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("MoveFileExW -> ");
std::string from = lpExistingFileName ? wideStringToString(lpExistingFileName) : "";
std::string to = lpNewFileName ? wideStringToString(lpNewFileName) : "";
return MoveFileExA(lpExistingFileName ? from.c_str() : nullptr, lpNewFileName ? to.c_str() : nullptr, dwFlags);
}

DWORD WINAPI SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("SetFilePointer(%p, %ld, %p, %u)\n", hFile, static_cast<long>(lDistanceToMove), lpDistanceToMoveHigh,
Expand Down
9 changes: 9 additions & 0 deletions dll/kernel32/fileapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ constexpr DWORD OPEN_EXISTING = 3;
constexpr DWORD OPEN_ALWAYS = 4;
constexpr DWORD TRUNCATE_EXISTING = 5;

constexpr DWORD MOVEFILE_REPLACE_EXISTING = 0x1;
constexpr DWORD MOVEFILE_COPY_ALLOWED = 0x2;
constexpr DWORD MOVEFILE_DELAY_UNTIL_REBOOT = 0x4;
constexpr DWORD MOVEFILE_WRITE_THROUGH = 0x8;
constexpr DWORD MOVEFILE_CREATE_HARDLINK = 0x10;
constexpr DWORD MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x20;

constexpr DWORD FILE_BEGIN = 0;
constexpr DWORD FILE_CURRENT = 1;
constexpr DWORD FILE_END = 2;
Expand Down Expand Up @@ -91,6 +98,8 @@ BOOL WINAPI DeleteFileA(LPCSTR lpFileName);
BOOL WINAPI DeleteFileW(LPCWSTR lpFileName);
BOOL WINAPI MoveFileA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName);
BOOL WINAPI MoveFileW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName);
BOOL WINAPI MoveFileExA(LPCSTR lpExistingFileName, LPCSTR lpNewFileName, DWORD dwFlags);
BOOL WINAPI MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, DWORD dwFlags);
DWORD WINAPI SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod);
BOOL WINAPI SetFilePointerEx(HANDLE hFile, LARGE_INTEGER liDistanceToMove, PLARGE_INTEGER lpNewFilePointer,
DWORD dwMoveMethod);
Expand Down
16 changes: 15 additions & 1 deletion dll/kernel32/memoryapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,21 @@ static LPVOID mapViewOfFileInternal(Pin<MappingObject> mapping, DWORD dwDesiredA
setLastError(ERROR_INVALID_ADDRESS);
return nullptr;
}
requestedBase = reinterpret_cast<void *>(mapBaseAddr);
uintptr_t mapEnd = alignUp(mapBaseAddr + mapLength, pageSize);
if (mapEnd <= mapBaseAddr || mapEnd > kProcessAddressLimit) {
setLastError(ERROR_INVALID_ADDRESS);
return nullptr;
}
// Fixed views must participate in the same address-space accounting as
// VirtualAlloc, both to reject overlaps and to prevent later allocations
// from replacing the view (VC6 restores PCH data at a fixed address).
wibo::heap::VmStatus reserveStatus =
wibo::heap::reserveViewRange(mapLength, mapBaseAddr, mapEnd, &requestedBase);
if (reserveStatus != wibo::heap::VmStatus::Success) {
setLastError(ERROR_INVALID_ADDRESS);
return nullptr;
}
reservedMapping = true;
#ifdef MAP_FIXED_NOREPLACE
mapFlags |= MAP_FIXED_NOREPLACE;
#else
Expand Down
1 change: 1 addition & 0 deletions src/errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#define ERROR_ACCESS_DENIED 5
#define ERROR_INVALID_HANDLE 6
#define ERROR_NOT_ENOUGH_MEMORY 8
#define ERROR_NOT_SAME_DEVICE 17
#define ERROR_NO_MORE_FILES 18
#define ERROR_NO_MORE_ITEMS 259
#define ERROR_FILE_EXISTS 80
Expand Down
36 changes: 36 additions & 0 deletions test/test_createfilemapping.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,46 @@ static void test_zero_size_pagefile_mapping_fails(void) {
TEST_CHECK_EQ(ERROR_INVALID_PARAMETER, GetLastError());
}

static void test_fixed_view_reserves_address_space(void) {
const SIZE_T size = 2 * 65536;
uint8_t *base = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE);
TEST_CHECK(base != NULL);
base[0] = 0x42;
HANDLE file = create_temp_file();
HANDLE mapping = CreateFileMappingA(file, NULL, PAGE_READWRITE, 0, (DWORD)size, NULL);
TEST_CHECK(mapping != NULL);
// A fixed view must not overwrite an existing allocation.
TEST_CHECK(MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base) == NULL);
TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError());
TEST_CHECK_EQ(0x42, base[0]);
TEST_CHECK(VirtualFree(base, 0, MEM_RELEASE));
uint8_t *view = MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base);
TEST_CHECK(view == base);
view[0] = 0x24;
view[size - 1] = 0x5a;
TEST_CHECK(MapViewOfFileEx(mapping, FILE_MAP_COPY, 0, 0, size, base) == NULL);
TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError());
TEST_CHECK(VirtualAlloc(base, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == NULL);
TEST_CHECK_EQ(ERROR_INVALID_ADDRESS, GetLastError());
// VC6 reserves another large top-down arena after restoring its PCH view.
uint8_t *arena = VirtualAlloc(NULL, 50 * 1024 * 1024, MEM_RESERVE | MEM_TOP_DOWN, PAGE_READWRITE);
TEST_CHECK(arena != NULL);
TEST_CHECK((uintptr_t)arena + 50 * 1024 * 1024 <= (uintptr_t)view || (uintptr_t)arena >= (uintptr_t)view + size);
TEST_CHECK_EQ(0x24, view[0]);
TEST_CHECK_EQ(0x5a, view[size - 1]);
TEST_CHECK(VirtualFree(arena, 0, MEM_RELEASE));
TEST_CHECK(UnmapViewOfFile(view));
TEST_CHECK(VirtualAlloc(base, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base);
TEST_CHECK(VirtualFree(base, 0, MEM_RELEASE));
TEST_CHECK(CloseHandle(mapping));
TEST_CHECK(CloseHandle(file));
}

int main(void) {
test_readwrite_mapping_extends_file();
test_copy_mapping_does_not_write_file();
test_zero_size_file_mapping_fails();
test_zero_size_pagefile_mapping_fails();
test_fixed_view_reserves_address_space();
return 0;
}
117 changes: 117 additions & 0 deletions test/test_movefileex.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#include "test_assert.h"
#include <windows.h>

static BOOL move_file(BOOL wide, const char *from, const char *to, DWORD flags) {
if (!wide)
return MoveFileExA(from, to, flags);
WCHAR from_w[MAX_PATH], to_w[MAX_PATH];
TEST_CHECK(MultiByteToWideChar(CP_ACP, 0, from, -1, from_w, MAX_PATH));
TEST_CHECK(MultiByteToWideChar(CP_ACP, 0, to, -1, to_w, MAX_PATH));
return MoveFileExW(from_w, to_w, flags);
}

static void write_file(const char *name, const char *content) {
HANDLE file = CreateFileA(name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
TEST_CHECK(file != INVALID_HANDLE_VALUE);
DWORD written;
TEST_CHECK(WriteFile(file, content, (DWORD)strlen(content), &written, NULL));
TEST_CHECK_EQ(strlen(content), written);
TEST_CHECK(CloseHandle(file));
}

static void check_file(const char *name, const char *content) {
HANDLE file = CreateFileA(name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
TEST_CHECK(file != INVALID_HANDLE_VALUE);
char buffer[32] = {0};
DWORD count;
TEST_CHECK(ReadFile(file, buffer, sizeof(buffer) - 1, &count, NULL));
TEST_CHECK_EQ(strlen(content), count);
TEST_CHECK_STR_EQ(content, buffer);
TEST_CHECK(CloseHandle(file));
}

static void check_missing(const char *name) {
TEST_CHECK_EQ(INVALID_FILE_ATTRIBUTES, GetFileAttributesA(name));
TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError());
}

static void test_moves(BOOL wide) {
write_file("source", "new contents");
TEST_CHECK(move_file(wide, "source", "dest", MOVEFILE_COPY_ALLOWED));
check_missing("source");
check_file("dest", "new contents");
write_file("source", "replacement");
TEST_CHECK(!move_file(wide, "source", "dest", 0));
TEST_CHECK_EQ(ERROR_ALREADY_EXISTS, GetLastError());
check_file("source", "replacement");
check_file("dest", "new contents");
TEST_CHECK(move_file(wide, "source", "dest", MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH));
check_missing("source");
check_file("dest", "replacement");
TEST_CHECK(move_file(wide, "dest", "dest", MOVEFILE_REPLACE_EXISTING));
check_file("dest", "replacement");
TEST_CHECK(!move_file(wide, "missing", "dest", MOVEFILE_REPLACE_EXISTING));
TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError());
check_file("dest", "replacement");
TEST_CHECK(!move_file(wide, "dest", "missing/child", 0));
TEST_CHECK_EQ(ERROR_PATH_NOT_FOUND, GetLastError());
TEST_CHECK(CreateDirectoryA("dir", NULL));
TEST_CHECK(!move_file(wide, "dest", "dir", MOVEFILE_REPLACE_EXISTING));
TEST_CHECK_EQ(ERROR_ACCESS_DENIED, GetLastError());
TEST_CHECK(CreateDirectoryA("dir2", NULL));
TEST_CHECK(!move_file(wide, "dir", "dir2", MOVEFILE_REPLACE_EXISTING));
TEST_CHECK_EQ(ERROR_ACCESS_DENIED, GetLastError());
TEST_CHECK(RemoveDirectoryA("dir2"));
TEST_CHECK(move_file(wide, "dir", "dir2", 0));
TEST_CHECK(RemoveDirectoryA("dir2"));
check_file("dest", "replacement");
TEST_CHECK(DeleteFileA("dest"));
}

// Optional destination directory on another volume, for exercising the EXDEV path.
static void test_cross_volume(BOOL wide, const char *directory) {
char dest[MAX_PATH];
TEST_CHECK(GetTempFileNameA(directory, "wbo", 0, dest));
TEST_CHECK(DeleteFileA(dest));
write_file("source", "cross volume");
TEST_CHECK(!move_file(wide, "source", dest, 0));
TEST_CHECK_EQ(ERROR_NOT_SAME_DEVICE, GetLastError());
check_file("source", "cross volume");
TEST_CHECK(move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH));
check_missing("source");
check_file(dest, "cross volume");
write_file("source", "replacement");
TEST_CHECK(!move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED));
TEST_CHECK_EQ(ERROR_ALREADY_EXISTS, GetLastError());
check_file("source", "replacement");
check_file(dest, "cross volume");
TEST_CHECK(
move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH));
check_missing("source");
check_file(dest, "replacement");
TEST_CHECK(DeleteFileA(dest));
TEST_CHECK(CreateDirectoryA("source", NULL));
TEST_CHECK(!move_file(wide, "source", dest, MOVEFILE_COPY_ALLOWED));
DWORD error = GetLastError();
TEST_CHECK(error == ERROR_NOT_SAME_DEVICE || error == ERROR_ACCESS_DENIED);
TEST_CHECK(RemoveDirectoryA("source"));
}

int main(int argc, char **argv) {
char original[MAX_PATH], temp[MAX_PATH], fixture[MAX_PATH];
TEST_CHECK(GetCurrentDirectoryA(MAX_PATH, original));
TEST_CHECK(GetTempPathA(MAX_PATH, temp));
TEST_CHECK(GetTempFileNameA(temp, "wbo", 0, fixture));
TEST_CHECK(DeleteFileA(fixture));
TEST_CHECK(CreateDirectoryA(fixture, NULL));
TEST_CHECK(SetCurrentDirectoryA(fixture));
test_moves(FALSE);
test_moves(TRUE);
if (argc > 1) {
test_cross_volume(FALSE, argv[1]);
test_cross_volume(TRUE, argv[1]);
}
TEST_CHECK(SetCurrentDirectoryA(original));
TEST_CHECK(RemoveDirectoryA(fixture));
return 0;
}
32 changes: 32 additions & 0 deletions test/vc6_pch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# VC6 automatic precompiled-header regression

VC6's `/YX /Fprepro.pch` writes a temporary file and calls
`MoveFileExW(temp, "repro.pch", MOVEFILE_COPY_ALLOWED)` to publish it.
Before the MoveFileEx shim, this aborts on a missing import.
Reuse also needs fixed-address `MapViewOfFileEx` views to reserve their address
range in the allocator. Otherwise VC6's subsequent top-down `VirtualAlloc`
can overwrite the restored PCH view. `test_createfilemapping.exe` checks that
fixed views reject overlapping allocations and remain intact.

Supply your own VC6 compiler; no proprietary compiler files are included.
Run with Wine first, then the newly built wibo:

```sh
WINEDEBUG=-all python3 test/vc6_pch/check.py \
--runner /path/to/wine --compiler /path/to/CL.EXE --output /tmp/pch-wine
python3 test/vc6_pch/check.py \
--runner build/debug/wibo --compiler /path/to/CL.EXE --output /tmp/pch-wibo
```

The check compiles without PCH, creates an automatic PCH, and recompiles with
that PCH. It requires the PCH's hash and modification time to remain unchanged
on reuse, and compares all three objects after excluding only the COFF
header timestamp. It retains the objects and command results in the output
directory so Wine and wibo output can also be compared.

`test_movefileex.exe` covers the API without requiring VC6. An optional first
argument names a writable directory on another volume to exercise cross-volume
copy, replacement, write-through and directory-move rejection. Run that variant
under Wine first, then wibo with the same destination. Delayed reboot operations
and link tracking are unsupported by the shim and return `ERROR_NOT_SUPPORTED`;
copy combined with delay and unknown flags return `ERROR_INVALID_PARAMETER`.
Loading