Files
AnyPS5/core/libs/tests/GuestNamedShmWindows.cpp

615 lines
33 KiB
C++

#include "prx/libc/include/Api.hpp"
#include "prx/libkernel/include/Api.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/SharedMemory.hpp"
#include "nid/Sha1.hpp"
#include <windows.h>
#include <aclapi.h>
#include <io.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <cwchar>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
#include <barrier>
#include <algorithm>
namespace {
constexpr int ReadOnly = 0;
constexpr int ReadWrite = 2;
constexpr int Create = 0x200;
constexpr int Exclusive = 0x800;
constexpr std::int64_t RaceSize = 4096;
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")");
}
void RequireError(int descriptor, int expected, const char* message) {
Require(descriptor == -1 && *__error_nid_postfix() == expected, message);
}
void RequireNoAdministratorsAccess(HANDLE handle) {
PACL dacl = nullptr;
PSECURITY_DESCRIPTOR descriptor = nullptr;
const DWORD result = ::GetSecurityInfo(handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, &dacl, nullptr, &descriptor);
if (result != ERROR_SUCCESS || dacl == nullptr) {
if (descriptor != nullptr) ::LocalFree(descriptor);
Require(false, "reading named shared-memory object permissions failed");
}
SID_IDENTIFIER_AUTHORITY authority = SECURITY_NT_AUTHORITY;
PSID administrators = nullptr;
if (::AllocateAndInitializeSid(&authority, 2, SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administrators) == FALSE) {
::LocalFree(descriptor);
Require(false, "building the Administrators SID failed");
}
ACL_SIZE_INFORMATION information{};
const BOOL gotInformation = ::GetAclInformation(dacl, &information, sizeof(information), AclSizeInformation);
bool grantsAdministrators = false;
if (gotInformation) {
for (DWORD index = 0; index < information.AceCount; ++index) {
void* rawAce = nullptr;
if (!::GetAce(dacl, index, &rawAce)) continue;
const auto* header = static_cast<const ACE_HEADER*>(rawAce);
if (header->AceType != ACCESS_ALLOWED_ACE_TYPE) continue;
const auto* ace = static_cast<const ACCESS_ALLOWED_ACE*>(rawAce);
if (::EqualSid(const_cast<DWORD*>(&ace->SidStart), administrators) != FALSE) {
grantsAdministrators = true;
break;
}
}
}
::FreeSid(administrators);
::LocalFree(descriptor);
Require(gotInformation && !grantsAdministrators,
"named shared-memory object grants access to the Administrators group");
}
void RequireNoUnlinkedFiles(const std::string& name) {
const auto digest = Nid::Sha1(std::vector<std::uint8_t>(name.begin(), name.end()));
std::string prefix;
for (const auto byte : digest) {
prefix += "0123456789abcdef"[byte >> 4];
prefix += "0123456789abcdef"[byte & 15];
}
const auto directory = std::filesystem::temp_directory_path() / L"AnyPS5" / L"shm";
if (!std::filesystem::exists(directory)) return;
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
const auto filename = entry.path().filename().string();
Require(!filename.starts_with(prefix + "-unlinked-"),
"closing the last descriptor did not reap its unlinked host object");
}
}
void VerifyUntrackedDescriptor(int descriptor) {
Require(File::IsSharedMemoryDescriptor_nid_no_patch(descriptor), "named descriptor was not tracked");
std::vector<wchar_t> path(32768);
const DWORD length = ::GetFinalPathNameByHandleW(reinterpret_cast<HANDLE>(::_get_osfhandle(descriptor)),
path.data(), static_cast<DWORD>(path.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
Require(length > 0 && length < path.size(), "locating the tracked file failed");
const HANDLE handle = ::CreateFileW(path.data(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
Require(handle != INVALID_HANDLE_VALUE, "opening an untracked handle to the named file failed");
const int untracked = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), _O_RDONLY | _O_BINARY);
if (untracked < 0) ::CloseHandle(handle);
Require(untracked >= 0, "creating an untracked descriptor failed");
const bool classified = File::IsSharedMemoryDescriptor_nid_no_patch(untracked);
Require(::_close(untracked) == 0, "closing the untracked descriptor failed");
Require(!classified, "an untracked descriptor entered the named shared-memory path");
Require(!File::IsSharedMemoryDescriptor_nid_no_patch(untracked), "a closed untracked descriptor was classified as shared memory");
}
void VerifyConcurrentDescriptorReuse(const std::string& name) {
std::barrier start(3);
std::vector<int> okay(2, 1);
std::vector<std::thread> workers;
for (int index = 0; index < 2; ++index) {
workers.emplace_back([&, index] {
start.arrive_and_wait();
for (int iteration = 0; iteration < 64; ++iteration) {
const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite, 0);
if (descriptor < 0) { okay[index] = 0; break; }
if (!File::IsSharedMemoryDescriptor_nid_no_patch(descriptor) ||
pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, 64) != 0) okay[index] = 0;
if (close_nid_postfix(descriptor) != 0) okay[index] = 0;
}
});
}
start.arrive_and_wait();
for (auto& worker : workers) worker.join();
Require(okay[0] && okay[1], "concurrent close/open lost named-descriptor tracking or bounded writes");
}
bool RunResizeWriteRace(const std::string& name) {
const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite, 0);
if (descriptor < 0) return false;
bool okay = true;
for (int iteration = 0; iteration < 300; ++iteration) {
const auto size = (iteration & 1) ? 0 : RaceSize;
if (ftruncate_nid_postfix(descriptor, size) != 0) { okay = false; break; }
if (pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, RaceSize / 2) < 0) { okay = false; break; }
struct _stat64 status{};
if (::_fstat64(descriptor, &status) != 0 || (status.st_size != 0 && status.st_size != RaceSize)) {
okay = false;
break;
}
}
close_nid_postfix(descriptor);
return okay;
}
void VerifyCrossProcessResizeWrite(const std::string& name, int descriptor) {
wchar_t executable[32768]{};
const DWORD length = ::GetModuleFileNameW(nullptr, executable, static_cast<DWORD>(std::size(executable)));
Require(length > 0 && length < std::size(executable), "locating the Windows shared-memory test executable failed");
std::vector<PROCESS_INFORMATION> workers;
for (int index = 0; index < 2; ++index) {
std::wstring command = L"\"" + std::wstring(executable, length) + L"\" --race " + std::wstring(name.begin(), name.end());
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
Require(::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, nullptr, &startup, &process) != FALSE,
"starting a cross-process shared-memory race worker failed");
workers.push_back(process);
}
const bool localSucceeded = RunResizeWriteRace(name);
bool childrenSucceeded = true;
for (const auto& process : workers) {
childrenSucceeded = childrenSucceeded && ::WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0;
DWORD exitCode = 1;
childrenSucceeded = childrenSucceeded && ::GetExitCodeProcess(process.hProcess, &exitCode) && exitCode == 0;
::CloseHandle(process.hThread);
::CloseHandle(process.hProcess);
}
Require(localSucceeded && childrenSucceeded,
"cross-process truncate/write race produced an invalid shared-object size");
struct _stat64 status{};
Require(::_fstat64(descriptor, &status) == 0 && (status.st_size == 0 || status.st_size == RaceSize),
"cross-process truncate/write race left an invalid final size");
Require(shm_unlink_nid_postfix(name.c_str()) == 0, "cross-process race cleanup failed");
}
void VerifyResizeReadPosition(const std::string& name) {
constexpr std::size_t payloadSize = 2u << 20;
const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, payloadSize) == 0,
"creating the resize/read object failed");
const int resizer = dup_nid_postfix(descriptor);
Require(resizer >= 0, "duplicating the resize/read descriptor failed");
std::vector<char> payload(payloadSize, 'r');
Require(pwrite_nid_disambig1_nid_postfix(descriptor, payload.data(), payload.size(), 0) == payloadSize &&
lseek_nid_postfix(descriptor, 137, 0) == 137, "preparing the resize/read object failed");
std::barrier start(2);
bool resized = true;
std::thread resizing([&] {
start.arrive_and_wait();
for (int iteration = 0; iteration < 256; ++iteration)
if (ftruncate_nid_postfix(resizer, payloadSize + ((iteration & 1) ? 4096 : 0)) != 0) resized = false;
});
start.arrive_and_wait();
std::vector<char> output(payloadSize);
bool read = true;
for (int iteration = 0; iteration < 32; ++iteration)
if (pread_nid_postfix(descriptor, output.data(), output.size(), 0) != payloadSize || output != payload) read = false;
resizing.join();
Require(resized && read && lseek_nid_postfix(descriptor, 0, 1) == 137,
"concurrent resize/read changed the shared file position or contents");
Require(shm_unlink_nid_postfix(name.c_str()) == 0 && close_nid_postfix(descriptor) == 0 && close_nid_postfix(resizer) == 0,
"resize/read cleanup failed");
RequireNoUnlinkedFiles(name);
}
void Run() {
const std::string base = "/anyps5-windows-named-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(::GetTickCount64());
const std::string name = base + "-lifecycle";
Require(shm_unlink_nid_postfix(name.c_str()) == -1 && *__error_nid_postfix() == 2,
"new test name unexpectedly existed");
*__error_nid_postfix() = 73;
const int original = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(original >= 0 && *__error_nid_postfix() == 73, "exclusive creation failed or changed guest errno");
VerifyUntrackedDescriptor(original);
Require(ftruncate_nid_postfix(original, 64) == 0, "sizing the named object failed");
VerifyConcurrentDescriptorReuse(name);
const char originalBytes[] = "original";
Require(pwrite_nid_disambig1_nid_postfix(original, originalBytes, sizeof(originalBytes), 0) == sizeof(originalBytes),
"write to named object failed");
*__error_nid_postfix() = 0;
RequireError(shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600), 17,
"exclusive creation replaced an existing named object");
const int reader = shm_open_nid_postfix(name.c_str(), ReadOnly, 0);
char contents[sizeof(originalBytes)]{};
Require(reader >= 0 && pread_nid_postfix(reader, contents, sizeof(contents), 0) == sizeof(contents) &&
std::memcmp(contents, originalBytes, sizeof(contents)) == 0, "reopen did not share the original contents");
*__error_nid_postfix() = 0;
Require(pwrite_nid_disambig1_nid_postfix(reader, "x", 1, 0) == -1 && *__error_nid_postfix() == 9,
"read-only descriptor accepted a write");
*__error_nid_postfix() = 0;
Require(pwrite_nid_disambig1_nid_postfix(reader, "x", 1, 64) == -1 && *__error_nid_postfix() == 9,
"read-only descriptor accepted a write at end of file");
*__error_nid_postfix() = 0;
Require(pwrite_nid_disambig1_nid_postfix(reader, "", 0, 64) == -1 && *__error_nid_postfix() == 9,
"read-only descriptor accepted a zero-length write");
Require(shm_unlink_nid_postfix(name.c_str()) == 0, "unlink of named object failed");
*__error_nid_postfix() = 0;
RequireError(shm_open_nid_postfix(name.c_str(), ReadOnly, 0), 2, "unlinked name remained openable");
std::memset(contents, 0, sizeof(contents));
Require(pread_nid_postfix(original, contents, sizeof(contents), 0) == sizeof(contents) &&
std::memcmp(contents, originalBytes, sizeof(contents)) == 0, "open descriptor stopped working after unlink");
const char updatedByte = 'X';
Require(pwrite_nid_disambig1_nid_postfix(original, &updatedByte, 1, 0) == 1,
"open descriptor stopped accepting writes after unlink");
const int replacement = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(replacement >= 0, "name could not be recreated while old descriptors remained open");
Require(ftruncate_nid_postfix(replacement, 64) == 0, "sizing the replacement object failed");
Require(fchmod_nid_postfix(original, 0400) == 0,
"fchmod failed on the old open object after unlink and name recreation");
Require(fchmod_nid_postfix(original, 0600) == 0,
"restoring mode on the old open object after name recreation failed");
std::memset(contents, 0x7f, sizeof(contents));
Require(pread_nid_postfix(replacement, contents, sizeof(contents), 0) == sizeof(contents) &&
std::memcmp(contents, "\0\0\0\0\0\0\0\0", sizeof(contents)) == 0,
"recreated name still referred to the unlinked object");
Require(pwrite_nid_disambig1_nid_postfix(replacement, "y", 1, 0) == 1,
"fchmod on the old descriptor changed the replacement object's permissions");
const char replacementBytes[] = "replacement";
Require(pwrite_nid_disambig1_nid_postfix(replacement, replacementBytes, sizeof(replacementBytes), 0) == sizeof(replacementBytes),
"write to replacement object failed");
std::memset(contents, 0, sizeof(contents));
Require(pread_nid_postfix(original, contents, sizeof(contents), 0) == sizeof(contents) &&
contents[0] == updatedByte && std::memcmp(contents + 1, originalBytes + 1, sizeof(originalBytes) - 2) == 0,
"replacement write changed the old open object");
Require(shm_unlink_nid_postfix(name.c_str()) == 0, "replacement unlink failed");
Require(close_nid_postfix(reader) == 0 && close_nid_postfix(original) == 0 && close_nid_postfix(replacement) == 0,
"closing lifecycle descriptors failed");
RequireNoUnlinkedFiles(name);
const std::string duplicateName = base + "-duplicate";
const int duplicateOriginal = shm_open_nid_postfix(duplicateName.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(duplicateOriginal >= 0 && ftruncate_nid_postfix(duplicateOriginal, 64) == 0,
"creating the duplicate test object failed");
const int duplicate = dup_nid_postfix(duplicateOriginal);
Require(duplicate >= 0 && pwrite_nid_disambig1_nid_postfix(duplicate, "d", 1, 0) == 1,
"dup lost shared-memory write access");
const int duplicateReader = shm_open_nid_postfix(duplicateName.c_str(), ReadOnly, 0);
Require(duplicateReader >= 0 && shm_unlink_nid_postfix(duplicateName.c_str()) == 0,
"unlinking the duplicate test object failed");
Require(close_nid_postfix(duplicateOriginal) == 0, "closing the duplicated original failed");
Require(dup2_nid_postfix(-1, duplicate) == -1,
"dup2 accepted an invalid source");
Require(pwrite_nid_disambig1_nid_postfix(duplicate, "e", 1, 0) == 1,
"failed dup2 destroyed its shared-memory target");
Require(dup2_nid_postfix(duplicateReader, duplicate) == duplicate,
"dup2 could not replace a shared-memory target");
Require(pwrite_nid_disambig1_nid_postfix(duplicate, "f", 1, 0) == -1 && *__error_nid_postfix() == 9,
"dup2 lost shared-memory read-only access");
Require(close_nid_postfix(duplicateReader) == 0, "closing the duplicate reader failed");
const std::string replacementName = base + "-duplicate-replacement";
const int duplicateReplacement = shm_open_nid_postfix(replacementName.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(duplicateReplacement >= 0 && dup2_nid_postfix(duplicateReplacement, duplicate) == duplicate,
"dup2 could not replace the last unlinked descriptor");
RequireNoUnlinkedFiles(duplicateName);
Require(shm_unlink_nid_postfix(replacementName.c_str()) == 0 &&
close_nid_postfix(duplicateReplacement) == 0 && close_nid_postfix(duplicate) == 0,
"cleaning up dup2 replacement descriptors failed");
RequireNoUnlinkedFiles(replacementName);
VerifyResizeReadPosition(base + "-resize-read");
const std::string concurrentName = base + "-exclusive-race";
constexpr int contenderCount = 12;
std::vector<int> descriptors(contenderCount, -2);
std::vector<int> errors(contenderCount, 0);
std::vector<std::thread> contenders;
contenders.reserve(contenderCount);
for (int index = 0; index < contenderCount; ++index) {
contenders.emplace_back([&, index] {
descriptors[index] = shm_open_nid_postfix(concurrentName.c_str(), ReadWrite | Create | Exclusive, 0600);
errors[index] = *__error_nid_postfix();
});
}
for (auto& contender : contenders) contender.join();
int created = 0;
for (int index = 0; index < contenderCount; ++index) {
if (descriptors[index] >= 0) {
++created;
Require(close_nid_postfix(descriptors[index]) == 0, "closing exclusive-race descriptor failed");
} else {
Require(errors[index] == 17, "exclusive-race loser did not receive EEXIST");
}
}
Require(created == 1, "exclusive create race produced more or fewer than one winner");
Require(shm_unlink_nid_postfix(concurrentName.c_str()) == 0, "exclusive-race cleanup failed");
const std::string raceName = base + "-cross-process-race";
const int raceDescriptor = shm_open_nid_postfix(raceName.c_str(), ReadWrite | Create | Exclusive, 0600);
Require(raceDescriptor >= 0 && ftruncate_nid_postfix(raceDescriptor, RaceSize) == 0,
"cross-process race object creation failed");
VerifyCrossProcessResizeWrite(raceName, raceDescriptor);
Require(close_nid_postfix(raceDescriptor) == 0, "closing cross-process race descriptor failed");
const std::string permissionsName = base + "-permissions";
const int restricted = shm_open_nid_postfix(permissionsName.c_str(), ReadWrite | Create | Exclusive, 0400);
Require(restricted >= 0, "read-only mode creation failed");
RequireNoAdministratorsAccess(reinterpret_cast<HANDLE>(::_get_osfhandle(restricted)));
Require(close_nid_postfix(restricted) == 0, "closing read-only mode object failed");
*__error_nid_postfix() = 0;
RequireError(shm_open_nid_postfix(permissionsName.c_str(), ReadWrite, 0), 13,
"mode 0400 allowed a later read-write open");
const int restrictedReader = shm_open_nid_postfix(permissionsName.c_str(), ReadOnly, 0);
Require(restrictedReader >= 0, "mode 0400 rejected a read-only open");
Require(fchmod_nid_postfix(restrictedReader, 0600) == 0,
"fchmod could not restore write permission on a named object");
const int restoredWriter = shm_open_nid_postfix(permissionsName.c_str(), ReadWrite, 0);
Require(restoredWriter >= 0, "mode 0600 after fchmod rejected read-write open");
Require(fchmod_nid_postfix(restoredWriter, 0400) == 0,
"fchmod could not remove write permission on a named object");
*__error_nid_postfix() = 0;
Require(shm_unlink_nid_postfix(permissionsName.c_str()) == -1 && *__error_nid_postfix() == 13,
"unlink of a read-only named object did not return EACCES");
Require(fchmod_nid_postfix(restoredWriter, 0600) == 0,
"fchmod could not restore permission before unlink");
Require(shm_unlink_nid_postfix(permissionsName.c_str()) == 0,
"owner could not unlink a writable named object");
Require(close_nid_postfix(restoredWriter) == 0, "closing restored writer failed");
Require(close_nid_postfix(restrictedReader) == 0, "closing restricted reader failed");
const std::string noPermissionsName = base + "-mode-zero";
const int noPermissions = shm_open_nid_postfix(noPermissionsName.c_str(), ReadWrite | Create | Exclusive, 0000);
Require(noPermissions >= 0 && fchmod_nid_postfix(noPermissions, 0600) == 0,
"fchmod could not restore access to a mode 0000 named object");
Require(shm_unlink_nid_postfix(noPermissionsName.c_str()) == 0 && close_nid_postfix(noPermissions) == 0,
"mode 0000 object cleanup failed");
*__error_nid_postfix() = 0;
RequireError(shm_open_nid_postfix(nullptr, ReadWrite, 0), 14, "null name did not return EFAULT");
*__error_nid_postfix() = 0;
RequireError(shm_open_nid_postfix("relative", ReadWrite, 0), 22, "relative name did not return EINVAL");
std::puts("Windows named shared-memory lifecycle checks passed");
}
bool EnvironmentEntryHasKey(const std::wstring& entry, const wchar_t* key) {
const auto separator = entry.find(L'=');
return separator != std::wstring::npos && separator == std::wcslen(key) &&
::CompareStringOrdinal(entry.data(), static_cast<int>(separator), key, static_cast<int>(separator), TRUE) == CSTR_EQUAL;
}
std::vector<wchar_t> EnvironmentWithTempPath(const std::wstring& path) {
std::vector<std::wstring> entries;
LPWCH environment = ::GetEnvironmentStringsW();
if (environment == nullptr) throw std::runtime_error("reading the Windows environment failed");
try {
for (const wchar_t* current = environment; *current != L'\0'; current += std::wcslen(current) + 1) {
std::wstring entry(current);
if (!EnvironmentEntryHasKey(entry, L"TMP") && !EnvironmentEntryHasKey(entry, L"TEMP"))
entries.push_back(std::move(entry));
}
} catch (...) {
::FreeEnvironmentStringsW(environment);
throw;
}
::FreeEnvironmentStringsW(environment);
entries.push_back(L"TMP=" + path);
entries.push_back(L"TEMP=" + path);
std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) {
return ::CompareStringOrdinal(left.c_str(), -1, right.c_str(), -1, TRUE) == CSTR_LESS_THAN;
});
std::vector<wchar_t> block;
for (const auto& entry : entries) {
block.insert(block.end(), entry.begin(), entry.end());
block.push_back(L'\0');
}
block.push_back(L'\0');
return block;
}
bool SameDirectory(HANDLE left, HANDLE right) {
BY_HANDLE_FILE_INFORMATION leftInfo{}, rightInfo{};
return ::GetFileInformationByHandle(left, &leftInfo) && ::GetFileInformationByHandle(right, &rightInfo) &&
leftInfo.dwVolumeSerialNumber == rightInfo.dwVolumeSerialNumber &&
leftInfo.nFileIndexHigh == rightInfo.nFileIndexHigh && leftInfo.nFileIndexLow == rightInfo.nFileIndexLow;
}
std::filesystem::path ExtendedPath(const std::filesystem::path& path) {
wchar_t fullPath[32768]{};
const DWORD length = ::GetFullPathNameW(path.c_str(), static_cast<DWORD>(std::size(fullPath)), fullPath, nullptr);
if (length == 0 || length >= std::size(fullPath))
throw std::runtime_error("getting the full private temporary path failed");
std::wstring full(fullPath, length);
if (full.starts_with(L"\\\\?\\")) return full;
if (full.starts_with(L"\\\\")) return L"\\\\?\\UNC\\" + full.substr(2);
return L"\\\\?\\" + full;
}
std::filesystem::path DosPath(const std::filesystem::path& path) {
const auto& native = path.native();
if (native.starts_with(L"\\\\?\\UNC\\")) return L"\\\\" + native.substr(8);
if (native.starts_with(L"\\\\?\\")) return native.substr(4);
return path;
}
void RemovePrivateDirectory(const std::filesystem::path& directory) {
WIN32_FIND_DATAW entry{};
const HANDLE search = ::FindFirstFileW((directory / L"*").c_str(), &entry);
if (search == INVALID_HANDLE_VALUE) {
if (::GetLastError() != ERROR_FILE_NOT_FOUND)
throw std::runtime_error("enumerating the private temporary directory for cleanup failed");
} else {
try {
do {
if (std::wcscmp(entry.cFileName, L".") == 0 || std::wcscmp(entry.cFileName, L"..") == 0) continue;
if (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
throw std::runtime_error("the private temporary directory contains a reparse point");
const auto child = directory / entry.cFileName;
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) RemovePrivateDirectory(child);
else if (!::DeleteFileW(child.c_str()))
throw std::runtime_error("removing a private temporary file failed");
} while (::FindNextFileW(search, &entry));
if (::GetLastError() != ERROR_NO_MORE_FILES)
throw std::runtime_error("enumerating the private temporary directory for cleanup failed");
} catch (...) {
::FindClose(search);
throw;
}
::FindClose(search);
}
if (!::RemoveDirectoryW(directory.c_str()))
throw std::runtime_error("removing the private temporary directory failed");
}
int RunWithShortTempPath(bool longPath) {
wchar_t tempBuffer[32768]{};
const DWORD tempLength = ::GetTempPathW(static_cast<DWORD>(std::size(tempBuffer)), tempBuffer);
if (tempLength == 0 || tempLength >= std::size(tempBuffer))
throw std::runtime_error("locating the Windows temporary directory failed");
const std::filesystem::path parent(std::wstring(tempBuffer, tempLength));
std::filesystem::path baseRoot;
bool created = false;
for (unsigned attempt = 0; attempt < 16; ++attempt) {
baseRoot = parent / (L"AnyPS5-Temp-Regression-" + std::to_wstring(::GetCurrentProcessId()) + L"-" +
std::to_wstring(::GetTickCount64()) + L"-" + std::to_wstring(attempt));
if (::CreateDirectoryW(baseRoot.c_str(), nullptr)) {
created = true;
break;
}
if (::GetLastError() != ERROR_ALREADY_EXISTS) throw std::runtime_error("creating a private temporary directory failed");
}
if (!created) throw std::runtime_error("creating a unique private temporary directory failed");
const auto cleanupRoot = ExtendedPath(baseRoot);
std::filesystem::path longRoot = cleanupRoot;
const auto cleanup = [&] {
RemovePrivateDirectory(cleanupRoot);
};
try {
std::vector<wchar_t> shortBuffer(32768);
DWORD shortLength = ::GetShortPathNameW(longRoot.c_str(), shortBuffer.data(), static_cast<DWORD>(shortBuffer.size()));
if (shortLength == 0)
throw std::runtime_error("GetShortPathNameW failed (Win32 error " + std::to_string(::GetLastError()) + ")");
if (shortLength >= shortBuffer.size())
throw std::runtime_error("GetShortPathNameW required a buffer of " + std::to_string(shortLength) + " characters");
std::filesystem::path shortRoot(std::wstring(shortBuffer.data(), shortLength));
if (longPath) {
bool aliasAvailable = true;
bool longAliasReady = false;
for (unsigned level = 0; level < 80; ++level) {
longRoot /= L"LongSharedMemoryTempDirectoryRegressionSegment";
if (!::CreateDirectoryW(longRoot.c_str(), nullptr))
throw std::runtime_error("creating the nested long temporary directory failed (Win32 error " + std::to_string(::GetLastError()) + ")");
shortLength = ::GetShortPathNameW(longRoot.c_str(), shortBuffer.data(), static_cast<DWORD>(shortBuffer.size()));
if (shortLength == 0)
throw std::runtime_error("GetShortPathNameW failed for nested path (Win32 error " + std::to_string(::GetLastError()) + ")");
if (shortLength >= shortBuffer.size())
throw std::runtime_error("GetShortPathNameW required a buffer of " + std::to_string(shortLength) + " characters");
shortRoot = std::filesystem::path(std::wstring(shortBuffer.data(), shortLength));
if (::CompareStringOrdinal(longRoot.filename().c_str(), -1, shortRoot.filename().c_str(), -1, TRUE) == CSTR_EQUAL) {
aliasAvailable = false;
break;
}
if (DosPath(longRoot).native().size() > MAX_PATH + 64 && DosPath(shortRoot).native().size() < MAX_PATH - 64) {
longAliasReady = true;
break;
}
}
if (!aliasAvailable) {
std::puts("long-short-temp regression skipped: this volume did not create an 8.3 alias for the nested directory");
cleanup();
return 77;
}
if (!longAliasReady)
throw std::runtime_error("nested private path did not exceed MAX_PATH with a usable short alias");
}
if (::CompareStringOrdinal(longRoot.filename().c_str(), -1, shortRoot.filename().c_str(), -1, TRUE) == CSTR_EQUAL) {
std::puts("short-temp regression skipped: this volume did not create an 8.3 alias for the private directory");
cleanup();
return 77;
}
const HANDLE longHandle = ::CreateFileW(longRoot.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (longHandle == INVALID_HANDLE_VALUE) throw std::runtime_error("opening the long temporary path failed");
const HANDLE shortHandle = ::CreateFileW(shortRoot.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (shortHandle == INVALID_HANDLE_VALUE) {
::CloseHandle(longHandle);
throw std::runtime_error("opening the 8.3 temporary alias failed");
}
const bool isSameDirectory = SameDirectory(longHandle, shortHandle);
::CloseHandle(shortHandle);
::CloseHandle(longHandle);
if (!isSameDirectory) throw std::runtime_error("the short temporary path did not identify the created directory");
std::wprintf(L"%ls regression verified 8.3 directory alias (%llu/%llu characters):\n%ls\n%ls\n",
longPath ? L"long-short-temp" : L"short-temp",
static_cast<unsigned long long>(longRoot.native().size()),
static_cast<unsigned long long>(shortRoot.native().size()), longRoot.c_str(), shortRoot.c_str());
std::fflush(stdout);
wchar_t executable[32768]{};
const DWORD executableLength = ::GetModuleFileNameW(nullptr, executable, static_cast<DWORD>(std::size(executable)));
if (executableLength == 0 || executableLength >= std::size(executable))
throw std::runtime_error("locating the Windows shared-memory test executable failed");
std::wstring command = L"\"" + std::wstring(executable, executableLength) + L"\" --short-temp-child";
auto environment = EnvironmentWithTempPath(DosPath(shortRoot).native());
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process{};
if (!::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, environment.data(), nullptr, &startup, &process))
throw std::runtime_error("starting the short temporary path child failed");
const DWORD wait = ::WaitForSingleObject(process.hProcess, 16000);
DWORD exitCode = 1;
const bool childSucceeded = wait == WAIT_OBJECT_0 && ::GetExitCodeProcess(process.hProcess, &exitCode) && exitCode == 0;
if (wait != WAIT_OBJECT_0) {
::TerminateProcess(process.hProcess, 1);
::WaitForSingleObject(process.hProcess, 1000);
}
if (!childSucceeded)
std::fprintf(stderr, "%s child failed: wait=%lu exit=%lu\n", longPath ? "long-short-temp" : "short-temp",
static_cast<unsigned long>(wait), static_cast<unsigned long>(exitCode));
::CloseHandle(process.hThread);
::CloseHandle(process.hProcess);
cleanup();
return childSucceeded ? 0 : 1;
} catch (...) {
cleanup();
throw;
}
}
}
int main(int argumentCount, char** arguments) {
try {
if (argumentCount == 3 && std::string_view(arguments[1]) == "--race")
return RunResizeWriteRace(arguments[2]) ? 0 : 1;
if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp")
return RunWithShortTempPath(false);
if (argumentCount == 2 && std::string_view(arguments[1]) == "--long-short-temp")
return RunWithShortTempPath(true);
if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp-child") {
Run();
return 0;
}
Run();
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}