mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
Merge pull request #7945 from veosett/codex/windows-cache-housekeeping-safety
fix(shader): avoid unsafe Windows cache directory traversal
This commit is contained in:
5 files changed
+133
No files matched your search
@@ -2220,6 +2220,24 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
set(housekeepingModes plain)
|
||||
if(WIN32)
|
||||
list(APPEND housekeepingModes extended extended-long device)
|
||||
else()
|
||||
list(APPEND housekeepingModes plain-long)
|
||||
endif()
|
||||
foreach(housekeepingMode IN LISTS housekeepingModes)
|
||||
set(housekeepingTest agc_shader_cache_housekeeping_${housekeepingMode})
|
||||
add_test(NAME ${housekeepingTest} COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_housekeeping.py
|
||||
$<TARGET_FILE:agc_shader_disk_cache_tests> ${housekeepingMode})
|
||||
set_tests_properties(${housekeepingTest} PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(${housekeepingTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
|
||||
@@ -2452,6 +2452,14 @@ void verifyDefaultDirectory(const char* self) {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--housekeeping-safety") {
|
||||
const auto directory = ShaderDiskCache::EntryDirectory();
|
||||
require(!directory.empty(), "the housekeeping fixture cache is disabled");
|
||||
ShaderDiskCache::Shutdown();
|
||||
require(ShaderDiskCache::Writers() == 0, "the housekeeping writer did not stop");
|
||||
std::cout << "housekeeping version " << directory.filename().string() << '\n';
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") {
|
||||
verifyResultRoundTrip();
|
||||
verifyEntryRoundTrip();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def native(path):
|
||||
return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path
|
||||
|
||||
|
||||
def snapshot(directory):
|
||||
return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes()
|
||||
for path in directory.rglob('*')}
|
||||
|
||||
|
||||
def main():
|
||||
executable = str(Path(sys.argv[1]).resolve())
|
||||
mode = sys.argv[2]
|
||||
assert mode in ('plain', 'plain-long', 'extended', 'extended-long', 'device')
|
||||
assert os.name == 'nt' or mode in ('plain', 'plain-long')
|
||||
sandbox = Path(tempfile.mkdtemp(prefix='aps5-housekeeping-')).resolve()
|
||||
temporary = Path(tempfile.gettempdir()).resolve()
|
||||
try:
|
||||
outside = sandbox / 'outside'
|
||||
outside.mkdir()
|
||||
environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0',
|
||||
ANYPS5_SHADER_CACHE_DIR=str(sandbox / 'bootstrap'))
|
||||
|
||||
def run():
|
||||
return subprocess.run([executable, '--housekeeping-safety'], cwd=outside,
|
||||
env=environment, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True, errors='replace', timeout=5)
|
||||
|
||||
bootstrap = run()
|
||||
assert bootstrap.returncode == 0, bootstrap.stdout
|
||||
match = re.search(r'housekeeping version ([0-9a-f]{16})', bootstrap.stdout)
|
||||
assert match, bootstrap.stdout
|
||||
version = match.group(1)
|
||||
siblings = [digit + version[1:] for digit in '0123' if digit != version[0]]
|
||||
root = sandbox / 'cache-\u00e4\u6f22'
|
||||
if mode.endswith('long'):
|
||||
while len(str(root)) < 230:
|
||||
root /= 'd' * 40
|
||||
root /= 'd' * max(1, 269 - len(str(root)))
|
||||
assert len(str(root)) >= 270
|
||||
root = native(root)
|
||||
current = root / version
|
||||
current.mkdir(parents=True)
|
||||
old = time.time() - 30 * 24 * 3600
|
||||
for parent in (root, outside):
|
||||
for index, name in enumerate(siblings):
|
||||
folder = parent / name
|
||||
folder.mkdir()
|
||||
(folder / 'sentinel').write_bytes(b'preserve outside the cache root')
|
||||
if index != 1:
|
||||
(folder / 'last-used').write_bytes(b'')
|
||||
os.utime(folder / 'last-used', (old if index == 0 else time.time(),) * 2)
|
||||
os.utime(folder, (old, old))
|
||||
(outside / 'outside.tmp.1').write_bytes(b'not a cache temporary')
|
||||
os.utime(outside / 'outside.tmp.1', (old, old))
|
||||
for name in ('shader.bin', 'old.tmp.1', 'fresh.tmp.1', 'last-used'):
|
||||
(current / name).write_bytes(b'cached bytes')
|
||||
for name in ('shader.bin', 'old.tmp.1', 'last-used'):
|
||||
os.utime(current / name, (old, old))
|
||||
before = snapshot(outside)
|
||||
if mode.startswith('extended'):
|
||||
environment['ANYPS5_SHADER_CACHE_DIR'] = str(root)
|
||||
elif mode == 'device':
|
||||
environment['ANYPS5_SHADER_CACHE_DIR'] = '\\\\.\\' + str(root)[4:]
|
||||
else:
|
||||
environment['ANYPS5_SHADER_CACHE_DIR'] = str(root)[4:] if os.name == 'nt' else str(root)
|
||||
started = time.time()
|
||||
try:
|
||||
result = run()
|
||||
except subprocess.TimeoutExpired as error:
|
||||
assert snapshot(outside) == before, 'housekeeping changed files outside its root'
|
||||
raise AssertionError('housekeeping did not return within five seconds; child terminated') from error
|
||||
assert snapshot(outside) == before, 'housekeeping changed files outside its root'
|
||||
assert result.returncode == 0, result.stdout
|
||||
assert 'housekeeping version ' + version in result.stdout, result.stdout
|
||||
assert (current / 'last-used').stat().st_mtime >= started - 2, 'active cache stamp was not refreshed'
|
||||
skipped = mode.startswith('extended') or mode == 'device'
|
||||
for name in siblings[:2]:
|
||||
assert (root / name).exists() == skipped, 'unexpected stale cache directory eviction'
|
||||
assert (root / siblings[2] / 'sentinel').read_bytes() == b'preserve outside the cache root'
|
||||
assert (current / 'old.tmp.1').exists() == skipped, 'unexpected old temporary removal'
|
||||
assert (current / 'fresh.tmp.1').read_bytes() == b'cached bytes'
|
||||
assert (current / 'shader.bin').read_bytes() == b'cached bytes'
|
||||
print(mode + ': returned, active stamp refreshed, outside tree intact, eviction policy verified')
|
||||
finally:
|
||||
assert sandbox.parent == temporary and sandbox.name.startswith('aps5-housekeeping-')
|
||||
shutil.rmtree(native(sandbox))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1669,6 +1669,11 @@ private:
|
||||
const auto stamp = directory / "last-used";
|
||||
std::ofstream(stamp, std::ios::binary | std::ios::trunc).close();
|
||||
std::filesystem::last_write_time(stamp, std::filesystem::file_time_type::clock::now(), error);
|
||||
#ifdef _WIN32
|
||||
auto nativeRoot = root;
|
||||
nativeRoot.make_preferred();
|
||||
if (nativeRoot.native().starts_with(L"\\\\?\\") || nativeRoot.native().starts_with(L"\\\\.\\")) return;
|
||||
#endif
|
||||
const auto now = std::filesystem::file_time_type::clock::now();
|
||||
for (std::filesystem::directory_iterator it(root, error), end; !error && it != end; it.increment(error)) {
|
||||
const auto name = it->path().filename().string();
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
### Functional
|
||||
|
||||
- [Windows cache housekeeping](../../../core/shader/recompiler/ShaderDiskCache.cpp): eviction of old versions and temporary files is skipped for `\\?\` and `\\.\` cache roots because the supported WinLibs directory iterator can enumerate the process working directory for long prefixed paths. The active version's `last-used` stamp update still runs; it fails for long `\\.\` aliases with the supported toolchain. Ordinary Windows directory enumeration remains subject to MAX_PATH.
|
||||
|
||||
- [f64 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) flush denormal inputs to signed zero when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0 and 2); modes 1 and 3 and an unknown mode keep them. This follows the RDNA2 ISA section 6.4 and LLVM's sign-preserving AMDGPU denormal modes. The gfx1036 audit in [#981](https://github.com/boykopovar/AnyPS5/issues/981#issuecomment-6070046381) reports mode-sensitive `VopcCompareF64` results. Class compares still inspect the original bits. The full four-mode matrix has not been newly measured on AMD hardware or the PS5's gfx1013.
|
||||
|
||||
- [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled.
|
||||
|
||||
Reference in new issue
Block a user