)]}'
{
  "commit": "d2b6142bc34b0e10ffd9f8a21634f1fa0c54139e",
  "tree": "b4dd99e09ec609325ddb12ce18fbdcbed8cdff9d",
  "parents": [
    "fb4dcf442ee46b70279fdf9b9cabe3e1ac419560"
  ],
  "author": {
    "name": "liangxiao.lv",
    "email": "liangxiao.lv@potentia.ai",
    "time": "Mon Sep 28 16:52:02 2026 +0800"
  },
  "committer": {
    "name": "hathach",
    "email": "thach@tinyusb.org",
    "time": "Mon Oct 05 23:49:53 2026 +0700"
  },
  "message": "dcd_dwc2: serialize diepempmsk read-modify-write with usbd_spin_lock\n\ndiepempmsk gates the TXFE (TxFIFO empty) interrupt of every IN endpoint in a\nsingle 32-bit register, and two places read-modify-write it:\n\n  edpt_schedule_packets()  sets the endpoint\u0027s bit\n  handle_epin_slave()      clears the endpoint\u0027s bit\n\nThey touch *different* endpoints\u0027 bits of the *same* word, so a write based on a\nstale snapshot wipes the other writer\u0027s bit.\n\nObserved interleaving (core0 \u003d USB ISR, core1 \u003d task submitting CDC data):\n\n  core0 (ISR)                                core1 (task)\n  set ep2: 0x00 -\u003e 0x04\n                                             read 0x04, write 0x14\n  clear ep2: read 0x04 (stale)\n             write 0x00  ------------------\u003e ep4\u0027s just-set bit is gone too\n\nThe victim is a bulk IN endpoint: its gate is now closed, so the already armed\ntransfer (DIEPTSIZ.XferSize \u003e 0) never gets its TXFE interrupt, nothing is moved\ninto the TxFIFO, and the transfer never completes - a bulk endpoint is only\nre-armed on completion. Isochronous endpoints re-arm every frame and therefore\nself-heal, which is why the failure presents as \"audio keeps working, CDC\nsilently dies\".\n\nLatch snapshot: DIEPTSIZ.XferSize \u003e 0, DTXFSTS \u003d 0, the endpoint\u0027s diepempmsk\nbit cleared.\n\nThe set side is already serialized: it only runs for non-EP0 endpoints, and\ndcd_edpt_xfer()/dcd_edpt_xfer_fifo() hold usbd_spin_lock() across the whole\nscheduling (dcd_edpt_close_all() zeroes the same register under that lock as\nwell). The clear side runs in the endpoint ISR, which does not hold it, so take\nthe same lock there - around the epin-\u003etsiz read as well as the update. The ISR\nqueues the transfer completion before reading tsiz, so the task can re-arm that\nendpoint in between, and a clear based on the pre-arm tsiz would wipe the bit of\nthe transfer that was just armed.\n\nThat is deliberately the existing lock and not a dedicated one: OSAL spinlocks\nmust not be nested. On OPT_OS_NONE each spinlock keeps its own nesting count, so\nreleasing an inner lock re-enables interrupts while the outer critical section\nis still running. Sharing it also keeps a single protection domain for the\nregister.\n\nValidated on ESP32-S3 with ESP-IDF 6.0 (composite UAC + CDC-ACM + DFU, host\nrecording audio and streaming CDC at the same time): putting a critical section\naround these two statements took the latch rate from 4 events / 6120 s to 0\nevents / 9600 s (~2.0e8 mask writes). That run serialized them with a separate\nOSAL spinlock in espressif/tinyusb 0.19.0~3; this patch gives the same mutual\nexclusion through the lock the writers already share.\n",
  "tree_diff": [
    {
      "type": "modify",
      "old_id": "5c0f4201d341190f830174034b7ce26cb0e0c15d",
      "old_mode": 33188,
      "old_path": "src/portable/synopsys/dwc2/dcd_dwc2.c",
      "new_id": "90467651ac52d27f7272663f5b2f739da3b57e41",
      "new_mode": 33188,
      "new_path": "src/portable/synopsys/dwc2/dcd_dwc2.c"
    }
  ]
}
