From ddfd3bcfb9f087344f5161273c8ad6bb70f02859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 21:47:39 +0200 Subject: [PATCH] CmdLine: Take integer parameters in hex too, and refuse trailing junk They were read with sscanf("%d"), which stops at the first character it can't use, so --disable-hle=0x110 was accepted as 0 and quietly ran everything on our HLE. Now decimal or 0x hex, optionally signed, and the whole value has to parse. Up to 0xFFFFFFFF is taken, for full masks. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CmdLine.cpp | 31 ++++++++++++++++++++++++++++++- unittest/UnitTest.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 34651fb05e..1e1e8f3e8e 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -51,6 +52,34 @@ enum class ParseParamResult { BadValue, }; +// Decimal, or hex with 0x, optionally signed. Not base 0, which would read a leading 0 as octal. +// Up to 0xFFFFFFFF is accepted so a full 32-bit mask can be given, and wraps into the int. +static bool ParseIntParam(const std::string &value, int *out) { + const char *p = value.c_str(); + bool negative = false; + if (*p == '-' || *p == '+') { + negative = *p == '-'; + p++; + } + int base = 10; + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + base = 16; + p += 2; + } + // strtoull would take another sign or leading spaces here. + if (!isxdigit((unsigned char)*p)) { + return false; + } + char *end = nullptr; + errno = 0; + const unsigned long long magnitude = strtoull(p, &end, base); + if (*end != '\0' || errno == ERANGE || magnitude > (negative ? 0x80000000ULL : 0xFFFFFFFFULL)) { + return false; + } + *out = negative ? (int)(0 - (uint32_t)magnitude) : (int)(uint32_t)magnitude; + return true; +} + ParseParamResult SetValue(CommandLineOptions *options, const CommandLineParam ¶m, const std::string &value) { uint8_t *optionsPtr = reinterpret_cast(options); switch (param.type) { @@ -77,7 +106,7 @@ ParseParamResult SetValue(CommandLineOptions *options, const CommandLineParam &p { // Not std::stoi - that throws on junk, which takes the process down with no diagnostic. int v; - if (sscanf(value.c_str(), "%d", &v) != 1) { + if (!ParseIntParam(value, &v)) { PRINT_STDERR("Error: Invalid value for integer parameter --%s: '%s'.\n", param.longName, value.c_str()); return ParseParamResult::BadValue; } diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index da1860fb52..f82f42f424 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3391,6 +3391,33 @@ bool TestCmdLine() { EXPECT_TRUE(options.memStickInserted.has_value()); EXPECT_FALSE(options.memStickInserted.value_or(true)); } + // Integers take decimal or 0x hex. A leading 0 is still decimal, and a full 32-bit mask wraps + // into the int rather than being refused. + { + const char *argv[] = { + "ppsspp", + "--disable-hle=0x110", + "--force-hle=0xFFFFFFFF", + "--cpu-mhz=0333", + "--resolution-scale=-2", + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + EXPECT_EQ_INT((int)options.Parse(argc, argv, CmdLineMode::Headless), (int)CommandLineParseResult::Continue); + EXPECT_EQ_INT(options.disableHLE.value_or(0), 0x110); + EXPECT_EQ_INT(options.forceHLE.value_or(0), -1); + EXPECT_EQ_INT(options.lockedCPUSpeed.value_or(0), 333); + EXPECT_EQ_INT(options.resolutionScale.value_or(0), -2); + } + // Trailing junk used to be ignored, so 0x110 quietly meant 0. + { + const char *bad[] = { "--disable-hle=0x110junk", "--disable-hle=12abc", "--disable-hle=0x", "--disable-hle= 5", "--disable-hle=0x100000000", "--disable-hle=--1" }; + for (const char *arg : bad) { + const char *argv[] = { "ppsspp", arg }; + CommandLineOptions options; + EXPECT_EQ_INT((int)options.Parse(ARRAY_SIZE(argv), argv, CmdLineMode::Headless), (int)CommandLineParseResult::Error); + } + } // Test GL version override { const char *argv[] = {