Compare commits

...

4 Commits

Author SHA1 Message Date
xirvik b24db0eaa5 Do not dereference missing text nodes in the tinyxml2 backend.
Empty elements and element children crashed the parser on a null.
2026-08-14 13:16:17 +02:00
Jari Sundell 6fa139f7bf Reduce max_string_pad_size from 1<<20 to 1<<14 2026-08-14 12:46:37 +02:00
xirvik 68904ba005 Limit the padding string.rpad and string.lpad will build.
An untrusted caller could exhaust memory or the response limit.
2026-08-14 12:46:37 +02:00
xirvik b401068246 Let the choke group list own its groups.
Each choke_group.insert leaked a group, and size reported a stale copy.
2026-08-14 12:15:36 +02:00
3 changed files with 43 additions and 14 deletions
+11 -9
View File
@@ -1,5 +1,7 @@
#include "config.h"
#include <memory>
#include <torrent/download/resource_manager.h>
#include <torrent/download/choke_group.h>
#include <torrent/download/choke_queue.h>
@@ -111,7 +113,7 @@ apply_cg_all_update_balance(bool is_up) {
//
#else
std::vector<torrent::choke_group*> cg_list_hack;
std::vector<std::unique_ptr<torrent::choke_group>> cg_list_hack;
int64_t
cg_get_index(const torrent::Object& raw_args) {
@@ -121,7 +123,7 @@ cg_get_index(const torrent::Object& raw_args) {
if (arg.is_string()) {
if (!rpc::parse_whole_value_nothrow(arg.as_string().c_str(), &index)) {
auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](torrent::choke_group* cg) { return arg.as_string() == cg->name(); });
auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg.as_string() == cg->name(); });
if (itr == cg_list_hack.end())
throw torrent::input_error("Choke group not found.");
@@ -149,7 +151,7 @@ cg_get_group(const torrent::Object& raw_args) {
if ((size_t)index >= cg_list_hack.size())
throw torrent::input_error("Choke group not found.");
return cg_list_hack.at(index);
return cg_list_hack.at(index).get();
}
int64_t cg_d_group(core::Download* download) { return download->group(); }
@@ -162,7 +164,7 @@ torrent::Object
apply_cg_list() {
torrent::Object::list_type result;
for (auto itr : cg_list_hack)
for (const auto& itr : cg_list_hack)
result.push_back(itr->name());
return torrent::Object::from_list(result);
@@ -180,10 +182,10 @@ apply_cg_insert(const std::string& arg) {
if (rpc::parse_whole_value_nothrow(arg.c_str(), &dummy))
throw torrent::input_error("Cannot use a value string as choke group name.");
if (arg.empty() || std::any_of(cg_list_hack.begin(), cg_list_hack.end(), [&arg](auto cg) { return arg == cg->name(); }))
if (arg.empty() || std::any_of(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg == cg->name(); }))
throw torrent::input_error("Duplicate name for choke group.");
cg_list_hack.push_back(new torrent::choke_group());
cg_list_hack.push_back(std::make_unique<torrent::choke_group>());
cg_list_hack.back()->set_name(arg);
cg_list_hack.back()->up_queue()->set_heuristics(torrent::HEURISTICS_UPLOAD_LEECH);
@@ -194,7 +196,7 @@ apply_cg_insert(const std::string& arg) {
torrent::Object
apply_cg_index_of(const std::string& arg) {
auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](torrent::choke_group* cg) { return arg == cg->name(); });
auto itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(), [&arg](const auto& cg) { return arg == cg->name(); });
if (itr == cg_list_hack.end())
throw torrent::input_error("Choke group not found.");
@@ -206,7 +208,7 @@ torrent::Object
apply_cg_all_update_balance(bool is_up) {
LT_LOG_SUBSYSTEM("apply update balance: hack is_up:%i", (int)is_up);
for (auto itr : cg_list_hack) {
for (const auto& itr : cg_list_hack) {
if (is_up)
itr->up_queue()->balance();
else
@@ -346,7 +348,7 @@ initialize_command_groups() {
#else
apply_cg_insert("default");
CMD_ANY ("choke_group.size", std::bind(&std::vector<torrent::choke_group*>::size, cg_list_hack));
CMD_ANY ("choke_group.size", [](auto, auto) { return (int64_t)cg_list_hack.size(); });
CMD_ANY_STRING ("choke_group.index_of", std::bind(&apply_cg_index_of, std::placeholders::_2));
#endif
+6
View File
@@ -19,6 +19,8 @@
namespace {
constexpr int64_t max_string_pad_size = 1 << 14;
const std::string whitespace_characters = " \t\n\r\f\v";
// The byte offset of every utf-8 character in 'text', terminated by the offset
@@ -249,10 +251,14 @@ apply_string_pad(const char* name, const torrent::Object::list_type& args, bool
if (pad_size <= text_length || padding.empty())
return text;
if (pad_size - text_length > max_string_pad_size)
throw torrent::input_error(std::string(name) + ": padding is too large.");
auto padding_offsets = utf8_offsets(padding);
auto padding_length = static_cast<int64_t>(padding_offsets.size() - 1);
std::string result;
result.reserve(pad_size - text_length);
for (int64_t i = 0; i < pad_size - text_length; i++) {
auto index = static_cast<size_t>(i % padding_length);
+26 -5
View File
@@ -49,13 +49,23 @@ element_access(const tinyxml2::XMLElement* elem, std::initializer_list<std::stri
return result;
}
const char*
element_text_value(const tinyxml2::XMLNode* node) {
auto text = node->ToText();
if (text == nullptr)
throw rpc_error(XMLRPC_TYPE_ERROR, "expected a text value");
return text->Value();
}
long long
element_to_int(const tinyxml2::XMLNode* elem) {
char* pos;
if (elem->FirstChild() == nullptr) {
throw rpc_error(XMLRPC_TYPE_ERROR, "unable to parse empty integer");
}
auto str = elem->FirstChild()->ToText()->Value();
auto str = element_text_value(elem->FirstChild());
auto result = std::strtoll(str, &pos, 10);
if (pos == str || *pos != '\0')
throw rpc_error(XMLRPC_TYPE_ERROR, "unable to parse integer value");
@@ -85,7 +95,7 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
if (child_element == nullptr)
return torrent::Object("");
return torrent::Object(child_element->ToText()->Value());
return torrent::Object(element_text_value(child_element));
} else if (std::strncmp(root_type, "int", sizeof("int")) == 0 ||
std::strncmp(root_type, "i4", sizeof("i4")) == 0 ||
@@ -98,7 +108,7 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
if (child_element == nullptr)
throw rpc_error(XMLRPC_TYPE_ERROR, "empty boolean element");
auto boolean_text = std::string(child_element->ToText()->Value());
auto boolean_text = std::string(element_text_value(child_element));
if (boolean_text == "1")
return torrent::Object((int64_t)1);
@@ -129,7 +139,12 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
if (name_element == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "struct member missing name element");
map[name_element->GetText()] = std::move(xml_value_to_object(child->FirstChildElement("value")));
auto name_text = name_element->GetText();
if (name_text == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "struct member has an empty name element");
map[name_text] = std::move(xml_value_to_object(child->FirstChildElement("value")));
}
return map_raw;
@@ -140,7 +155,7 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
if (child_element == nullptr)
return torrent::Object("");
return torrent::Object(utils::decode_base64(utils::remove_newlines(child_element->ToText()->Value())));
return torrent::Object(utils::decode_base64(utils::remove_newlines(element_text_value(child_element))));
} else {
throw rpc_error(XMLRPC_INTERNAL_ERROR, "received unsupported value type: " + std::string(root_type));
@@ -300,6 +315,9 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
if (doc->FirstChildElement("methodCall")->FirstChildElement("methodName") == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "methodName element not found");
auto method_name = doc->FirstChildElement("methodCall")->FirstChildElement("methodName")->GetText();
if (method_name == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "methodName element is empty");
torrent::Object result;
// Add a shim here for system.multicall to allow better code reuse, and
@@ -310,6 +328,9 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
auto parent_elements = element_access(doc->RootElement(), {"params", "param", "value", "array", "data"});
for (auto child = parent_elements->FirstChildElement("value"); child; child = child->NextSiblingElement("value")) {
auto sub_method_name = element_access(child, {"struct", "member", "value", "string"})->GetText();
if (sub_method_name == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall methodName element is empty");
// If sub_params ends up a nullptr at the end of this if-chian,
// execute_command will turn it into an empty list
auto sub_params = element_access(child, {"struct", "member"});