fix(native-usb): isolate two-pair transport and hand off read status in IRQ

This commit is contained in:
Joey Yakimowich-Payne 2026-09-17 20:24:32 -06:00
commit 9f8678af96
51 changed files with 8159 additions and 815 deletions

View file

@ -13,6 +13,8 @@
extern "C" {
void native_test_initialize(void);
void native_test_drain(void);
bool native_test_startup(void);
void native_test_advance(uint32_t);
bool native_test_setup(uint8_t, const tusb_control_request_t*, bool);
bool native_test_out(uint8_t, const uint8_t*, uint16_t, bool);
bool native_test_in(uint8_t, uint8_t*, uint16_t*, bool);
@ -20,6 +22,12 @@ void native_test_bus_reset(bool);
void native_test_hold_abort(bool);
bool native_test_select(uint8_t);
bool native_test_private_in(uint8_t, uint8_t, uint8_t*, uint16_t*);
bool native_test_private_out(uint8_t, uint8_t, const uint8_t*, uint16_t, bool);
extern uint32_t native_test_hid_completions[PROBE_CONTROLLER_COUNT];
extern uint32_t native_test_bulk_completions[PROBE_CONTROLLER_COUNT];
extern uint32_t native_test_received_count[PROBE_CONTROLLER_COUNT][2];
extern uint16_t native_test_received_length[PROBE_CONTROLLER_COUNT][2];
extern uint8_t native_test_received_data[PROBE_CONTROLLER_COUNT][2][64];
extern uint32_t native_test_interrupt_mask;
}
@ -29,8 +37,10 @@ std::array<uint8_t, PROFILE_STORAGE_ARENA_COUNT * PROFILE_STORAGE_ARENA_SIZE> fl
uint32_t programs = 0;
uint32_t erases = 0;
uint32_t bootsel_calls = 0;
std::array<uint8_t, 64> child_identity[2];
std::array<uint8_t, 64> child_identity[PROBE_CONTROLLER_COUNT];
bool interleave_identity_ack = false;
bool synthetic_root_management = false;
uint32_t bootsel_time_ms = 0;
void require(bool condition, const char* message) {
if (!condition) { std::cerr << message << '\n'; std::exit(1); }
@ -155,6 +165,240 @@ void read_child(uint8_t slot) {
"native child identity leaked root or sibling vendor bytes");
}
void assign_address(uint8_t slot, uint8_t address) {
tusb_control_request_t setup{};
setup.bRequest = TUSB_REQ_SET_ADDRESS;
setup.wValue = address;
require(native_test_setup(slot, &setup, true), "SET_ADDRESS stalled");
acknowledge(slot);
}
void configure(uint8_t slot, uint8_t value = 1) {
tusb_control_request_t setup{};
setup.bRequest = TUSB_REQ_SET_CONFIGURATION;
setup.wValue = value;
require(native_test_setup(slot, &setup, true), "SET_CONFIGURATION stalled");
acknowledge(slot);
}
tusb_control_request_t port_feature(uint8_t port, uint16_t feature, bool set) {
tusb_control_request_t setup{};
setup.bmRequestType = 0x23;
setup.bRequest = set ? TUSB_REQ_SET_FEATURE : TUSB_REQ_CLEAR_FEATURE;
setup.wIndex = port;
setup.wValue = feature;
return setup;
}
void change_port(uint8_t port, uint16_t feature, bool set) {
const auto setup = port_feature(port, feature, set);
require(native_test_setup(0, &setup, true), "port feature request stalled");
acknowledge();
}
std::vector<uint8_t> port_status(uint8_t port) {
tusb_control_request_t setup{};
setup.bmRequestType = 0xa3;
setup.bRequest = TUSB_REQ_GET_STATUS;
setup.wIndex = port;
setup.wLength = 4;
require(native_test_setup(0, &setup, true), "port status request stalled");
return receive();
}
void require_hub_change(uint8_t expected) {
uint8_t packet[64]; uint16_t length = 0;
require(native_test_private_in(0, 0x8f, packet, &length) && length == 1 && packet[0] == expected,
"root interrupt endpoint omitted or mixed port change bits");
native_test_drain();
require(!native_test_private_in(0, 0x8f, packet, &length),
"cleared hub port changes did not return to NAK");
}
void test_port_enumeration_and_bounds() {
require(native_test_startup(), "native hub startup failed");
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot)
require(!native_test_select(slot), "startup assigned an address to an unreset child");
assign_address(0, 9);
configure(0);
tusb_control_request_t descriptor{};
descriptor.bmRequestType = 0xa0;
descriptor.bRequest = TUSB_REQ_GET_DESCRIPTOR;
descriptor.wValue = 0x2900;
descriptor.wLength = 64;
require(native_test_setup(0, &descriptor, true), "hub descriptor stalled");
const auto bytes = receive();
require(bytes.size() == 9 && bytes[0] == 9 && bytes[1] == 0x29 &&
bytes[2] == PROBE_CONTROLLER_COUNT && bytes[7] == (1u << (PROBE_CONTROLLER_COUNT + 1u)) - 2u &&
bytes[8] == 0xff, "hub descriptor has incorrect port or non-removable masks");
for (uint8_t port = 1; port <= PROBE_CONTROLLER_COUNT; ++port) {
require(u16(port_status(port), 0) == 0, "unpowered port is not disconnected");
change_port(port, 8, true);
auto status = port_status(port);
require(u16(status, 0) == 0x101 && u16(status, 2) == 1, "port power did not signal connection");
change_port(port, 16, false);
require_hub_change(1u << port);
change_port(port, 4, true);
status = port_status(port);
require(u16(status, 0) == 0x111 && u16(status, 2) == 0, "port reset completed before its deadline");
native_test_advance(10000);
status = port_status(port);
require(u16(status, 0) == 0x103 && u16(status, 2) == 16, "port reset did not enable its child");
assign_address(port, 17u * port);
configure(port);
read_child(port);
change_port(port, 20, false);
require_hub_change(1u << port);
change_port(port, 2, true);
require(native_hub_suspended(port - 1) && !native_hub_hid_ready(port - 1),
"suspended port remained ready for input");
change_port(port, 2, false);
change_port(port, 18, false);
require_hub_change(1u << port);
}
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) read_child(slot);
for (uint8_t port : {uint8_t{0}, uint8_t{PROBE_CONTROLLER_COUNT + 1}}) {
auto setup = port_feature(port, 8, true);
require(!native_test_setup(0, &setup, true), "out-of-range port feature was accepted");
setup.bmRequestType = 0xa3; setup.bRequest = 0; setup.wValue = 0; setup.wLength = 4;
require(!native_test_setup(0, &setup, true), "out-of-range port status was accepted");
}
const uint8_t data = 1;
for (uint8_t instance : {uint8_t{PROBE_CONTROLLER_COUNT}, uint8_t{255}}) {
require(!native_hub_mounted(instance) && native_hub_suspended(instance) &&
!native_hub_hid_ready(instance) && !native_hub_hid_report(instance, 1, &data, 1) &&
native_hub_vendor_write_available(instance) == 0 &&
native_hub_vendor_write(instance, &data, 1) == 0 && native_hub_vendor_write_flush(instance) == 0,
"out-of-range controller instance touched a bank");
require(!native_hub_control_xfer(instance + (instance != 255), &descriptor, nullptr, 0, false) &&
!native_hub_control_status(instance + (instance != 255), &descriptor),
"out-of-range control slot was accepted");
}
configure(0, 0);
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot)
require(!native_hub_mounted(slot - 1) && !native_test_select(slot),
"root deconfiguration retained a child bank or address");
native_test_initialize();
}
void test_child_control_and_receive_isolation() {
native_test_initialize();
tusb_control_request_t identity{};
identity.bmRequestType = 0xc0; identity.bRequest = 3; identity.wLength = 128;
uint8_t packet[64]; uint16_t length;
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
configure(slot);
const uint8_t payload = 0x70 + slot;
require(native_hub_hid_report(slot - 1, 8, &payload, 1) &&
native_test_private_in(slot, 0x81, packet, &length), "HID completion setup failed");
require(native_test_setup(slot, &identity, false), "interleaved child control setup failed");
}
native_test_drain();
for (uint8_t slot = PROBE_CONTROLLER_COUNT; slot; --slot) {
const auto bytes = receive(slot);
require(bytes == std::vector<uint8_t>(child_identity[slot - 1].begin(), child_identity[slot - 1].end()),
"concurrent control transfers shared another child's EP0 data");
require(native_test_hid_completions[slot - 1] == 1 && native_hub_hid_ready(slot - 1),
"new SETUP invalidated an unrelated HID completion");
}
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
for (uint8_t endpoint : {1, 2}) {
const uint8_t payload[] = {slot, endpoint, uint8_t(slot ^ 0x5a)};
require(native_test_private_out(slot, endpoint, payload, sizeof(payload), false),
"private OUT packet was not accepted");
require(!native_test_private_out(slot, endpoint, payload, sizeof(payload), false),
"pending OUT buffer failed to NAK before foreground consumption");
}
}
native_test_drain();
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
for (uint8_t endpoint : {1, 2}) {
const uint8_t payload[] = {slot, endpoint, uint8_t(slot ^ 0x5a)};
require(native_test_received_count[slot - 1][endpoint - 1] == 1 &&
native_test_received_length[slot - 1][endpoint - 1] == sizeof(payload) &&
std::memcmp(native_test_received_data[slot - 1][endpoint - 1], payload, sizeof(payload)) == 0,
"OUT callback received another child's endpoint payload");
}
}
native_test_initialize();
}
void test_port_reset_revokes_only_its_child_events() {
for (uint8_t target = 1; target <= PROBE_CONTROLLER_COUNT; ++target) {
native_test_initialize();
assign_address(0, 9);
tusb_control_request_t identity{};
identity.bmRequestType = 0xc0; identity.bRequest = 3; identity.wLength = 128;
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
change_port(slot, 8, true);
configure(slot);
const uint8_t data = slot;
require(native_hub_hid_report(slot - 1, 8, &data, 1), "reset isolation HID setup failed");
require(native_test_setup(slot, &identity, true), "reset isolation control setup failed");
}
const auto reset = port_feature(target, 4, true);
require(native_test_setup(0, &reset, true), "port reset request failed");
acknowledge(0, false);
uint8_t packet[64]; uint16_t length;
require(native_test_in(target, packet, &length, false) && length == 64,
"could not queue the reset child's old control completion");
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
const uint8_t data = slot;
require(native_test_private_in(slot, 0x81, packet, &length) &&
native_test_private_out(slot, 2, &data, 1, false), "reset isolation completion setup failed");
}
native_test_drain();
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
const unsigned expected = slot == target ? 0 : 1;
require(native_test_hid_completions[slot - 1] == expected &&
native_test_received_count[slot - 1][1] == expected,
"port reset revoked a sibling event or dispatched a stale child event");
if (slot != target) {
const auto bytes = receive(slot);
require(bytes == std::vector<uint8_t>(child_identity[slot - 1].begin(), child_identity[slot - 1].end()),
"port reset corrupted a sibling control transfer");
}
}
const uint8_t other = target == PROBE_CONTROLLER_COUNT ? 1 : target + 1;
const auto concurrent_reset = port_feature(other, 4, true);
require(!native_test_setup(0, &concurrent_reset, true),
"simultaneous port resets created competing address-zero owners");
native_test_advance(10000);
require(!native_test_in(target, packet, &length, true) && !native_hub_mounted(target - 1),
"port reset retained a stale control packet or configuration");
assign_address(target, 17u * target);
configure(target);
read_child(target);
}
native_test_initialize();
}
void require_interleaved_profile(const std::vector<uint8_t>& expected) {
const auto setup = request(Operation::kProfileRead, true, kMaximumResponseSize);
require(native_test_setup(0, &setup, true), "interleaved profile read setup failed");
std::vector<uint8_t> bytes;
const size_t total = kResponseHeaderSize + expected.size();
for (unsigned index = 0; bytes.size() < total; ++index) {
// Every root IN follows another owner's tokens, including the first.
read_child(1u + index % PROBE_CONTROLLER_COUNT);
uint8_t packet[64]; uint16_t length = 0;
require(native_test_in(0, packet, &length, false),
"prepared profile packet required foreground work after selection");
require(length == std::min<size_t>(64, total - bytes.size()),
"address alternation changed the profile packet boundary");
bytes.insert(bytes.end(), packet, packet + length);
native_test_drain(); // Only the completed packet may prepare its successor.
}
read_child(PROBE_CONTROLLER_COUNT);
require(native_test_out(0, nullptr, 0, true), "interleaved profile status OUT failed");
require(std::memcmp(bytes.data(), "SPMG", 4) == 0 &&
bytes[5] == static_cast<uint8_t>(Operation::kProfileRead) &&
bytes[6] == static_cast<uint8_t>(Status::kOk) && u16(bytes, 8) == expected.size() &&
u32(bytes, 16) == configuration_crc32(expected.data(), expected.size()) &&
std::vector<uint8_t>(bytes.begin() + kResponseHeaderSize, bytes.end()) == expected,
"alternating root and child reads mixed profile or identity bytes");
}
void test_profile_transport() {
const uint32_t programs_before = programs, erases_before = erases;
const auto original = encoded_profile(0);
@ -168,9 +412,9 @@ void test_profile_transport() {
auto playtest = read_operation(Operation::kProfilePlaytest);
require(playtest[kResponseHeaderSize] == 0 && playtest[kResponseHeaderSize + 1] == 0xff,
"disconnected playtest fabricated controller input");
require_profile(original);
require_interleaved_profile(original);
require(programs == programs_before && erases == erases_before, "editor reads wrote saved storage");
for (uint8_t slot : {1, 2}) {
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
const auto management = request(Operation::kProfileList, true, kMaximumResponseSize);
require(!native_test_setup(slot, &management, true), "native child accepted regular management");
read_child(slot);
@ -184,7 +428,7 @@ void test_profile_transport() {
const auto chunk = envelope(Operation::kProfileChunk, chunk_payload(1, edited, 0));
const auto setup = request(Operation::kProfileChunk, false, chunk.size());
require(native_test_setup(0, &setup, true) && native_test_out(0, chunk.data(), 64, true), "first full OUT packet failed");
read_child(1); read_child(2);
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) read_child(slot);
const auto child_management = request(Operation::kInfo, true, kMaximumResponseSize);
require(!native_test_setup(1, &child_management, true), "child INFO was accepted during a root write");
require(native_test_out(0, chunk.data() + 64, chunk.size() - 64, true), "interleaved child requests corrupted root OUT tail");
@ -296,13 +540,16 @@ void test_private_transmit_survives_round_robin_tokens() {
tusb_control_request_t configuration{};
configuration.bRequest = TUSB_REQ_SET_CONFIGURATION;
configuration.wValue = 1;
for (uint8_t slot : {1, 2}) {
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
require(native_test_setup(slot, &configuration, true), "child configuration failed");
acknowledge(slot);
}
const uint8_t payloads[2][3] = {{0x11, 0x22, 0x33}, {0x44, 0x55, 0x66}};
for (uint8_t instance : {0, 1}) {
require(native_hub_hid_report(instance, instance ? 7 : 8, payloads[instance], 3),
uint8_t payloads[PROBE_CONTROLLER_COUNT][3];
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
payloads[instance][0] = 0x11u + instance;
payloads[instance][1] = 0x42u + instance;
payloads[instance][2] = 0x83u + instance;
require(native_hub_hid_report(instance, 8u - instance, payloads[instance], 3),
"could not queue HID packet");
require(native_hub_vendor_write(instance, payloads[instance], 3) == 3 &&
native_hub_vendor_write_flush(instance) == 3, "could not queue bulk packet");
@ -310,26 +557,30 @@ void test_private_transmit_survives_round_robin_tokens() {
uint8_t packet[64];
uint16_t length = 0;
for (uint8_t endpoint : {0x81, 0x82}) {
for (uint8_t slot : {1, 2}) {
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
require(native_test_private_in(slot, endpoint, packet, &length),
"queued private IN packet required foreground work after bank selection");
const unsigned prefix = endpoint == 0x81 ? 1 : 0;
require(length == 3 + prefix &&
(!prefix || packet[0] == (slot == 1 ? 8 : 7)) &&
(!prefix || packet[0] == 9u - slot) &&
std::memcmp(packet + prefix, payloads[slot - 1], 3) == 0,
"round-robin IN token received another endpoint's payload");
require(!native_test_private_in(slot, endpoint, packet, &length),
"unarmed endpoint reused another child's IN packet instead of NAK");
}
}
native_test_drain();
require(native_hub_hid_ready(0) && native_hub_hid_ready(1),
"acknowledged HID packets did not release their queues");
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
require(native_hub_hid_ready(instance) && native_test_hid_completions[instance] == 1 &&
native_test_bulk_completions[instance] == 1,
"acknowledged packets did not release exactly one completion per endpoint");
require(!native_test_private_in(1, 0x81, packet, &length),
"acknowledged HID packet was retransmitted");
// The idle poll selected R without restoring its shared EP0 image.
// An idle EP0 bank must not block newly queued private endpoint traffic.
require(native_hub_hid_report(0, 8, payloads[0], 3), "could not queue the next HID packet");
require(native_test_private_in(1, 0x81, packet, &length) && length == 4 &&
std::memcmp(packet + 1, payloads[0], 3) == 0,
"pending shared EP0 restoration blocked a newly queued private IN packet");
"idle shared EP0 blocked a newly queued private IN packet");
native_test_drain();
native_test_initialize();
}
@ -338,62 +589,196 @@ void test_masked_irq_completion_handoff() {
tusb_control_request_t configuration{};
configuration.bRequest = TUSB_REQ_SET_CONFIGURATION;
configuration.wValue = 1;
for (uint8_t slot : {1, 2}) {
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
require(native_test_setup(slot, &configuration, true), "child configuration failed");
acknowledge(slot);
}
const uint8_t payloads[2][3] = {{0x12, 0x34, 0x56}, {0x78, 0x9a, 0xbc}};
for (uint8_t instance : {0, 1})
uint8_t payloads[PROBE_CONTROLLER_COUNT][3];
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance) {
payloads[instance][0] = 0x12u + instance;
payloads[instance][1] = 0x34u + instance;
payloads[instance][2] = 0x56u + instance;
require(native_hub_hid_report(instance, 8, payloads[instance], 3),
"could not queue masked-window HID packet");
}
uint8_t packet[64];
uint16_t length = 0;
native_test_interrupt_mask = 1;
require(native_test_private_in(1, 0x81, packet, &length),
"first controller did not complete during masked window");
require(!native_test_select(2),
"pending completion must prevent overwriting the active bank");
native_hub_service_pending_usb();
require(native_test_interrupt_mask == 1,
"SRAM service must preserve the caller's interrupt mask");
require(native_test_private_in(2, 0x81, packet, &length) && length == 4 &&
packet[0] == 8 && std::memcmp(packet + 1, payloads[1], 3) == 0,
"SRAM service did not permit the other controller's real packet");
native_hub_service_pending_usb();
require(!native_hub_hid_ready(0) && !native_hub_hid_ready(1),
"SRAM service must defer protocol callbacks to foreground dispatch");
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
require(native_test_private_in(slot, 0x81, packet, &length) && length == 4 &&
packet[0] == 8 && std::memcmp(packet + 1, payloads[slot - 1], 3) == 0,
"controller did not retain its packet during the masked window");
const uint8_t next = slot == PROBE_CONTROLLER_COUNT ? 1 : slot + 1;
require(!native_test_select(next),
"pending completion must prevent overwriting the active bank");
native_hub_service_pending_usb();
require(native_test_interrupt_mask == 1,
"SRAM service must preserve the caller's interrupt mask");
require(!native_hub_hid_ready(slot - 1) && native_test_hid_completions[slot - 1] == 0,
"SRAM service must defer protocol callbacks to foreground dispatch");
}
native_test_interrupt_mask = 0;
native_test_drain();
require(native_hub_hid_ready(0) && native_hub_hid_ready(1),
"deferred completions did not release both controller queues");
for (uint8_t instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
require(native_hub_hid_ready(instance) && native_test_hid_completions[instance] == 1,
"deferred completions did not release every controller queue exactly once");
require(!native_test_private_in(1, 0x81, packet, &length),
"later IRQ dispatch duplicated a serviced completion");
native_test_initialize();
}
void test_private_bootsel() {
void require_no_bootsel() {
bootsel_time_ms += 100;
probe_bootsel_task(bootsel_time_ms);
probe_bootsel_task(bootsel_time_ms + 50);
require(bootsel_calls == 0, "unauthorized or unacknowledged BOOTSEL rebooted the device");
}
void test_neutral_management_surface() {
require(!synthetic_root_management, "neutral surface must use the production BOOTSEL-only callback");
const uint32_t programs_before = programs, erases_before = erases;
struct WriteRequest { Operation operation; uint16_t payload_size; };
const WriteRequest writes[] = {
{Operation::kModeSet, 5}, {Operation::kReboot, 4},
{Operation::kConfigurationBegin, 12}, {Operation::kConfigurationChunk, 9},
{Operation::kConfigurationCommit, 4}, {Operation::kConfigurationReset, 4},
{Operation::kProfileSelect, 15}, {Operation::kProfileBegin, 28},
{Operation::kProfileChunk, 9}, {Operation::kProfileCommit, 4},
{Operation::kProfileReset, 19}, {Operation::kProfileActivate, 19},
{Operation::kProfileMetadataSet, 20}, {Operation::kProfileIdentify, 14},
{Operation::kWiiOrientation, 19}, {Operation::kPairingRefresh, 0},
{Operation::kPairingClear, 0},
};
for (uint8_t slot = 0; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
for (Operation op : {Operation::kInfo, Operation::kConfigurationRead,
Operation::kTransactionStatus, Operation::kPairingRead,
Operation::kRuntimeDiagnostics, Operation::kProfileList,
Operation::kProfileRead, Operation::kProfilePlaytest,
Operation::kProfileTransactionStatus, Operation::kProfileMetadataRead}) {
const auto setup = request(op, true, kMaximumResponseSize);
require(!native_test_setup(slot, &setup, true), "neutral device exposed full management reads");
}
for (const auto& item : writes) {
const auto setup = request(item.operation, false, kRequestHeaderSize + item.payload_size);
require(!native_test_setup(slot, &setup, true), "neutral device exposed a management mutation");
}
if (slot) read_child(slot);
}
profile_service_task_on_storage_core(5000);
require(programs == programs_before && erases == erases_before,
"neutral management rejection changed saved profiles");
require_no_bootsel();
}
void test_private_bootsel(uint8_t reboot_slot) {
require(!synthetic_root_management, "BOOTSEL must use the production transport callback");
const uint32_t programs_before = programs, erases_before = erases;
const auto bytes = envelope(Operation::kBootselReboot, {});
const auto setup = request(Operation::kBootselReboot, false, bytes.size());
for (uint8_t slot : {0, 1, 2}) {
require(native_test_setup(slot, &setup, true), "private BOOTSEL setup stalled");
require(!native_test_out(slot, bytes.data(), bytes.size() - 1, true), "short BOOTSEL was accepted");
probe_bootsel_task(100); probe_bootsel_task(200);
require(bootsel_calls == 0, "short BOOTSEL rebooted the device");
require(native_test_setup(slot, &setup, true) && native_test_out(slot, bytes.data(), bytes.size(), true),
"valid private BOOTSEL envelope failed");
// An unrelated identity/INFO SETUP cancels an unacknowledged BOOTSEL.
if (slot) read_child(slot); else read_operation(Operation::kInfo);
probe_bootsel_task(300); probe_bootsel_task(400);
require(bootsel_calls == 0, "unacknowledged BOOTSEL rebooted the device");
tusb_control_request_t replacement{};
replacement.bmRequestType = 0x80;
replacement.bRequest = TUSB_REQ_GET_STATUS;
replacement.wLength = 2;
uint8_t packet[64]; uint16_t length;
for (uint8_t slot = 0; slot <= PROBE_CONTROLLER_COUNT; ++slot) {
for (uint16_t size : {uint16_t{0}, uint16_t{kRequestHeaderSize - 1}}) {
require(native_test_setup(slot, &setup, true), "private BOOTSEL setup stalled");
require(!native_test_in(slot, packet, &length, true), "BOOTSEL armed status before receiving its envelope");
require(!native_test_out(slot, bytes.data(), size, true), "short BOOTSEL was accepted");
require(!native_test_in(slot, packet, &length, true), "short BOOTSEL armed a status ACK");
require_no_bootsel();
}
// Every reserved field and CRC byte must be checked by the shared decoder.
for (size_t offset : {0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}) {
auto malformed = bytes;
malformed[offset] ^= 1;
require(native_test_setup(slot, &setup, true) &&
native_test_out(slot, bytes.data(), bytes.size(), true), "superseded BOOTSEL setup failed");
require(native_test_setup(slot, &setup, true), "malformed BOOTSEL setup stalled");
require(!native_test_out(slot, malformed.data(), malformed.size(), true), "malformed BOOTSEL was accepted");
require(!native_test_in(slot, packet, &length, true), "malformed BOOTSEL reused an earlier authorization");
require_no_bootsel();
}
std::array<tusb_control_request_t, 8> wrong_setup;
wrong_setup.fill(setup);
wrong_setup[0].bmRequestType = 0x41; // Interface recipient.
wrong_setup[1].bmRequestType = 0x42; // Endpoint recipient.
wrong_setup[2].bmRequestType = 0xc0; // Wrong direction.
wrong_setup[3].wValue ^= 1;
wrong_setup[4].wIndex ^= 1;
wrong_setup[5].wLength = 0;
wrong_setup[6].wLength = kRequestHeaderSize - 1;
wrong_setup[7].wLength = kRequestHeaderSize + 1;
for (const auto& invalid : wrong_setup) {
require(native_test_setup(slot, &setup, true) &&
native_test_out(slot, bytes.data(), bytes.size(), true), "interrupted BOOTSEL setup failed");
require(!native_test_setup(slot, &invalid, true), "wrong BOOTSEL setup was accepted");
require(!native_test_in(slot, packet, &length, true) &&
!native_test_out(slot, bytes.data(), bytes.size(), true), "rejected SETUP retained an old BOOTSEL transfer");
require_no_bootsel();
}
// Standard requests do not call the vendor handler: transport ownership
// must still revoke both incomplete DATA and unacknowledged status.
for (bool send_data : {false, true}) {
require(native_test_setup(slot, &setup, true), "interruptible BOOTSEL setup stalled");
if (send_data)
require(native_test_out(slot, bytes.data(), bytes.size(), true), "interruptible BOOTSEL DATA failed");
require(native_test_setup(slot, &replacement, true), "replacement standard request stalled");
require(receive(slot).size() == 2, "replacement standard transfer did not complete");
require(!native_test_in(slot, packet, &length, true) &&
!native_test_out(slot, bytes.data(), bytes.size(), true), "superseded BOOTSEL retained a transfer");
require_no_bootsel();
}
// Reset revokes queued DATA, validated DATA, and even a captured status
// ACK that has not reached the foreground callback yet.
for (unsigned phase : {0, 1, 2}) {
require(native_test_setup(slot, &setup, true) &&
native_test_out(slot, bytes.data(), bytes.size(), phase != 0), "resettable BOOTSEL setup failed");
if (phase == 2) acknowledge(slot, false);
native_test_bus_reset(true);
require(!native_test_in(slot, packet, &length, true), "bus reset retained BOOTSEL status");
require_no_bootsel();
}
}
require(native_test_setup(2, &setup, true) && native_test_out(2, bytes.data(), bytes.size(), true),
"validated child BOOTSEL failed");
acknowledge(2);
probe_bootsel_task(500); probe_bootsel_task(549);
require(bootsel_calls == 0, "BOOTSEL did not retain the post-ACK delay");
probe_bootsel_task(550);
require(bootsel_calls == 1, "validated child BOOTSEL did not reach ROM after the delay");
// Concurrent children must not share the valid envelope or authorization.
for (uint8_t slot : {uint8_t{1}, uint8_t{PROBE_CONTROLLER_COUNT}})
require(native_test_setup(slot, &setup, true), "concurrent BOOTSEL setup failed");
require(native_test_out(1, bytes.data(), bytes.size(), true), "first child's BOOTSEL DATA failed");
auto corrupt = bytes;
corrupt[12] ^= 1;
require(!native_test_out(PROBE_CONTROLLER_COUNT, corrupt.data(), corrupt.size(), true),
"last child inherited its sibling's BOOTSEL authorization");
native_test_bus_reset(true);
require_no_bootsel();
if (reboot_slot == 0) {
require(native_test_startup(), "root-only BOOTSEL startup failed");
for (uint8_t slot = 1; slot <= PROBE_CONTROLLER_COUNT; ++slot)
require(!native_test_select(slot), "root-only recovery unexpectedly requires an enumerated child");
} else {
native_test_initialize();
}
require(native_test_setup(reboot_slot, &setup, true), "valid BOOTSEL setup failed");
require_no_bootsel();
require(native_test_out(reboot_slot, bytes.data(), bytes.size(), true), "valid BOOTSEL DATA failed");
require_no_bootsel();
acknowledge(reboot_slot, false);
require_no_bootsel();
// Unlike reset, the next SETUP preserves a genuine, already-captured ACK.
require(native_test_setup(reboot_slot, &replacement, false), "post-ACK SETUP failed");
native_test_drain();
require(receive(reboot_slot).size() == 2, "post-ACK standard transfer failed");
const uint32_t now = bootsel_time_ms + 100;
probe_bootsel_task(now); probe_bootsel_task(now + 49);
require(bootsel_calls == 0, "BOOTSEL did not retain the post-ACK 50ms delay");
probe_bootsel_task(now + 50);
require(bootsel_calls == 1, "validated BOOTSEL did not reach ROM after the delay");
probe_bootsel_task(now + 100);
require(bootsel_calls == 1, "BOOTSEL dispatched more than once");
profile_service_task_on_storage_core(now + 100);
require(programs == programs_before && erases == erases_before,
"private BOOTSEL changed saved profiles");
}
bool flash_read(void*, uint8_t arena, size_t offset, uint8_t* data, size_t size) {
@ -445,7 +830,11 @@ bool bluepad32_input_backend_capture_page(uint32_t, uint16_t, Bluepad32CaptureSn
extern "C" void reset_usb_boot(uint32_t, uint32_t) { ++bootsel_calls; }
extern "C" bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tusb_control_request_t* setup) {
if (probe_management_vendor_control(slot, stage, setup)) return true;
if (slot < 1 || slot > 2 || setup->bmRequestType != 0xc0 ||
// Exercise the full root service over a synthetic four-child transport
// without claiming that the neutral firmware exposes that service.
if (synthetic_root_management && slot == 0 &&
usb_configuration_management_vendor_control(slot, stage, setup)) return true;
if (slot < 1 || slot > PROBE_CONTROLLER_COUNT || setup->bmRequestType != 0xc0 ||
setup->bRequest != 3 || setup->wValue || setup->wIndex) return false;
if (stage == CONTROL_STAGE_ACK && slot == 1 && interleave_identity_ack) {
interleave_identity_ack = false;
@ -456,20 +845,31 @@ extern "C" bool tud_vendor_control_xfer_cb(uint8_t slot, uint8_t stage, const tu
require(native_test_select(0), "read ACK callback blocked servicing the next USB SETUP");
}
return stage != CONTROL_STAGE_SETUP || native_hub_control_xfer(slot, setup,
child_identity[slot - 1].data(), child_identity[slot - 1].size());
child_identity[slot - 1].data(), child_identity[slot - 1].size(), true);
}
int main() {
int main(int argc, char** argv) {
static_assert(sizeof(tusb_control_request_t) == 8);
flash.fill(0xff); child_identity[0].fill(0x31); child_identity[1].fill(0x72);
require(argc == 2 && (std::strcmp(argv[1], "root") == 0 || std::strcmp(argv[1], "child") == 0),
"select the root or last-child BOOTSEL completion scenario");
const uint8_t reboot_slot = std::strcmp(argv[1], "root") == 0 ? 0 : PROBE_CONTROLLER_COUNT;
flash.fill(0xff);
for (unsigned instance = 0; instance < PROBE_CONTROLLER_COUNT; ++instance)
child_identity[instance].fill(0x31u + instance * 0x21u);
profile_service_prepare(); profile_service_initialize_on_storage_core();
native_test_initialize();
synthetic_root_management = SWITCH2_PROBE_NEUTRAL_INPUT;
test_profile_transport();
test_interrupted_transactions();
test_pending_control_buffer_ownership();
synthetic_root_management = false;
test_read_ack_allows_usb_progress();
test_private_transmit_survives_round_robin_tokens();
test_masked_irq_completion_handoff();
test_private_bootsel();
std::cout << "native root management packet and persistence regressions passed\n";
test_port_enumeration_and_bounds();
test_child_control_and_receive_isolation();
test_port_reset_revokes_only_its_child_events();
if (SWITCH2_PROBE_NEUTRAL_INPUT) test_neutral_management_surface();
test_private_bootsel(reboot_slot);
std::cout << "native transport, synthetic root management and private BOOTSEL regressions passed\n";
}