file_sys: Normalize guest paths and fix mount matching (#5207)

- Lexically normalize guest paths to resolve redundant slashes (//) and directory traversal (..)
- Prevent duplicate mount entries when registering overlays in MntPoints::Mount
- Sanitize paths before mount matching to prevent false matches against read-only mounts
- Validate and sanitize paths in sys_open
- Handle root directory ("/") queries in VFS and reachability checks
This commit is contained in:
Marcin Mitura
2026-10-09 08:13:16 -07:00
committed by GitHub
parent 149089be74
commit 661d6fd046
4 changed files with 119 additions and 40 deletions
@@ -1,9 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "common/types.h"
@@ -33,7 +34,7 @@ private:
};
#pragma pack(pop)
std::string_view guest_directory{};
std::string guest_directory{};
s64 previous_file_offset = -1;
void RebuildDirents(void);
+96 -36
View File
@@ -5,6 +5,7 @@
#include <array>
#include <unordered_set>
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/file_sys/backends/host_fs.h"
#include "core/file_sys/backends/zarchive_fs.h"
@@ -112,6 +113,20 @@ void MntPoints::Mount(const std::filesystem::path& host_folder, const std::strin
bool read_only) {
std::scoped_lock lock{m_mutex};
const auto guest_folder_sanitized = RemoveTrailingSlashes(guest_folder);
auto it = std::find_if(m_mnt_pairs.begin(), m_mnt_pairs.end(), [&](const MntPair& pair) {
return pair.mount == guest_folder_sanitized;
});
if (it != m_mnt_pairs.end()) {
if (it->host_path == host_folder && it->read_only == read_only) {
LOG_DEBUG(Kernel_Fs, "Mount point {} already mounted to {}, skipping",
guest_folder_sanitized, host_folder.string());
return;
}
LOG_INFO(Kernel_Fs, "Replacing mount point {} (old: {}, new: {})", guest_folder_sanitized,
it->host_path.string(), host_folder.string());
m_mnt_pairs.erase(it);
}
// Build the backend stack for this mount.
std::vector<std::shared_ptr<IBackend>> stack;
const bool eligible_for_overlays =
@@ -158,18 +173,43 @@ void MntPoints::UnmountAll() {
m_mnt_pairs.clear();
}
// Normalize a guest path. Resolves redundant slashes (e.g. Turok2 double slashes like
// /app0//game.kpf) and directory traversal (e.g. /app0/../savedata0/) per POSIX pathname resolution
// standards.
std::optional<std::string> MntPoints::SanitizeGuestPath(std::string_view path) {
if (path.empty() || path.length() > 255) {
return std::nullopt;
}
std::filesystem::path p(path);
std::string normalized = p.lexically_normal().generic_string();
if (normalized.length() > 255) {
return std::nullopt;
}
return normalized;
}
// Strip the mount prefix from a corrected guest path
std::string_view RelativeToMount(const std::string& corrected, const MntPoints::MntPair& mount) {
if (corrected.size() <= mount.mount.size() + 1) {
return {};
}
return std::string_view{corrected}.substr(mount.mount.size() + 1);
}
std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_read_only,
HostPathType path_type) {
// Evil games like Turok2 pass double slashes e.g /app0//game.kpf
std::string corrected_path(path);
size_t pos = corrected_path.find("//");
while (pos != std::string::npos) {
corrected_path.replace(pos, 2, "/");
pos = corrected_path.find("//", pos + 1);
}
if (path.length() > 255)
const auto sanitized = SanitizeGuestPath(path);
if (!sanitized) {
return "";
}
const std::string& corrected_path = *sanitized;
if (corrected_path == "/") {
if (is_read_only) {
*is_read_only = true;
}
return "/";
}
const auto* mount = GetMount(corrected_path);
if (!mount) {
@@ -207,10 +247,12 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
}
// Remove device (e.g /app0) from path to retrieve relative path.
const auto rel_path = std::string_view{corrected_path}.substr(mount->mount.size() + 1);
host_path /= rel_path;
patch_path /= rel_path;
mods_path /= rel_path;
const auto rel_path = RelativeToMount(corrected_path, *mount);
if (!rel_path.empty()) {
host_path /= rel_path;
patch_path /= rel_path;
mods_path /= rel_path;
}
if (path_type == HostPathType::Mod) {
return mods_path;
@@ -317,28 +359,6 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
return host_path;
}
// Normalize a guest path
std::optional<std::string> SanitizeGuestPath(std::string_view path) {
if (path.length() > 255) {
return std::nullopt;
}
std::string corrected(path);
size_t pos = corrected.find("//");
while (pos != std::string::npos) {
corrected.replace(pos, 2, "/");
pos = corrected.find("//", pos + 1);
}
return corrected;
}
// Strip the mount prefix from a corrected guest path
std::string_view RelativeToMount(const std::string& corrected, const MntPoints::MntPair& mount) {
if (corrected.size() <= mount.mount.size() + 1) {
return {};
}
return std::string_view{corrected}.substr(mount.mount.size() + 1);
}
void MntPoints::IterateDirectory(std::string_view guest_directory,
const IterateDirectoryCallback& callback) {
const auto corrected_opt = SanitizeGuestPath(guest_directory);
@@ -346,6 +366,22 @@ void MntPoints::IterateDirectory(std::string_view guest_directory,
return;
}
const auto& corrected = *corrected_opt;
if (corrected == "/") {
callback(std::filesystem::path("/."), false);
callback(std::filesystem::path("/.."), false);
std::scoped_lock lock{m_mutex};
std::unordered_set<std::string> seen;
for (const auto& mount : m_mnt_pairs) {
std::string name = mount.mount;
if (name.starts_with("/")) {
name = name.substr(1);
}
if (!name.empty() && seen.insert(name).second) {
callback(std::filesystem::path("/") / name, /*is_file=*/false);
}
}
return;
}
const auto mount = GetMount(corrected);
if (!mount || mount->backends.empty()) {
return;
@@ -422,6 +458,9 @@ bool MntPoints::Exists(std::string_view guest_path) {
if (!corrected) {
return false;
}
if (*corrected == "/") {
return true;
}
const auto mount = GetMount(*corrected);
if (!mount || mount->backends.empty()) {
return false;
@@ -440,6 +479,9 @@ bool MntPoints::IsDirectory(std::string_view guest_path) {
if (!corrected) {
return false;
}
if (*corrected == "/") {
return true;
}
const auto mount = GetMount(*corrected);
if (!mount || mount->backends.empty()) {
return false;
@@ -461,7 +503,7 @@ std::unique_ptr<IFile> MntPoints::Open(std::string_view guest_path, bool writabl
std::unique_ptr<IFile> MntPoints::Open(std::string_view guest_path,
Common::FS::FileAccessMode mode) {
const auto corrected = SanitizeGuestPath(guest_path);
if (!corrected) {
if (!corrected || *corrected == "/") {
return nullptr;
}
const auto mount = GetMount(*corrected);
@@ -492,6 +534,24 @@ std::unique_ptr<IDirectory> MntPoints::OpenDir(std::string_view guest_path) {
if (!corrected) {
return nullptr;
}
if (*corrected == "/") {
std::scoped_lock lock{m_mutex};
std::vector<DirEntry> entries;
std::unordered_set<std::string> seen;
for (const auto& mount : m_mnt_pairs) {
std::string name = mount.mount;
if (name.starts_with("/")) {
name = name.substr(1);
}
if (!name.empty() && seen.insert(name).second) {
entries.push_back(DirEntry{
.name = std::move(name),
.is_directory = true,
});
}
}
return std::make_unique<MergedDirectory>(std::move(entries));
}
const auto mount = GetMount(*corrected);
if (!mount || mount->backends.empty()) {
return nullptr;
+10 -1
View File
@@ -92,6 +92,9 @@ public:
/// when the file does not exist or is unreadable.
std::optional<std::vector<u8>> ReadFile(std::string_view guest_path);
/// Normalizes and validates a guest path (resolves dot and dot-dot components).
static std::optional<std::string> SanitizeGuestPath(std::string_view path);
const MntPair* GetMountFromHostPath(const std::string& host_path) {
std::scoped_lock lock{m_mutex};
const auto it = std::ranges::find_if(m_mnt_pairs, [&](const MntPair& mount) {
@@ -100,7 +103,13 @@ public:
return it == m_mnt_pairs.end() ? nullptr : &*it;
}
const MntPair* GetMount(const std::string& guest_path) {
const MntPair* GetMount(std::string_view guest_path) {
std::string normalized;
if (guest_path.find("..") != std::string_view::npos ||
guest_path.find("//") != std::string_view::npos) {
normalized = std::filesystem::path(guest_path).lexically_normal().generic_string();
guest_path = normalized;
}
std::scoped_lock lock{m_mutex};
const auto it = std::ranges::find_if(m_mnt_pairs, [&](const auto& mount) {
// When doing starts-with check, add a trailing slash to make sure we don't match
+10 -1
View File
@@ -125,7 +125,13 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
LOG_WARNING(Kernel_Fs, "flags {:#x} not fully handled", flags);
}
std::string_view path{raw_path};
const auto sanitized_path = Core::FileSys::MntPoints::SanitizeGuestPath(raw_path);
if (!sanitized_path) {
*__Error() = POSIX_EINVAL;
LOG_ERROR(Kernel_Fs, "Opening path {} failed, invalid path", raw_path);
return -1;
}
const std::string& path = *sanitized_path;
u32 handle = h->CreateHandle();
auto* file = h->GetFile(handle);
@@ -789,6 +795,9 @@ s32 PS4_SYSV_ABI sceKernelCheckReachability(const char* path) {
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
std::string_view guest_path{path};
if (guest_path == "/") {
return ORBIS_OK;
}
for (const auto& prefix : available_device | std::views::keys) {
if (guest_path.starts_with(prefix)) {
return ORBIS_OK;