ref:1583e7c4a7e99538c7700315a1d2a2101c6d2812

Merge commit from fork

SHA: 1583e7c4a7e99538c7700315a1d2a2101c6d2812
Author: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: 2026-09-02 23:44
Parents: b91470f
3 files changed +302 -1
Type
src/input.cpp +106 −1
@@ -13,10 +13,12 @@
#include <bitset>
#include <chrono>
#include <cmath>
#include <cstring>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <thread>
@@ -294,7 +296,7 @@
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event; ///< Touch port event.
platf::feedback_queue_t feedback_queue; ///< Queue used to deliver controller feedback to the platform backend.
std::list<std::vector<uint8_t>> input_queue; ///< Pending raw input packets waiting for processing.
std::list<std::vector<uint8_t>> input_queue; ///< Validated input packets waiting for processing.
std::mutex input_queue_lock; ///< Input queue lock.
thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; ///< Mouse left button timeout.
@@ -1659,6 +1661,82 @@
}
/**
* @brief Validate the declared and available sizes of a fixed-size input packet.
*
* @tparam Packet Protocol packet structure.
* @param packet Raw packet bytes.
* @param declared_size Packet size declared after the size field.
* @return True when both sizes safely contain the fixed packet structure.
*/
template<typename Packet>
bool validate_fixed_input_packet(std::span<const std::uint8_t> packet, std::uint32_t declared_size) {
constexpr auto expected_size = static_cast<std::uint32_t>(sizeof(Packet) - sizeof(std::uint32_t));
if (declared_size != expected_size) {
return false;
}
return packet.size() >= sizeof(Packet);
}
/**
* @brief Validate an input packet before any typed access or batching.
*
* @param packet Raw packet bytes.
* @param parsed_header Optional destination for the safely copied packet header.
* @return True when the packet is large enough for its declared and protocol-specific fields.
*/
bool validate_input_packet(std::span<const std::uint8_t> packet, NV_INPUT_HEADER *parsed_header = nullptr) {
if (packet.size() < sizeof(NV_INPUT_HEADER)) {
return false;
}
NV_INPUT_HEADER header {};
std::memcpy(&header, packet.data(), sizeof(header));
if (parsed_header) {
*parsed_header = header;
}
const auto declared_size = util::endian::big(header.size);
if (declared_size < sizeof(header.magic) || declared_size > packet.size() - sizeof(header.size)) {
return false;
}
switch (util::endian::little(header.magic)) {
case MOUSE_MOVE_REL_MAGIC_GEN5:
return validate_fixed_input_packet<NV_REL_MOUSE_MOVE_PACKET>(packet, declared_size);
case MOUSE_MOVE_ABS_MAGIC:
return validate_fixed_input_packet<NV_ABS_MOUSE_MOVE_PACKET>(packet, declared_size);
case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5:
case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5:
return validate_fixed_input_packet<NV_MOUSE_BUTTON_PACKET>(packet, declared_size);
case SCROLL_MAGIC_GEN5:
return validate_fixed_input_packet<NV_SCROLL_PACKET>(packet, declared_size);
case SS_HSCROLL_MAGIC:
return validate_fixed_input_packet<SS_HSCROLL_PACKET>(packet, declared_size);
case KEY_DOWN_EVENT_MAGIC:
case KEY_UP_EVENT_MAGIC:
return validate_fixed_input_packet<NV_KEYBOARD_PACKET>(packet, declared_size);
case UTF8_TEXT_EVENT_MAGIC:
return declared_size - sizeof(header.magic) <= UTF8_TEXT_EVENT_MAX_COUNT;
case MULTI_CONTROLLER_MAGIC_GEN5:
return validate_fixed_input_packet<NV_MULTI_CONTROLLER_PACKET>(packet, declared_size);
case SS_TOUCH_MAGIC:
return validate_fixed_input_packet<SS_TOUCH_PACKET>(packet, declared_size);
case SS_PEN_MAGIC:
return validate_fixed_input_packet<SS_PEN_PACKET>(packet, declared_size);
case SS_CONTROLLER_ARRIVAL_MAGIC:
return validate_fixed_input_packet<SS_CONTROLLER_ARRIVAL_PACKET>(packet, declared_size);
case SS_CONTROLLER_TOUCH_MAGIC:
return validate_fixed_input_packet<SS_CONTROLLER_TOUCH_PACKET>(packet, declared_size);
case SS_CONTROLLER_MOTION_MAGIC:
return validate_fixed_input_packet<SS_CONTROLLER_MOTION_PACKET>(packet, declared_size);
case SS_CONTROLLER_BATTERY_MAGIC:
return validate_fixed_input_packet<SS_CONTROLLER_BATTERY_PACKET>(packet, declared_size);
default:
return true;
}
}
/**
* @brief Enumerates supported batch result options.
*/
enum class batch_result_e {
@@ -2041,6 +2119,20 @@
* @param input_data The input message.
*/
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
NV_INPUT_HEADER header {};
if (!validate_input_packet(input_data, &header)) {
if (input_data.size() >= sizeof(header)) {
BOOST_LOG(warning)
<< "Dropping malformed input packet type ["sv
<< util::hex(util::endian::little(header.magic)).to_string_view()
<< "] with declared payload size ["sv << util::endian::big(header.size)
<< "] and actual size ["sv << input_data.size() << ']';
} else {
BOOST_LOG(warning) << "Dropping malformed input packet with actual size ["sv << input_data.size() << ']';
}
return;
}
{
std::lock_guard<std::mutex> lg(input->input_queue_lock);
input->input_queue.push_back(std::move(input_data));
@@ -2291,6 +2383,19 @@
void release_held_keys() {
reset_keyboard_keys();
}
bool is_valid_input_packet(std::span<const std::uint8_t> packet) {
return ::input::validate_input_packet(packet);
}
std::size_t queued_input_packet_count(const std::shared_ptr<input_t> &input) {
if (!input) {
return 0;
}
std::lock_guard lock {input->input_queue_lock};
return input->input_queue.size();
}
} // namespace testing
#endif
src/input.h +21 −0
@@ -5,8 +5,13 @@
#pragma once
// standard includes
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <span>
#include <string_view>
#include <vector>
// local includes
#include "platform/common.h"
@@ -147,5 +152,21 @@
* @brief Release every key Sunshine tracks as pressed, as a disconnect does.
*/
void release_held_keys();
/**
* @brief Validate raw protocol input bytes for a unit test.
*
* @param packet Raw packet bytes.
* @return True when the packet is safe for typed processing.
*/
bool is_valid_input_packet(std::span<const std::uint8_t> packet);
/**
* @brief Return the number of validated packets waiting in a test input queue.
*
* @param input Shared stream input state.
* @return Number of queued packets, or zero for an empty input pointer.
*/
std::size_t queued_input_packet_count(const std::shared_ptr<input_t> &input);
} // namespace testing
#endif
tests/unit/test_input.cpp +175 −0
@@ -7,16 +7,56 @@
#include "../tests_common.h"
// standard includes
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
#include <vector>
// moonlight-common-c includes
extern "C" {
#include <moonlight-common-c/src/Input.h>
}
// local includes
#include "src/config.h"
#include "src/input.h"
#include "src/platform/virtualhid_input.h"
#include "src/utility.h"
namespace {
/**
* @brief Protocol metadata for a fixed-size input packet.
*/
struct input_packet_spec_t {
std::uint32_t magic; ///< Packet type identifier.
std::size_t size; ///< Complete fixed packet size.
};
/**
* @brief Create raw input bytes with a caller-selected header and buffer size.
*
* @param magic Packet type identifier.
* @param declared_size Packet size declared after the size field.
* @param actual_size Number of bytes available in the packet buffer.
* @return Raw packet bytes.
*/
std::vector<std::uint8_t> make_input_packet(std::uint32_t magic, std::uint32_t declared_size, std::size_t actual_size) {
std::vector<std::uint8_t> packet(actual_size);
const NV_INPUT_HEADER header {
util::endian::big(declared_size),
util::endian::little(magic),
};
if (!packet.empty()) {
std::memcpy(packet.data(), &header, std::min(packet.size(), sizeof(header)));
}
return packet;
}
/**
* @brief Fixture that installs a fake global virtual input backend.
*/
class InputGamepadSessionTest: public ::testing::Test {
@@ -74,6 +114,141 @@
config::input_t original_input_; ///< Input configuration restored after each test.
};
} // namespace
TEST(InputPacketValidationTest, RejectsEveryBufferShorterThanTheHeader) {
for (std::size_t actual_size = 0; actual_size < sizeof(NV_INPUT_HEADER); ++actual_size) {
const auto packet = make_input_packet(UTF8_TEXT_EVENT_MAGIC, sizeof(std::uint32_t), actual_size);
EXPECT_FALSE(input::testing::is_valid_input_packet(packet)) << "actual_size=" << actual_size;
}
}
TEST(InputPacketValidationTest, RejectsDeclaredSizesOutsideTheAvailableBuffer) {
constexpr std::uint32_t unknown_magic = 0x12345678;
for (std::uint32_t declared_size = 0; declared_size < sizeof(std::uint32_t); ++declared_size) {
const auto packet = make_input_packet(unknown_magic, declared_size, sizeof(NV_INPUT_HEADER));
EXPECT_FALSE(input::testing::is_valid_input_packet(packet)) << "declared_size=" << declared_size;
}
const auto truncated_packet = make_input_packet(unknown_magic, sizeof(std::uint32_t) + 1, sizeof(NV_INPUT_HEADER));
EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_packet));
const auto overflowing_size = make_input_packet(
unknown_magic,
std::numeric_limits<std::uint32_t>::max(),
sizeof(NV_INPUT_HEADER)
);
EXPECT_FALSE(input::testing::is_valid_input_packet(overflowing_size));
}
TEST(InputPacketValidationTest, ValidatesUnicodePacketBoundsWithoutOverflow) {
const auto empty_packet = make_input_packet(UTF8_TEXT_EVENT_MAGIC, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER));
EXPECT_TRUE(input::testing::is_valid_input_packet(empty_packet));
constexpr std::uint32_t maximum_declared_size = sizeof(std::uint32_t) + UTF8_TEXT_EVENT_MAX_COUNT;
const auto maximum_packet = make_input_packet(
UTF8_TEXT_EVENT_MAGIC,
maximum_declared_size,
sizeof(std::uint32_t) + maximum_declared_size
);
EXPECT_TRUE(input::testing::is_valid_input_packet(maximum_packet));
const auto oversized_text = make_input_packet(
UTF8_TEXT_EVENT_MAGIC,
maximum_declared_size + 1,
sizeof(std::uint32_t) + maximum_declared_size + 1
);
EXPECT_FALSE(input::testing::is_valid_input_packet(oversized_text));
const auto truncated_text = make_input_packet(UTF8_TEXT_EVENT_MAGIC, maximum_declared_size, sizeof(NV_INPUT_HEADER));
EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_text));
}
TEST(InputPacketValidationTest, EnforcesEveryFixedPacketSize) {
constexpr std::array packet_specs {
input_packet_spec_t {MOUSE_MOVE_REL_MAGIC_GEN5, sizeof(NV_REL_MOUSE_MOVE_PACKET)},
input_packet_spec_t {MOUSE_MOVE_ABS_MAGIC, sizeof(NV_ABS_MOUSE_MOVE_PACKET)},
input_packet_spec_t {MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5, sizeof(NV_MOUSE_BUTTON_PACKET)},
input_packet_spec_t {MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5, sizeof(NV_MOUSE_BUTTON_PACKET)},
input_packet_spec_t {SCROLL_MAGIC_GEN5, sizeof(NV_SCROLL_PACKET)},
input_packet_spec_t {SS_HSCROLL_MAGIC, sizeof(SS_HSCROLL_PACKET)},
input_packet_spec_t {KEY_DOWN_EVENT_MAGIC, sizeof(NV_KEYBOARD_PACKET)},
input_packet_spec_t {KEY_UP_EVENT_MAGIC, sizeof(NV_KEYBOARD_PACKET)},
input_packet_spec_t {MULTI_CONTROLLER_MAGIC_GEN5, sizeof(NV_MULTI_CONTROLLER_PACKET)},
input_packet_spec_t {SS_TOUCH_MAGIC, sizeof(SS_TOUCH_PACKET)},
input_packet_spec_t {SS_PEN_MAGIC, sizeof(SS_PEN_PACKET)},
input_packet_spec_t {SS_CONTROLLER_ARRIVAL_MAGIC, sizeof(SS_CONTROLLER_ARRIVAL_PACKET)},
input_packet_spec_t {SS_CONTROLLER_TOUCH_MAGIC, sizeof(SS_CONTROLLER_TOUCH_PACKET)},
input_packet_spec_t {SS_CONTROLLER_MOTION_MAGIC, sizeof(SS_CONTROLLER_MOTION_PACKET)},
input_packet_spec_t {SS_CONTROLLER_BATTERY_MAGIC, sizeof(SS_CONTROLLER_BATTERY_PACKET)},
};
for (const auto &[magic, size] : packet_specs) {
const auto declared_size = static_cast<std::uint32_t>(size - sizeof(std::uint32_t));
const auto valid_packet = make_input_packet(magic, declared_size, size);
EXPECT_TRUE(input::testing::is_valid_input_packet(valid_packet)) << "magic=" << magic;
const auto trailing_bytes = make_input_packet(magic, declared_size, size + 8);
EXPECT_TRUE(input::testing::is_valid_input_packet(trailing_bytes)) << "magic=" << magic;
const auto wrong_declared_size = make_input_packet(magic, declared_size - 1, size);
EXPECT_FALSE(input::testing::is_valid_input_packet(wrong_declared_size)) << "magic=" << magic;
const auto oversized_declared_size = make_input_packet(magic, declared_size + 1, size + 1);
EXPECT_FALSE(input::testing::is_valid_input_packet(oversized_declared_size)) << "magic=" << magic;
const auto truncated_packet = make_input_packet(magic, declared_size, size - 1);
EXPECT_FALSE(input::testing::is_valid_input_packet(truncated_packet)) << "magic=" << magic;
}
}
TEST(InputPacketValidationTest, PreservesUnknownPacketHandlingWithinDeclaredBounds) {
constexpr std::uint32_t unknown_magic = 0x12345678;
const auto packet = make_input_packet(unknown_magic, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER));
EXPECT_TRUE(input::testing::is_valid_input_packet(packet));
}
TEST_F(InputGamepadSessionTest, RejectsMalformedBatchablePacketsAtQueueIngress) {
ASSERT_FALSE(task_pool.running());
const std::shared_ptr<input::input_t> empty_input;
EXPECT_EQ(input::testing::queued_input_packet_count(empty_input), 0);
auto stream_input = input::alloc(std::make_shared<safe::mail_raw_t>(), "packet-validation-client");
ASSERT_NE(stream_input, nullptr);
auto short_header = make_input_packet(SS_PEN_MAGIC, sizeof(std::uint32_t), sizeof(NV_INPUT_HEADER) - 1);
input::passthrough(stream_input, std::move(short_header));
EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), 0);
constexpr std::array batchable_packet_specs {
input_packet_spec_t {MOUSE_MOVE_REL_MAGIC_GEN5, sizeof(NV_REL_MOUSE_MOVE_PACKET)},
input_packet_spec_t {MOUSE_MOVE_ABS_MAGIC, sizeof(NV_ABS_MOUSE_MOVE_PACKET)},
input_packet_spec_t {SCROLL_MAGIC_GEN5, sizeof(NV_SCROLL_PACKET)},
input_packet_spec_t {SS_HSCROLL_MAGIC, sizeof(SS_HSCROLL_PACKET)},
input_packet_spec_t {MULTI_CONTROLLER_MAGIC_GEN5, sizeof(NV_MULTI_CONTROLLER_PACKET)},
input_packet_spec_t {SS_TOUCH_MAGIC, sizeof(SS_TOUCH_PACKET)},
input_packet_spec_t {SS_PEN_MAGIC, sizeof(SS_PEN_PACKET)},
input_packet_spec_t {SS_CONTROLLER_TOUCH_MAGIC, sizeof(SS_CONTROLLER_TOUCH_PACKET)},
input_packet_spec_t {SS_CONTROLLER_MOTION_MAGIC, sizeof(SS_CONTROLLER_MOTION_PACKET)},
};
std::size_t valid_packet_count = 0;
for (const auto &[magic, size] : batchable_packet_specs) {
const auto declared_size = static_cast<std::uint32_t>(size - sizeof(std::uint32_t));
auto truncated_packet = make_input_packet(magic, declared_size, size - 1);
input::passthrough(stream_input, std::move(truncated_packet));
EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic;
auto valid_packet = make_input_packet(magic, declared_size, size);
input::passthrough(stream_input, std::move(valid_packet));
++valid_packet_count;
EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic;
auto oversized_declared_size = make_input_packet(magic, declared_size + 1, size + 1);
input::passthrough(stream_input, std::move(oversized_declared_size));
EXPECT_EQ(input::testing::queued_input_packet_count(stream_input), valid_packet_count) << "magic=" << magic;
}
}
TEST_F(InputGamepadSessionTest, ReusesGamepadsAcrossPauseAndDestroysThemOnTermination) {
const std::string session_id = "paired-client-certificate";