usbh: add enumeration control transfer timeout

A device that ACKs SETUP but never completes the following stage (e.g. NAKs
the EP0 IN data stage forever) froze the control state machine: enumeration
never finishes and hub interrupt polling never resumes, so one dead device
blocks all further enumeration on the bus. Observed with a degraded USB disk
behind a hub on a rp2040 PIO-USB host: wire capture shows SETUP ACKed then
an endless IN/NAK storm with no completion event.

Arm a watchdog (CFG_TUH_ENUM_TIMEOUT_MS, default 5s to match Linux's
USB_CTRL_GET_TIMEOUT, 0 disables) on enumeration-owned control transfers
using the existing call_after slot, which already wakes a blocked host task.
On expiry abort the transfer at hcd level and complete it as FAILED so the
normal enumeration failure path frees dev0 and resumes hub polling.
Non-enumeration control transfers keep their current behavior.
diff --git a/src/host/usbh.c b/src/host/usbh.c
index 44819b0..dd85b17 100644
--- a/src/host/usbh.c
+++ b/src/host/usbh.c
@@ -336,6 +336,10 @@
 //--------------------------------------------------------------------+
 static void enum_new_device(hcd_event_t* event);
 static void enum_delay_async(uintptr_t state);
+static void process_enumeration(tuh_xfer_t *xfer);
+#if CFG_TUH_ENUM_TIMEOUT_MS
+static void enum_xfer_timeout_async(uintptr_t user_data);
+#endif
 static void process_remove_event(hcd_event_t *event);
 static void remove_device_tree(uint8_t rhport, uint8_t hub_addr, uint8_t hub_port);
 
@@ -870,6 +874,20 @@
   s->result     = xfer->result;
 }
 
+// Arm a watchdog on the in-flight control transfer if it belongs to enumeration: submitted by the
+// enum state machine (incl. those to the parent hub) or addressed to the device being enumerated
+// (class driver config). Without it a broken device that never completes a stage wedges enumeration
+// and hub polling forever. Non-enumeration transfers are not armed: application owns their pacing.
+TU_ATTR_ALWAYS_INLINE static inline void enum_xfer_timeout_arm(void) {
+#if CFG_TUH_ENUM_TIMEOUT_MS
+  const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
+  if ((ctrl_info->complete_cb == process_enumeration || ctrl_info->daddr == _usbh_data.enumerating_daddr) &&
+      _usbh_data.call_after.func == NULL) {
+    usbh_defer_func_ms_async(CFG_TUH_ENUM_TIMEOUT_MS, enum_xfer_timeout_async, ctrl_info->daddr);
+  }
+#endif
+}
+
 // TODO timeout_ms is not supported yet
 bool tuh_control_xfer (tuh_xfer_t* xfer) {
   const uint8_t daddr = xfer->daddr;
@@ -960,6 +978,7 @@
     control_xfer_set_stage(CONTROL_STAGE_IDLE);
     return false;
   }
+  enum_xfer_timeout_arm();
 
   if (!is_nonblocking) {
     // No tuh_connected() escape needed: usbh_device_close() routes through
@@ -1019,6 +1038,7 @@
                       tu_str_std_request[xfer.setup.bRequest] : "Class Request");
       TU_LOG_BUF_USBH(&xfer.setup, 8);
       if (hcd_setup_send(usbh_get_rhport(xfer.daddr), xfer.daddr, (uint8_t const *) &_usbh_epbuf.request)) {
+        enum_xfer_timeout_arm();
         return; // transfer kicked-off, we are done
       }
     }
@@ -1032,6 +1052,13 @@
   TU_LOG_USBH("\r\n");
   usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
 
+#if CFG_TUH_ENUM_TIMEOUT_MS
+  // disarm enumeration watchdog before callback, which may schedule a delay function
+  if (_usbh_data.call_after.func == enum_xfer_timeout_async) {
+    _usbh_data.call_after.func = NULL;
+  }
+#endif
+
   // duplicate xfer since user can execute control transfer within callback
   tusb_control_request_t const request = _usbh_epbuf.request;
   tuh_xfer_t xfer_temp = {
@@ -1696,7 +1723,6 @@
 static uint8_t enum_get_new_address(bool is_hub);
 static bool    enum_parse_configuration_desc(uint8_t dev_addr, const tusb_desc_configuration_t *desc_cfg);
 static void    enum_full_complete(bool success);
-static void    process_enumeration(tuh_xfer_t *xfer);
 
 enum {
   ENUM_AFTER_DEBOUNCING_DELAY,
@@ -2198,6 +2224,30 @@
   }
 }
 
+#if CFG_TUH_ENUM_TIMEOUT_MS
+// Enumeration watchdog expired: the device ACKed SETUP but never completed a later stage
+// (e.g NAK forever). Abort at hcd level and complete as FAILED (not ABORTED, which
+// process_enumeration does not treat as error) to fail this enumeration and resume hub polling.
+static void enum_xfer_timeout_async(uintptr_t user_data) {
+  const uint8_t daddr = (uint8_t) user_data;
+  const usbh_ctrl_xfer_info_t* ctrl_info = &_usbh_data.ctrl_xfer_info;
+  TU_VERIFY(ctrl_info->stage != CONTROL_STAGE_IDLE && ctrl_info->daddr == daddr, );
+
+  const tusb_control_request_t* request = &_usbh_epbuf.request;
+  uint8_t ep_addr = 0; // SETUP stage is OUT
+  if (ctrl_info->stage == CONTROL_STAGE_DATA) {
+    ep_addr = tu_edpt_addr(0, request->bmRequestType_bit.direction);
+  } else if (ctrl_info->stage == CONTROL_STAGE_ACK) {
+    ep_addr = tu_edpt_addr(0, 1 - request->bmRequestType_bit.direction);
+  }
+
+  const uint8_t rhport = usbh_get_rhport(daddr);
+  TU_LOG_USBH("[%u:%u] Control transfer timed out after %u ms\r\n", rhport, daddr, (unsigned) CFG_TUH_ENUM_TIMEOUT_MS);
+  hcd_edpt_abort_xfer(rhport, daddr, ep_addr);
+  control_xfer_complete(daddr, XFER_RESULT_FAILED);
+}
+#endif
+
 static void enum_full_complete(bool success) {
   (void)success;
   TU_LOG_USBH("Enumeration complete: success = %u\r\n", success);
diff --git a/src/tusb_option.h b/src/tusb_option.h
index 1eb23fb..8b3a41a 100644
--- a/src/tusb_option.h
+++ b/src/tusb_option.h
@@ -753,6 +753,13 @@
     #define CFG_TUH_ENUMERATION_BUFSIZE 256
   #endif
 
+  // Timeout in ms for each enumeration control transfer, 0 to wait forever.
+  // A broken device that ACKs SETUP but never completes the following stage would otherwise
+  // stall enumeration and hub polling forever. 5s matches Linux's USB_CTRL_GET_TIMEOUT.
+  #ifndef CFG_TUH_ENUM_TIMEOUT_MS
+    #define CFG_TUH_ENUM_TIMEOUT_MS 5000
+  #endif
+
 #endif // CFG_TUH_ENABLED
 
 // Attribute to place data in accessible RAM for host controller (default: CFG_TUSB_MEM_SECTION)