From 7916e6022f2abac6b993764c95a1f3a22e3f9b1c Mon Sep 17 00:00:00 2001 From: xirvik Date: Sat, 19 Sep 2026 20:02:11 +0000 Subject: [PATCH] Stop the value command on the untrusted-safe allowlist crashing the daemon glibc leaves endptr unset for a small invalid base; ERANGE throws instead of clamping. --- src/command_ui.cc | 14 ++++++++++++-- test/src/test_command_dynamic.cc | 29 +++++++++++++++++++++++++++++ test/src/test_command_dynamic.h | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/command_ui.cc b/src/command_ui.cc index 1d53b1e6..e2b151db 100644 --- a/src/command_ui.cc +++ b/src/command_ui.cc @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -126,11 +127,20 @@ apply_value([[maybe_unused]] rpc::target_type target, const torrent::Object::lis if (args.front().is_value()) { val = args.front().as_value(); } else { - int base = args.size() > 1 ? args.back().is_value() ? - args.back().as_value() : strtol(args.back().as_string().c_str(), NULL, 10) : 10; + int64_t base = args.size() > 1 ? args.back().is_value() ? + args.back().as_value() : strtoll(args.back().as_string().c_str(), NULL, 10) : 10; + + if (base != 0 && (base < 2 || base > 36)) + throw torrent::input_error("'value' base must be 0 or between 2 and 36!"); + char* endptr = 0; + errno = 0; val = strtoll(args.front().as_string().c_str(), &endptr, base); + + if (errno == ERANGE) + throw torrent::input_error("Number out of range: " + args.front().as_string()); + while (*endptr == ' ' || *endptr == '\n') ++endptr; if (*endptr) throw torrent::input_error("Junk at end of number: " + args.front().as_string()); diff --git a/test/src/test_command_dynamic.cc b/test/src/test_command_dynamic.cc index f81fc274..a9eb63ab 100644 --- a/test/src/test_command_dynamic.cc +++ b/test/src/test_command_dynamic.cc @@ -2,6 +2,8 @@ #include "test/src/test_command_dynamic.h" +#include "helpers/assert.h" + #include "control.h" #include "globals.h" #include "rpc/parse_commands.h" @@ -82,3 +84,30 @@ TestCommandDynamic::test_insert_list() { CPPUNIT_ASSERT(filled.is_list()); CPPUNIT_ASSERT_EQUAL((size_t)2, filled.as_list().size()); } + +void +TestCommandDynamic::test_value_base() { + auto value = [](std::initializer_list objects) { + auto args = torrent::Object::create_list(); + + for (const auto& object : objects) + args.as_list().push_back(object); + + return rpc::commands.call_command("value", args).as_value(); + }; + + CPPUNIT_ASSERT_EQUAL(int64_t(10), value({"10"})); + CPPUNIT_ASSERT_EQUAL(int64_t(255), value({"ff", int64_t(16)})); + + // strtoll only defines base 0 and base 2 through 36. + ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(1)}); } ); + ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(37)}); } ); + ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(-1)}); } ); + + // An out-of-range base must not be narrowed into a valid one. + ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(1) << 40}); } ); + ASSERT_CATCH_INPUT_ERROR( { value({"ff", (int64_t(1) << 32) + 16}); } ); + + // A number too large for the result must be rejected, not clamped. + ASSERT_CATCH_INPUT_ERROR( { value({"99999999999999999999999"}); } ); +} diff --git a/test/src/test_command_dynamic.h b/test/src/test_command_dynamic.h index 11e9b06b..294ea2a6 100644 --- a/test/src/test_command_dynamic.h +++ b/test/src/test_command_dynamic.h @@ -8,6 +8,7 @@ class TestCommandDynamic : public test_fixture { CPPUNIT_TEST(test_get_set); CPPUNIT_TEST(test_old_style); CPPUNIT_TEST(test_insert_list); + CPPUNIT_TEST(test_value_base); CPPUNIT_TEST_SUITE_END(); @@ -20,6 +21,7 @@ public: void test_old_style(); void test_insert_list(); + void test_value_base(); private: std::unique_ptr m_test_main_thread;