[Linux] Handle NAND moves across mount points (#212)

* Fix NAND moves across mount points

* Harden cross-mount NAND move fallback

* Preserve directory copy on NAND move cleanup failure

* Clarify NAND move cleanup behavior

* Use exclusive scratch paths for NAND moves

* Copy NAND move directories into reserved destination

* Publish NAND move directories without replacement

(cherry picked from commit 83463764b8acda394e058b0c689a10b8561fc380)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tcyLz63pXjoYEwsFjfg8F
This commit is contained in:
Daan Vervacke authored and Claude committed 2026-10-05 12:56:12 +00:00
1 parent a532ffa6f3
commit 794b715d31
1 file changed
+145
+145
View File
@@ -4,6 +4,14 @@
#include "nand_internal.h"
#include <atomic>
#include <cerrno>
#ifdef __linux__
#include <linux/fs.h>
#include <sys/syscall.h>
#endif
// ============================================================================
// Local helpers
// ============================================================================
@@ -44,6 +52,32 @@ static FileHandle* ResolveNandFileHandle(const char* who, uint32_t fileInfoPtr)
// The synchronous RVL NAND* library
// ============================================================================
static bool RenameNoReplace(const std::filesystem::path& from,
const std::filesystem::path& to,
std::error_code& error) {
#ifdef _WIN32
if (MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_WRITE_THROUGH)) {
error.clear();
return true;
}
error = std::error_code(static_cast<int>(GetLastError()), std::system_category());
return false;
#elif defined(__linux__)
const int result = syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE);
if (result == 0) {
error.clear();
return true;
}
error = std::error_code(errno, std::generic_category());
return false;
#else
(void)from;
(void)to;
error = std::make_error_code(std::errc::operation_not_supported);
return false;
#endif
}
extern "C" int32_t NANDInit_HLE(void) {
// Initialize ISFS
ISFS_OpenLib_Initialize(&GetPersistentCpuContext());
@@ -348,6 +382,11 @@ extern "C" int32_t NANDCreateDir_HLE(uint32_t pathPtr, uint32_t perm, uint32_t a
PPC_NATIVE_OVERRIDE(8019BBE0, NANDCreateDir_HLE, int32_t, (uint32_t pathPtr, uint32_t perm, uint32_t attr), (pathPtr, perm, attr));
extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
// A cross-mount move is implemented as several host operations. Keep two
// guest moves from interleaving those operations and corrupting recovery.
static std::mutex moveMutex;
std::lock_guard<std::mutex> lock(moveMutex);
const char* srcPath = srcPathPtr ? (const char*)Memory::GetPointer(srcPathPtr) : nullptr;
const char* dstPath = dstPathPtr ? (const char*)Memory::GetPointer(dstPathPtr) : nullptr;
@@ -383,6 +422,112 @@ extern "C" int32_t NANDMove_HLE(uint32_t srcPathPtr, uint32_t dstPathPtr) {
return NAND_RESULT_OK;
}
// Flatpak can expose the managed NAND and an external Riivolution save
// directory as separate mounts. Linux cannot rename across mounts, but
// nandMove must still work for files such as banner.bin. Preserve the
// operation's semantics with a copy followed by source removal.
if (ec == std::errc::cross_device_link) {
static std::atomic<uint64_t> moveSequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
std::filesystem::path scratchHost;
std::error_code scratchEc;
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto name = ".nandmove-" + std::to_string(processId) + "-" +
std::to_string(moveSequence.fetch_add(1)) + "-" +
std::to_string(attempt);
const auto candidate = dstDirectoryHost / name;
scratchEc.clear();
if (std::filesystem::create_directory(candidate, scratchEc)) {
scratchHost = candidate;
break;
}
if (scratchEc && scratchEc != std::errc::file_exists) {
LogNandError("NANDMove", "failed to claim temporary directory '%s': %s",
HostPathText(candidate).c_str(), scratchEc.message().c_str());
return NAND_RESULT_UNKNOWN;
}
}
if (scratchHost.empty()) {
LogNandError("NANDMove", "could not claim a unique temporary directory");
return NAND_RESULT_UNKNOWN;
}
const bool sourceIsDirectory = IsDirectory(srcHost);
const std::filesystem::path tempHost = scratchHost / srcName;
const auto cleanupScratch = [&]() {
std::error_code cleanupEc;
std::filesystem::remove_all(scratchHost, cleanupEc);
if (cleanupEc) {
LogNandError("NANDMove", "failed to clean up temporary directory '%s': %s",
HostPathText(scratchHost).c_str(), cleanupEc.message().c_str());
}
};
std::error_code copyEc;
if (sourceIsDirectory) {
std::filesystem::copy(srcHost, tempHost,
std::filesystem::copy_options::recursive, copyEc);
} else {
std::filesystem::copy_file(srcHost, tempHost, copyEc);
}
if (copyEc) {
LogNandError("NANDMove", "cross-mount copy failed: %s", copyEc.message().c_str());
cleanupScratch();
return NAND_RESULT_UNKNOWN;
}
std::error_code publishEc;
if (sourceIsDirectory) {
RenameNoReplace(tempHost, dstHost, publishEc);
} else {
// link(2) and CreateHardLink do not replace an existing destination,
// unlike rename(2) on POSIX. Both paths are already on the target
// filesystem, so the link is a no-replace publication operation.
std::filesystem::create_hard_link(tempHost, dstHost, publishEc);
}
if (publishEc) {
LogNandError("NANDMove", "failed to publish cross-mount copy: %s",
publishEc.message().c_str());
cleanupScratch();
return NAND_RESULT_UNKNOWN;
}
cleanupScratch();
std::error_code removeEc;
std::filesystem::remove_all(srcHost, removeEc);
if (!removeEc) {
LogNandWarning("NANDMove", "used copy/remove fallback across mounts");
return NAND_RESULT_OK;
}
// Keep the source as the authoritative copy when cleanup fails. The
// destination was published atomically on its own mount; regular files
// are rolled back below, while directories keep the complete copy when
// their source removal was only partial. Cross-mount moves cannot
// provide crash-atomicity, so this is best effort.
LogNandError("NANDMove", "copy succeeded but source removal failed: %s",
removeEc.message().c_str());
if (sourceIsDirectory) {
// remove_all may have removed only part of a directory tree. Keep
// the complete published copy rather than rolling it back to a
// partially deleted source.
LogNandWarning("NANDMove", "preserving published directory copy after partial source removal");
} else {
std::error_code rollbackEc;
std::filesystem::remove_all(dstHost, rollbackEc);
if (rollbackEc) {
LogNandError("NANDMove", "failed to roll back destination '%s': %s",
HostPathText(dstHost).c_str(), rollbackEc.message().c_str());
}
}
return NAND_RESULT_UNKNOWN;
}
LogNandError("NANDMove", "FAILED error=%d message='%s'", ec.value(), ec.message().c_str());
return NAND_RESULT_UNKNOWN;
}