Harden legacy bounds

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Joey Yakimowich-Payne 2026-08-11 12:51:36 +09:00
commit e2a7635f2f
3 changed files with 228 additions and 49 deletions

71
switch_pro_bounds.h Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include <cstddef>
#include <cstdint>
enum class SwitchProOutputReportKind : uint8_t {
Ignore,
Noop,
Rumble,
Feature,
Configuration,
};
inline SwitchProOutputReportKind switch_pro_classify_output_report(
const uint8_t* report,
std::size_t length) {
if (report == nullptr || length < 2 || length > 64) {
return SwitchProOutputReportKind::Ignore;
}
switch (report[0]) {
case 0x00:
return SwitchProOutputReportKind::Noop;
case 0x01:
return length >= 16
? SwitchProOutputReportKind::Feature
: SwitchProOutputReportKind::Ignore;
case 0x10:
case 0x21:
return length >= 10
? SwitchProOutputReportKind::Rumble
: SwitchProOutputReportKind::Ignore;
case 0x80:
return SwitchProOutputReportKind::Configuration;
default:
return SwitchProOutputReportKind::Ignore;
}
}
inline bool switch_pro_spi_read_size_fits(std::size_t size) {
return size <= 64 - 20;
}
inline std::size_t switch_pro_fill_flash_read(
uint8_t* destination,
std::size_t destination_capacity,
const uint8_t* source,
std::size_t source_size,
std::size_t source_offset,
std::size_t requested) {
if (destination == nullptr) {
return 0;
}
const std::size_t produced = requested < destination_capacity
? requested
: destination_capacity;
for (std::size_t index = 0; index < produced; ++index) {
destination[index] = 0xFF;
}
if (source != nullptr && source_offset < source_size) {
const std::size_t available = source_size - source_offset;
const std::size_t copied = available < produced ? available : produced;
for (std::size_t index = 0; index < copied; ++index) {
destination[index] = source[source_offset + index];
}
}
return produced;
}

View file

@ -1,4 +1,5 @@
#include "switch_pro_driver.h"
#include "switch_pro_bounds.h"
#include <algorithm>
#include <cstring>
@ -178,9 +179,14 @@ static const uint8_t user_calibration_data[0x3F] = {
static const SwitchFactoryConfig* factory_config = reinterpret_cast<const SwitchFactoryConfig*>(factory_config_data);
static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast<const SwitchUserCalibration*>(user_calibration_data);
static std::map<uint32_t, const uint8_t*> spi_flash_data = {
{0x6000, factory_config_data},
{0x8000, user_calibration_data}
struct SpiFlashRegion {
const uint8_t* data;
std::size_t size;
};
static const std::map<uint32_t, SpiFlashRegion> spi_flash_data = {
{0x6000, {factory_config_data, sizeof(factory_config_data)}},
{0x8000, {user_calibration_data, sizeof(user_calibration_data)}}
};
static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; }
@ -235,16 +241,32 @@ static bool send_report(uint8_t reportID, const void* reportData, uint16_t repor
return result;
}
static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) {
static void read_spi_flash(
uint8_t* destination,
std::size_t destination_capacity,
uint32_t address,
uint8_t size) {
uint32_t addressBank = address & 0xFFFFFF00;
uint32_t addressOffset = address & 0x000000FF;
auto it = spi_flash_data.find(addressBank);
if (it != spi_flash_data.end()) {
const uint8_t* data = it->second;
memcpy(dest, data + addressOffset, size);
const SpiFlashRegion& region = it->second;
switch_pro_fill_flash_read(
destination,
destination_capacity,
region.data,
region.size,
addressOffset,
size);
} else {
memset(dest, 0xFF, size);
switch_pro_fill_flash_read(
destination,
destination_capacity,
nullptr,
0,
0,
size);
}
}
@ -371,7 +393,11 @@ static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSu
report_buffer[17] = reportData[13];
report_buffer[18] = reportData[14];
report_buffer[19] = reportData[15];
read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize);
read_spi_flash(
&report_buffer[20],
sizeof(report_buffer) - 20,
spiReadAddress,
spiReadSize);
canSend = true;
LOG_PRINTF("[HID] FEATURE SPI_READ addr=0x%08lx size=%u\n", (unsigned long)spiReadAddress, spiReadSize);
break;
@ -642,6 +668,52 @@ bool switch_pro_is_ready() {
return is_ready;
}
static void dispatch_output_report(
uint8_t instance,
uint8_t report_id,
const uint8_t* buffer,
uint16_t length) {
if (instance != 0) {
return;
}
const SwitchProOutputReportKind kind =
switch_pro_classify_output_report(buffer, length);
if (kind == SwitchProOutputReportKind::Ignore) {
return;
}
if (kind == SwitchProOutputReportKind::Feature &&
buffer[10] == SPI_READ &&
!switch_pro_spi_read_size_fits(buffer[15])) {
return;
}
memset(report_buffer, 0x00, sizeof(report_buffer));
const uint8_t switchReportID = buffer[0];
const uint8_t switchReportSubID = buffer[1];
LOG_PRINTF("[HID] output_report id=%u switchRID=0x%02x sub=0x%02x len=%u\n",
report_id, switchReportID, switchReportSubID, length);
switch (kind) {
case SwitchProOutputReportKind::Noop:
return;
case SwitchProOutputReportKind::Rumble:
forward_rumble_to_host(buffer, length);
return;
case SwitchProOutputReportKind::Feature:
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, length);
return;
case SwitchProOutputReportKind::Configuration:
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, length);
return;
case SwitchProOutputReportKind::Ignore:
return;
}
}
// HID callbacks
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
(void)instance;
@ -656,51 +728,12 @@ uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_t
}
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {
(void)instance;
if (report_type != HID_REPORT_TYPE_OUTPUT) return;
memset(report_buffer, 0x00, bufsize);
uint8_t switchReportID = buffer[0];
uint8_t switchReportSubID = buffer[1];
LOG_PRINTF("[HID] set_report type=%d id=%u switchRID=0x%02x sub=0x%02x len=%u\n",
report_type, report_id, switchReportID, switchReportSubID, bufsize);
if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) {
forward_rumble_to_host(buffer, bufsize);
}
if (switchReportID == REPORT_OUTPUT_00) {
// No-op, just acknowledge to clear any stalls.
return;
} else if (switchReportID == REPORT_FEATURE) {
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize);
} else if (switchReportID == REPORT_CONFIGURATION) {
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, bufsize);
} else {
}
dispatch_output_report(instance, report_id, buffer, bufsize);
}
void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id, uint8_t const* buffer, uint16_t bufsize) {
(void)instance;
// Host sent data on interrupt OUT; mirror the control path handling.
memset(report_buffer, 0x00, bufsize);
uint8_t switchReportID = buffer[0];
uint8_t switchReportSubID = buffer[1];
LOG_PRINTF("[HID] report_received id=%u switchRID=0x%02x sub=0x%02x len=%u\n",
report_id, switchReportID, switchReportSubID, bufsize);
if (switchReportID == REPORT_OUTPUT_10 || switchReportID == REPORT_OUTPUT_21) {
forward_rumble_to_host(buffer, bufsize);
}
if (switchReportID == REPORT_OUTPUT_00) {
return;
} else if (switchReportID == REPORT_FEATURE) {
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize);
} else if (switchReportID == REPORT_CONFIGURATION) {
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, bufsize);
}
dispatch_output_report(instance, report_id, buffer, bufsize);
}
uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) {

View file

@ -5,6 +5,7 @@
#include <cstring>
#include "../../switch_pro_descriptors.h"
#include "../../switch_pro_bounds.h"
namespace {
@ -88,6 +89,73 @@ bool legacy_string_descriptors_match_exact_bytes() {
return true;
}
bool legacy_output_classifier_rejects_invalid_report_framing() {
const uint8_t report[] = {0x01, 0x00};
CHECK(switch_pro_classify_output_report(nullptr, 2) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(report, 0) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(report, 1) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(report, 65) == SwitchProOutputReportKind::Ignore);
return true;
}
bool legacy_feature_reports_reject_short_payloads_and_accept_bounds() {
const uint8_t report[] = {0x01, 0x00};
CHECK(switch_pro_classify_output_report(report, 15) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(report, 16) == SwitchProOutputReportKind::Feature);
CHECK(switch_pro_classify_output_report(report, 64) == SwitchProOutputReportKind::Feature);
return true;
}
bool legacy_configuration_and_rumble_reports_reject_short_payloads() {
const uint8_t configuration[] = {0x80, 0x00};
const uint8_t rumble[] = {0x10, 0x00};
const uint8_t noop[] = {0x00, 0x00};
CHECK(switch_pro_classify_output_report(configuration, 1) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(configuration, 2) == SwitchProOutputReportKind::Configuration);
CHECK(switch_pro_classify_output_report(rumble, 9) == SwitchProOutputReportKind::Ignore);
CHECK(switch_pro_classify_output_report(rumble, 10) == SwitchProOutputReportKind::Rumble);
CHECK(switch_pro_classify_output_report(noop, 2) == SwitchProOutputReportKind::Noop);
return true;
}
bool legacy_spi_read_rejects_payload_overflow_at_forty_five_bytes() {
CHECK(switch_pro_spi_read_size_fits(0));
CHECK(switch_pro_spi_read_size_fits(44));
CHECK(!switch_pro_spi_read_size_fits(45));
CHECK(!switch_pro_spi_read_size_fits(255));
return true;
}
bool legacy_flash_read_copies_in_range_data_through_exact_end() {
const uint8_t source[] = {1, 2, 3, 4};
uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA};
CHECK(switch_pro_fill_flash_read(destination, 4, source, 4, 0, 4) == 4);
CHECK(destination[0] == 1 && destination[3] == 4);
return true;
}
bool legacy_flash_read_prefills_partial_source_end_with_ff() {
const uint8_t source[] = {1, 2};
uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA};
CHECK(switch_pro_fill_flash_read(destination, 4, source, 2, 0, 4) == 4);
CHECK(destination[0] == 1 && destination[1] == 2);
CHECK(destination[2] == 0xFF && destination[3] == 0xFF);
return true;
}
bool legacy_flash_read_leaves_canaries_on_invalid_range_or_null_source() {
const uint8_t source[] = {1, 2};
uint8_t destination[] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA};
CHECK(switch_pro_fill_flash_read(destination + 1, 4, source, 2, 3, 4) == 4);
CHECK(destination[0] == 0xAA && destination[5] == 0xAA);
CHECK(destination[1] == 0xFF && destination[4] == 0xFF);
CHECK(switch_pro_fill_flash_read(destination + 1, 4, nullptr, 2, 0, 4) == 4);
CHECK(destination[0] == 0xAA && destination[5] == 0xAA);
CHECK(destination[1] == 0xFF && destination[4] == 0xFF);
CHECK(switch_pro_fill_flash_read(nullptr, 4, source, 2, 0, 4) == 0);
return true;
}
} // namespace
void run_legacy_descriptor_tests(TestRunner& runner) {
@ -95,4 +163,11 @@ void run_legacy_descriptor_tests(TestRunner& runner) {
runner.run("legacy configuration descriptor exact bytes", legacy_configuration_descriptor_matches_exact_bytes);
runner.run("legacy HID report descriptor exact bytes", legacy_hid_report_descriptor_matches_exact_bytes);
runner.run("legacy string descriptors exact bytes", legacy_string_descriptors_match_exact_bytes);
runner.run("legacy output classifier rejects invalid report framing", legacy_output_classifier_rejects_invalid_report_framing);
runner.run("legacy feature reports reject short payloads and accept bounds", legacy_feature_reports_reject_short_payloads_and_accept_bounds);
runner.run("legacy configuration and rumble reports reject short payloads", legacy_configuration_and_rumble_reports_reject_short_payloads);
runner.run("legacy SPI read rejects payload overflow at 45 bytes", legacy_spi_read_rejects_payload_overflow_at_forty_five_bytes);
runner.run("legacy flash read copies in-range data through exact end", legacy_flash_read_copies_in_range_data_through_exact_end);
runner.run("legacy flash read prefills partial source end with FF", legacy_flash_read_prefills_partial_source_end_with_ff);
runner.run("legacy flash read leaves canaries on invalid range or null source", legacy_flash_read_leaves_canaries_on_invalid_range_or_null_source);
}