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
61 changes: 59 additions & 2 deletions ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,65 @@ inline std::string translatePs2Path(const char *ps2Path)
auto resolveWithBase = [&](const std::filesystem::path &base, const std::string &suffix) -> std::string
{
const std::string normalizedSuffix = normalizePs2PathSuffix(suffix);
std::filesystem::path resolved = base;
const std::filesystem::path normalizedBase = base.lexically_normal();
std::filesystem::path resolved = normalizedBase;
if (!normalizedSuffix.empty())
{
resolved /= std::filesystem::path(normalizedSuffix);
}
return resolved.lexically_normal().string();

const std::filesystem::path normalizedPath = resolved.lexically_normal();
std::error_code ec;
const std::filesystem::path canonicalBase = std::filesystem::weakly_canonical(normalizedBase, ec);
if (ec)
{
return {};
}
const std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(normalizedPath, ec);
if (ec)
{
return {};
}

std::size_t baseDepth = 0;
for (auto it = canonicalBase.begin(); it != canonicalBase.end(); ++it)
{
++baseDepth;
}
std::size_t pathDepth = 0;
for (auto it = canonicalPath.begin(); it != canonicalPath.end(); ++it)
{
++pathDepth;
}
if (pathDepth < baseDepth)
{
return {};
}

std::filesystem::path candidateBase = canonicalPath;
while (pathDepth-- > baseDepth)
{
candidateBase = candidateBase.parent_path();
}

const bool baseExists = std::filesystem::exists(canonicalBase, ec);
if (ec)
{
return {};
}
if (baseExists)
{
const bool sameBase = std::filesystem::equivalent(canonicalBase, candidateBase, ec);
if (ec || !sameBase)
{
return {};
}
}
else if (canonicalBase != candidateBase)
{
return {};
}
return normalizedPath.string();
};

if (lower.rfind("host0:", 0) == 0 || lower.rfind("host:", 0) == 0)
Expand Down Expand Up @@ -151,7 +204,11 @@ inline std::string translatePs2Path(const char *ps2Path)

if (pathStr.size() > 1 && pathStr[1] == ':')
{
#ifdef _WIN32
return resolveWithBase(getConfiguredCdRoot(), pathStr);
#else
return pathStr;
#endif
}

return resolveWithBase(getConfiguredCdRoot(), pathStr);
Expand Down
85 changes: 85 additions & 0 deletions ps2xTest/src/ps2_runtime_io_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,91 @@ void register_ps2_runtime_io_tests()
"mc0: directory should NOT exist under cdRoot");
});

tc.Run("PS2 paths cannot escape configured roots", [](TestCase &t)
{
TestContext test;

t.Equals(translatePs2Path("mc0:../outside.dat"), std::string(),
"mc0: traversal should be rejected");
t.Equals(translatePs2Path("cdrom:../../outside.dat"), std::string(),
"cdrom: traversal should be rejected");
t.Equals(translatePs2Path("host:..\\..\\outside.dat"), std::string(),
"mixed slash traversal should be rejected");

std::error_code ec;
std::filesystem::create_directory_symlink(test.paths.base,
test.paths.mcRoot / "escape", ec);
#ifndef _WIN32
t.IsFalse(static_cast<bool>(ec), "symlink traversal fixture should be created");
#endif
if (!ec)
{
t.Equals(translatePs2Path("mc0:/escape/outside.dat"), std::string(),
"symlink traversal should be rejected");
}
});

tc.Run("PS2 path normalization preserves valid paths", [](TestCase &t)
{
TestContext test;

t.Equals(translatePs2Path("mc0:/dir/../save.dat"),
(test.paths.mcRoot / "save.dat").string(),
"valid traversal within mcRoot should be preserved");
t.Equals(translatePs2Path("host:/config.ini"),
(test.paths.cdRoot / "config.ini").string(),
"host: path should resolve under hostRoot");
t.Equals(translatePs2Path("cdrom:/DATA.BIN;1"),
(test.paths.cdRoot / "DATA.BIN").string(),
"cdrom: path should preserve ISO version handling");
t.Equals(translatePs2Path("mc0:/SAVEDATA/save.dat"),
(test.paths.mcRoot / "SAVEDATA" / "save.dat").string(),
"mc0: path should resolve under mcRoot");
#ifdef _WIN32
const std::filesystem::path directPath = test.paths.cdRoot / "inside.dat";
t.Equals(translatePs2Path(directPath.string().c_str()), directPath.string(),
"direct Windows paths inside cdRoot should be preserved");
std::string caseVariant = directPath.string();
if (!caseVariant.empty() && caseVariant.front() >= 'A' && caseVariant.front() <= 'Z')
{
caseVariant.front() = static_cast<char>(caseVariant.front() - 'A' + 'a');
}
else if (!caseVariant.empty() && caseVariant.front() >= 'a' && caseVariant.front() <= 'z')
{
caseVariant.front() = static_cast<char>(caseVariant.front() - 'a' + 'A');
}
t.IsFalse(translatePs2Path(caseVariant.c_str()).empty(),
"Windows containment should compare path components case-insensitively");
t.Equals(translatePs2Path("C:\\outside.dat"), std::string(),
"direct Windows paths outside cdRoot should be rejected");
#else
t.Equals(translatePs2Path("C:\\outside.dat"), std::string("C:\\outside.dat"),
"drive-shaped paths should preserve non-Windows behavior");
#endif
});

tc.Run("fioOpen rejects paths outside configured roots", [](TestCase &t)
{
TestContext test;
const uint32_t pathAddr = GUEST_STRING_AREA_START + 0x300;
const std::filesystem::path escapedPath = test.paths.base / "outside.dat";
writeGuestString(test.rdram.data(), pathAddr, "mc0:../outside.dat");

setRegU32(test.ctx, 4, pathAddr);
setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC);
fioOpen(test.rdram.data(), &test.ctx, nullptr);

const int32_t fd = getRegS32(&test.ctx, 2);
if (fd >= 0)
{
setRegU32(test.ctx, 4, static_cast<uint32_t>(fd));
fioClose(test.rdram.data(), &test.ctx, nullptr);
}
t.Equals(fd, -1, "fioOpen should reject traversal outside mcRoot");
t.IsFalse(std::filesystem::exists(escapedPath),
"fioOpen should not create a file outside mcRoot");
});

tc.Run("sceMc open write read and close roundtrip through sync", [](TestCase &t)
{
TestContext test;
Expand Down