From 5cb7de6ba9655a834869361a3c48f5ed84be7f70 Mon Sep 17 00:00:00 2001 From: Doston Abdullayev Date: Sat, 10 Oct 2026 10:09:52 +0500 Subject: [PATCH] fix(libkernel): return every command-line argument as UTF-8 from getargv on Windows On Windows, getargc and getargv reported only the executable path, read with GetModuleFileNameA in the ANSI code page, and dropped every other argument. Linux returns the whole command line from /proc/self/cmdline, and the relinker's Windows entry stub already passes the guest entry all arguments converted to UTF-8. Parse GetCommandLineW with CommandLineToArgvW and convert each argument to UTF-8 with WC_ERR_INVALID_CHARS, as the entry stub does, so getargv matches the entry's argv and the Linux behaviour. The Linux path is unchanged. --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++++++- .../libs/prx/libkernel/System/src/Process.cpp | 28 +++++++++++++----- core/libs/tests/GuestProcessArguments.cpp | 29 +++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestProcessArguments.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b139729e2..845234267 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -67,7 +67,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32) + target_link_libraries(libkernel PRIVATE ws2_32 shell32) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) @@ -125,6 +125,15 @@ if(BUILD_TESTING) set_tests_properties(guest_process_time guest_process_time_counter PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_process_arguments_tests ${LIBS_INCLUDE_DIR}/tests/GuestProcessArguments.cpp) + target_include_directories(guest_process_arguments_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_process_arguments_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_process_arguments_tests) + add_test(NAME guest_process_arguments COMMAND guest_process_arguments_tests plain "two words" "ü日") + if(WIN32) + set_tests_properties(guest_process_arguments PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_library(guest_module_fixture SHARED ${LIBS_INCLUDE_DIR}/tests/GuestModuleFixture.cpp) target_include_directories(guest_module_fixture PRIVATE ${LIBS_INCLUDE_DIR}) add_custom_command(TARGET guest_module_fixture POST_BUILD diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 647f75814..7e03451c1 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -3,6 +3,7 @@ #define NOMINMAX #endif #include +#include #else #include #include @@ -46,17 +47,30 @@ constexpr unsigned freebsdWaitOptions = 0x8000003Fu; std::atomic gpoBits{0}; constexpr std::array openPsId{'A', 'n', 'y', 'P', 'S', '5', 'O', 'p', 'e', 'n', 'P', 's', 'I', 'd', 0, 1}; +#ifdef _WIN32 +std::string toUtf8(const wchar_t* value) { + const int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) + throw std::system_error(GetLastError(), std::system_category(), "Converting a process argument to UTF-8"); + std::string text(static_cast(size), '\0'); + if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value, -1, text.data(), size, nullptr, nullptr) != size) + throw std::system_error(GetLastError(), std::system_category(), "Converting a process argument to UTF-8"); + text.pop_back(); + return text; +} +#endif + class ProcessArguments { public: ProcessArguments() { #ifdef _WIN32 - std::array path{}; - const auto size = GetModuleFileNameA(nullptr, path.data(), static_cast(path.size())); - if (size == 0) - throw std::system_error(GetLastError(), std::system_category(), "Reading executable path"); - if (size >= path.size()) - throw std::runtime_error("Executable path exceeds the guest argument buffer"); - arguments.emplace_back(path.data(), size); + int count = 0; + const std::unique_ptr values(CommandLineToArgvW(GetCommandLineW(), &count), + [](LPWSTR* parsed) { LocalFree(parsed); }); + if (!values) + throw std::system_error(GetLastError(), std::system_category(), "Reading process arguments"); + for (int index = 0; index < count; ++index) + arguments.push_back(toUtf8(values.get()[index])); #else std::ifstream stream("/proc/self/cmdline", std::ios::binary); if (!stream) diff --git a/core/libs/tests/GuestProcessArguments.cpp b/core/libs/tests/GuestProcessArguments.cpp new file mode 100644 index 000000000..fa0f5584b --- /dev/null +++ b/core/libs/tests/GuestProcessArguments.cpp @@ -0,0 +1,29 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int APS5_VABI getargc_nid_postfix(void); +extern "C" const char** APS5_VABI getargv_nid_postfix(void); + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + const char* const expected[] = {"plain", "two words", "ü日"}; + constexpr int expectedCount = 1 + static_cast(sizeof(expected) / sizeof(expected[0])); + const int count = getargc_nid_postfix(); + const char** values = getargv_nid_postfix(); + Require(count == expectedCount, "getargc does not count every command-line argument"); + Require(values != nullptr && values[0] != nullptr && values[0][0] != '\0', "getargv has no program name"); + for (int index = 1; index < expectedCount; ++index) { + Require(values[index] != nullptr && std::strcmp(values[index], expected[index - 1]) == 0, + "getargv does not return a command-line argument as UTF-8"); + } + Require(values[count] == nullptr, "getargv is not terminated by a null pointer"); + return 0; +}