From 217769d80185aab7e4f0cb65c68e8741850a26d7 Mon Sep 17 00:00:00 2001 From: Elad <18193363+elad335@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:32:25 +0300 Subject: [PATCH] sys_fs: Fix ENOTDIR check for paths ending with /dot or /dotdot They must be directories, even file-path/dot points to itself. --- rpcs3/Emu/Cell/lv2/sys_fs.cpp | 90 ++++++++++++++++++++++++++++------- rpcs3/Emu/Cell/lv2/sys_fs.h | 2 +- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_fs.cpp b/rpcs3/Emu/Cell/lv2/sys_fs.cpp index e817b602c..771ff2e1a 100644 --- a/rpcs3/Emu/Cell/lv2/sys_fs.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_fs.cpp @@ -147,12 +147,52 @@ bool verify_mself(const fs::file& mself_file) } // TODO: May not be thread-safe (or even, process-safe) -bool has_non_directory_components(std::string_view path) +bool has_non_directory_components(std::string_view path, bool ends_with_delim_dot_or_dotdot) { std::string sub_path{path}; fs::stat_t stat{}; + if (ends_with_delim_dot_or_dotdot) + { + // Special case: /dev_bdvd/PS3_GAME/USRDIR/EBOOT.BIN/./ is not allowed + // Because lookup inside a file is not allowed, even if /./ points to itself + // This check is relevant only for when it is at the end of path + // Because if it is other path componenets the functions handle it fine + // We cannot know this from `path` argument because it is resolved by VFS + std::string_view edited_path{path}; + + edited_path = edited_path.substr(0, edited_path.find_last_not_of(fs::delim) + 1); + + const auto elem = edited_path.substr(edited_path.find_last_of(fs::delim) + 1); + + if (elem == "." || elem == "..") + { + edited_path.remove_suffix(elem.size()); + edited_path = edited_path.substr(0, edited_path.find_last_not_of(fs::delim) + 1); + } + + if (fs::get_stat(std::string{edited_path}, stat)) + { + if (!stat.is_directory) + { + sys_fs.warning("has_non_directory_components() returned an affirmative (edited_path=%s)", edited_path); + } + + return !stat.is_directory; + } + + if (fs::g_tls_error == fs::error::notdir) + { + return true; + } + + if (fs::g_tls_error != fs::error::inval && fs::g_tls_error != fs::error::noent) + { + fmt::throw_exception("sys_fs: fs::get_stat() failed with error '%s' (edited_path=%s)", fs::g_tls_error, edited_path); + } + } + while (true) { std::string_view path_sv = fs::get_parent_dir_view(sub_path); @@ -160,7 +200,7 @@ bool has_non_directory_components(std::string_view path) if (sv_size >= sub_path.size()) { - sys_fs.error("has_non_directory_components() did not find a directory! (path=%s, sub_path=%s)", path, sub_path); + sys_fs.warning("has_non_directory_components() did not find a directory! (path=%s, sub_path=%s)", path, sub_path); break; } @@ -172,13 +212,18 @@ bool has_non_directory_components(std::string_view path) { if (!stat.is_directory) { - sys_fs.error("has_non_directory_components() returned an affirmative (sub_path=%s)", sub_path); + sys_fs.warning("has_non_directory_components() returned an affirmative (sub_path=%s)", sub_path); } return !stat.is_directory; } - if (fs::g_tls_error != fs::error::noent) + if (fs::g_tls_error == fs::error::notdir) + { + return true; + } + + if (fs::g_tls_error != fs::error::inval && fs::g_tls_error != fs::error::noent) { fmt::throw_exception("sys_fs: fs::get_stat() failed with error '%s' (sub_path=%s)", fs::g_tls_error, sub_path); } @@ -187,6 +232,17 @@ bool has_non_directory_components(std::string_view path) return false; } +// Takes virtual path only +bool ends_with_delim_dot_or_dotdot(std::string_view vpath) +{ + if (vpath.find_last_not_of('/') != vpath.size() - 1) + { + return true; + } + + return vpath.ends_with("/.") || vpath.ends_with("/.."); +} + lv2_fs_mount_info_map::lv2_fs_mount_info_map() { for (auto mp = &g_mp_sys_dev_root; mp; mp = mp->next) // Scan and keep track of pre-mounted devices @@ -879,7 +935,7 @@ error_code sys_fs_test(ppu_thread&, u32 arg1, u32 arg2, vm::ptr arg3, u32 a return CELL_OK; } -lv2_file::open_raw_result_t lv2_file::open_raw(const std::string& local_path, s32 flags, bool has_write_access, lv2_file_type type, const lv2_fs_mount_info& mp) +lv2_file::open_raw_result_t lv2_file::open_raw(const std::string& local_path, s32 flags, bool has_write_access, lv2_file_type type, const lv2_fs_mount_info& mp, bool ends_with_dot) { // TODO: other checks for path @@ -1010,7 +1066,7 @@ lv2_file::open_raw_result_t lv2_file::open_raw(const std::string& local_path, s3 case fs::error::isdir: return {CELL_EISDIR}; default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_dot)) { return {CELL_ENOTDIR}; } @@ -1152,7 +1208,7 @@ lv2_file::open_result_t lv2_file::open(std::string_view vpath, s32 flags, s32 /* } } - auto [error, file] = open_raw(local_path, flags, has_fs_write_rights(vpath), type, mp); + auto [error, file] = open_raw(local_path, flags, has_fs_write_rights(vpath), type, mp, ends_with_delim_dot_or_dotdot(vpath)); return {.error = error, .ppath = std::move(path), .real_path = std::move(local_path), .file = std::move(file), .type = type}; } @@ -1492,7 +1548,7 @@ error_code sys_fs_opendir(ppu_thread& ppu, vm::cptr path, vm::ptr fd) } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -1723,7 +1779,7 @@ error_code sys_fs_stat(ppu_thread& ppu, vm::cptr path, vm::ptr } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -1868,7 +1924,7 @@ error_code sys_fs_mkdir(ppu_thread& ppu, vm::cptr path, s32 mode) } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -1940,7 +1996,7 @@ error_code sys_fs_rename(ppu_thread& ppu, vm::cptr from, vm::cptr to case fs::error::exist: return {CELL_EEXIST, to}; default: { - if (has_non_directory_components(local_from)) + if (has_non_directory_components(local_from, ends_with_delim_dot_or_dotdot(vfrom))) { return {CELL_ENOTDIR, from}; } @@ -2002,7 +2058,7 @@ error_code sys_fs_rmdir(ppu_thread& ppu, vm::cptr path) case fs::error::notempty: return {CELL_ENOTEMPTY, path}; default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, vpath }; } @@ -2070,7 +2126,7 @@ error_code sys_fs_unlink(ppu_thread& ppu, vm::cptr path) } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -2915,7 +2971,7 @@ error_code sys_fs_get_block_size(ppu_thread& ppu, vm::cptr path, vm::ptr path, u64 size) } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -3103,7 +3159,7 @@ error_code sys_fs_chmod(ppu_thread&, vm::cptr path, s32 mode) } default: { - if (has_non_directory_components(local_path)) + if (has_non_directory_components(local_path, ends_with_delim_dot_or_dotdot(vpath))) { return { CELL_ENOTDIR, path }; } @@ -3250,7 +3306,7 @@ error_code sys_fs_utime(ppu_thread& ppu, vm::cptr path, vm::cptr