mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-10-12 02:22:27 +00:00
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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user