Frontier systems such as Mythos can inspect a repository [19]. That is not the hard part. Security work still needs a record of the code that was analyzed, the paths considered, the assumptions left open, and the source behind a finding.
SecMate was built for cases where source code cannot leave the operator’s environment and where repeatedly handing a model a large repository is too expensive. Our current workflow runs gpt-oss. It does not give the model a repository and hope for the best. It first recovers the program relations that matter, then gives the model a small question with the relevant source attached.
We do not call this model agnostic. The prompts, tools, context, and validation process are tuned for the models we use today. Switching to another model family, such as DeepSeek or Qwen, would require integration and evaluation work. Adopting a new version of a model already in use would also require evaluation and, if its interface or behavior changed, integration work. For a fixed source revision, build configuration, semantic models, candidate query, and evidence package, the graph extraction is repeatable. The model’s conclusion remains a judgment.
We presented this work at LeHack 2026 in From USB to ESP: Security Vulnerabilities in Espressif Firmware. This post is the analysis part: three vulnerabilities in Espressif USB host drivers that SecMate found automatically with this workflow. Researchers validated them, completed responsible disclosure, and received CVE assignments. The cases span bounds, lifetime, and concurrency.
The Engineering Problem
These constraints are common in product security. The source may contain customer intellectual property, fall under an NDA, or live in an environment with data residency requirements. Sending it to an external model service may be prohibited. Some teams also want control over what leaves their environment. That can include whether a provider attaches provenance metadata to generated files. Anthropic’s marking of generated images is one current example [20]. Cost matters too. A security analysis reads the same repository repeatedly as it follows paths, revisits callers and callees, and gathers support for each result.
SecMate was built for those constraints. Its analysis and inference run inside infrastructure controlled by the operator. The workflow uses the open-weight gpt-oss-20b and gpt-oss-120b models. OpenAI states that the smaller model can run with 16 GB of memory and that the larger model fits on a single 80 GB GPU [1].
Keeping inference local solves where it runs. It does not decide what code the model should see. A repository that is too large or noisy for a hosted model is still too large or noisy on premises.
Why Reduce Before Reasoning
Security bugs are rarely contained in one function. A length can enter through a parser, cross several fields, influence an allocation, and later reach a copy. A pointer can be stored in a structure, freed during teardown, and used again through a local alias.
Giving a model every file leaves it with two jobs: discover which code matters, then decide whether that code is vulnerable. The first job consumes most of the context and must be repeated for every candidate. It also makes the result difficult to audit because the model’s conclusion is no longer attached to a small, inspectable path.
SecMate treats the first job as program analysis. It indexes an exact source revision with its build context and semantic models. For each candidate, it extracts the relevant calls, control conditions, values, aliases, and source locations into a small package for review.
A frontier model can search with rg, open files, and follow a hypothesis. That is useful. Without an equivalent persistent analysis layer, it must find and retain those relationships while it investigates. SecMate keeps them outside the model.
For a fixed analysis configuration, that extraction follows the same graph relations every time. A reviewer can inspect the source path without rerunning the model.
Method: From Repository to Evidence Slice
Here is the path from a USB descriptor to a reportable finding.
Figure 1. The pipeline used in this article. The model reviews a focused hypothesis. A researcher decides whether it supports disclosure.
1. Build a program index
Full-codebase analysis is not a glob over sources and headers. For this C/C++ work, the unit is a translation unit plus the compilation command used for it. Headers are interpreted under specific macro definitions, include paths, and build options. The same .c file may appear more than once under different configurations. Clang’s JSON compilation database records those per-translation-unit commands [18]. Other languages need an equivalent front end for their compilation or runtime model. SecMate starts from an exact commit, the compilation commands, the macros and configuration in force, and any generated sources. The frontend preprocesses and parses the translation units, records source spans, resolves declarations and types, and links calls across files where the available information permits it.
Every node retains its file, line, and column. In this C/C++ analysis, it also retains the relevant build variant, translation unit, and macro expansion context. Within a run, a reviewer can open the exact line and build variant behind a claim.
That scope is part of the result. Conditional compilation, generated sources, missing build metadata, function pointers, and unresolved external code can all limit coverage. They must remain visible as unresolved facts instead of being silently treated as complete.
2. Build the AST, then identify what it is missing
The Abstract Syntax Tree records declarations, expressions, calls, assignments, and scopes. A call node shows that call syntax exists. It does not necessarily identify every function that can run. An if node records a branch. It does not establish which later statements the condition controls.
Figure 2. The syntax tree of the three files involved in the HID finding. It records local syntax, but not resolved cross-file control flow, data flow, or call/return behavior. The descriptor length read in one function and the release and later use in another are not related by those semantic edges here.
Security analysis therefore needs relations that are not present in the AST:
| Security question | Required relation or analysis |
|---|---|
| What can execute next? | Control Flow Graph (CFG) |
| Which condition governs this operation? | dominance, post-dominance, and control dependence |
| Where did this value come from? | definitions, uses, and data dependence |
| Does the path cross a function boundary correctly? | call graph and call/return context |
| Do two expressions refer to the same object? | points-to and alias analysis |
| Can two paths execute concurrently on the same object? | task or callback concurrency, shared-state, and synchronization analysis |
A CFG over-approximates possible execution order. It does not prove that every graph path is feasible. Alias and indirect call resolution are approximations too. Those limits matter when a candidate is validated.
3. Enrich the graph
The Code Property Graph described by Yamaguchi and colleagues brings syntax, control flow, and program dependence into one queryable representation [5]. At repository scale, the graph also needs resolved symbols and types, call and return relations, and alias information. Control and data dependence support slicing, which retains the statements relevant to a chosen operation and value [2] [3]. Interprocedural traversal must preserve call and return context instead of accepting arbitrary paths through the call graph [4].
Not every relation has the same status. Syntax and source spans come from the frontend. Control flow, dependence, call targets, and aliases are computed. Context-sensitive call and return relations, alias results, and classifications such as allocations, releases, copies, and locks sit on top of the base graph. Modern CPG specifications describe those calculated layers as overlays [17]. A parser error and an imprecise alias result create different kinds of uncertainty.
Figure 3. The same nodes, now carrying execution order, value flow, and resolved calls. The graph can now be traversed from an attacker-controlled source to a sink across file and function boundaries.
4. Run security analyses on the CPG
Lifetime is not an AST edge. To find a use-after-free, the analysis has to connect object identity, aliases, execution order, a release, and a later use of the same object. The same applies to the other bug classes:
- a bounds analysis starts at a copy or write and recovers the input length, destination extent, governing checks, and interprocedural path;
- a lifetime analysis connects allocation, aliases, release, replacement, and later use of the same abstract object;
- a race or double-free analysis identifies a candidate overlap between task or callback paths, then compares their access to shared state and the synchronization that should order them.
The graph does not know on its own that usb_host_transfer_free() ends an object’s lifetime, that usb_host_transfer_alloc() creates or replaces one, or that a project function provides synchronization. SecMate gets that knowledge from API and semantic models. They cover allocators, releases, copies, writes, locking primitives, and project functions. Those models are explicit inputs to the analysis.
Figure 4. Entry points, sinks, and lifetime operations such as the release are not syntactic facts. API and semantic models classify them on the graph before any vulnerability traversal runs.
Each analysis follows only the relations relevant to its question. Resolved call and return context stays with the path. So do the branch conditions. Before disclosure, the researcher checks concurrency eligibility, path feasibility, alias precision, and build coverage.
Figure 5. The lifetime traversal starts at the release and keeps the entry point, the governing branch, the reallocation, and the later uses. Those nodes and their edges become the evidence slice; the rest of the graph never reaches the model.
5. Materialize the evidence slice
For each candidate, SecMate collects the source nodes, call chain, control predicates, data or alias edges, and unresolved assumptions needed for review. The local model sees that package, not the repository. It tests a specific vulnerability hypothesis against the extracted path. A researcher then checks the source path, attack preconditions, and exploitability claim before disclosure.
The graph recovers program relations. The analysis selects the relevant path. The model reviews that path. Candidate discovery and path extraction happen before model review. A SecMate researcher validates every claim before it is reported. The model’s output is one input to that review.
This is what one such package looks like for the use-after-free presented later in this article:
- Hypothesis
- use after free of usb_transfer_t through a stale local alias
- Source
- usb_host_hid / hid_host.c · usb_class_request_get_descriptor()
- Call path
- enumerate_device() → get_report_descriptor() → usb_class_request_get_descriptor()
- Object
- urb_t reached through hid_device->ctrl_xfer, owning its data_buffer
- Lifetime
- ctrl_xfer (local) aliases the urb_t — captured before the branch usb_host_transfer_free() — releases the urb_t and its data_buffer usb_host_transfer_alloc(&hid_device->ctrl_xfer) — on success the field refers to a new urb_t ctrl_xfer->data_buffer — dereference after that lifetime ended
- Path condition
- ctrl_size < USB_SETUP_PACKET_SIZE + req->wLength — right side device-controlled
- Unresolved
- indirect callback target — resolved with medium confidence
Figure 6. A sanitized evidence package for the HID use-after-free: hypothesis, bounded path, object lifetime, governing condition, and the facts the analysis could not resolve. This package is what the local model reviews, and what the researcher audits afterward.
LLMxCPG and codebadger also use CPGs to guide repository investigation [6] [7].
Applying the Method to Espressif USB Host
ESP-IDF is Espressif’s development framework for its systems-on-chip [10]. The esp-usb repository contains USB host and device class drivers distributed through the ESP Component Registry [11].
We focused on USB host mode in the HID and UVC class drivers. Espressif documents the USB Host Library as the lowest public-facing API layer of the host stack. Class drivers sit above it and handle class-specific descriptors, transfers, callbacks, and device lifecycle events [12].
A connected USB device controls the descriptors and lengths consumed during enumeration. It also controls when it disconnects and how it behaves during transfers. That makes the class-driver boundary attacker-controlled even though the attack requires physical access. The findings affect host-side drivers, not Espressif’s USB device mode.
The following sections cite the public advisories and fixing commits for the affected versions, vulnerability details, and patches.
The Espressif Findings
- Component
usb_host_hid- Analysis problem
- Stale alias after transfer reallocation
- Affected
1.0.4and earlier- Fixed
1.1.0
- Component
usb_host_hid- Analysis problem
- Concurrent teardown of the same transfer
- Affected
1.0.4and earlier- Fixed
1.1.0
- Component
usb_host_uvc- Analysis problem
- Descriptor length copied into a fixed stack buffer
- Affected
2.3.1and earlier- Fixed
2.4.0
GitHub classifies all three advisories as Moderate. Their CVSS 3.1 base scores are 6.8, 6.4, and 6.8. The affected and fixed versions above come from the published advisories [8] [13] [15].
CVE-2025-68656: HID Descriptor Use-After-Free
The first finding shows the method on a concrete lifetime path. During enumeration, a HID device advertises wReport. The host uses that value for req->wLength. If the current control transfer is too small, usb_class_request_ frees hid_device-> and allocates a larger transfer. The local ctrl_xfer pointer was captured before the free and remains unchanged:
1usb_transfer_t *ctrl_xfer = hid_device->ctrl_xfer;
2const size_t ctrl_size = hid_device->ctrl_xfer->data_buffer_size;
3
4if (ctrl_size < (USB_SETUP_PACKET_SIZE + req->wLength)) {
5 usb_host_transfer_free(hid_device->ctrl_xfer);
6 HID_RETURN_ON_ERROR(
7 usb_host_transfer_alloc(USB_SETUP_PACKET_SIZE + req->wLength,
8 0, &hid_device->ctrl_xfer),
9 "Unable to allocate transfer buffer for EP0");
10}
11
12usb_setup_packet_t *setup =
13 (usb_setup_packet_t *)ctrl_xfer->data_buffer;
14setup->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN |
15 USB_BM_REQUEST_TYPE_TYPE_STANDARD |
16 USB_BM_REQUEST_TYPE_RECIP_INTERFACE;
The AST contains every statement above. The security evidence is in the relationships between them:
Figure 7. A single usb_host_transfer_free() releases both the urb_t and the data_buffer it owns. The local ctrl_xfer pointer was read from the field before the branch. If reallocation succeeds, the field receives a new transfer but the local pointer still refers to the released urb_t when the setup packet is written through it; if allocation fails, the error path returns before that write.
The function then writes the USB setup packet through ctrl_xfer->data_buffer. Espressif’s advisory describes attacker-controlled writes to freed heap memory, arbitrary memory corruption, and potential code execution [8].
Espressif’s fix validates the requested length, performs any required reallocation, and only then reads the field into the local pointer [9]:
1if (req->wLength > HID_MAX_REPORT_DESC_LEN) {
2 return ESP_ERR_INVALID_SIZE;
3}
4
5// Argument checks and lock acquisition omitted.
6if (ctrl_size < required_size) {
7 usb_host_transfer_free(hid_device->ctrl_xfer);
8 if (usb_host_transfer_alloc(required_size, 0,
9 &hid_device->ctrl_xfer) != ESP_OK) {
10 hid_device->ctrl_xfer = NULL;
11 hid_device_unlock(hid_device);
12 return ESP_ERR_NO_MEM;
13 }
14}
15
16usb_transfer_t *ctrl_xfer = hid_device->ctrl_xfer;
17usb_setup_packet_t *setup =
18 (usb_setup_packet_t *)ctrl_xfer->data_buffer;
The reported stale alias disappears when the alias is created after the field replacement. The same patch caps report descriptors at 2048 bytes, handles allocation failure, and enables tests for large report descriptors. This is the finding we took into the exploitation work.
CVE-2025-68657: HID Close-Path Race
The second HID issue is in teardown rather than descriptor parsing. USB event handling and application code can both reach hid_host_device_close() while sharing the same hid_iface_t state. Existing FreeRTOS critical sections and device_busy serialize parts of this path, but they do not make the READY-state check and transfer release one atomic operation across both callers. Two callers can therefore proceed to free the same usb_transfer_t [13].
This requires evidence from two execution paths. A single-function pattern can find the release operation, but it cannot establish competing ownership or determine whether the existing synchronization covers the state-check-to-release transition.
Figure 8. The analysis needs evidence of three things: two paths that may execute in parallel, the same abstract transfer object reached from both, and a gap in the synchronization of the state-check-to-release transition. The fix adds a mutex that covers that transition.
Espressif’s fix adds an open_close_mutex and holds it across the open and close paths, including the state check and interface removal [14]. The advisory also records an important practical limit: the READY-to-free window is estimated at 0.1 to 1 microsecond, while detach handling and application reaction occur on a millisecond scale. The proof of concept widened that window to demonstrate the issue deterministically [13].
CVE-2025-68622: UVC Descriptor Stack Overflow
The UVC finding is a direct bounds path in print_vs_frame_mjpeg_desc(). When configuration-descriptor printing is enabled, the function reads the device-controlled bLength and copies that many bytes into raw_desc[25], a 100-byte stack buffer:
1uint32_t raw_desc[25];
2uint32_t desc_size = ((const uvc_frame_desc_t *)_desc)->bLength;
3memcpy(raw_desc, _desc, desc_size);
A UVC frame descriptor can contain a variable-length frame-interval array. The vulnerable code did not compare desc_size with sizeof(raw_desc) before the copy. A malicious camera could therefore corrupt the stack during enumeration when CONFIG_UVC_ was enabled [15].
Figure 9. A bounds finding appears when a device-controlled copy length is not checked against the destination extent on the analyzed path. Here the destination holds 100 bytes and no predicate on that path relates the two. The build condition is part of the evidence because the vulnerable path is compiled only when descriptor printing is enabled.
Espressif fixed the issue in commit 77a38b1 by removing the temporary stack copy and reading the descriptor directly. The commit applies the same change to both MJPEG and frame-based descriptor printers [16].
Fixes
Users of the affected components should update usb_host_hid to 1.1.0 or later and usb_host_uvc to 2.4.0 or later. Products that vendor or fork these components should verify that the three fixing commits are present rather than relying only on package metadata.
The three findings also show why one generic code excerpt is not enough for automated vulnerability research. The UVC overflow needs a source, destination extent, and missing bound. The HID use-after-free needs object identity across an alias, free, field replacement, and later use. The close-path race needs two potentially concurrent teardown paths over the same shared object and the synchronization governing their state-check-to-release transition.
Static analysis identifies the stale write in CVE-2025-68656 and reconstructs the device-controlled path to it. Turning that into a working exploit asked a different set of questions: allocator behavior, heap reuse, and how much a malicious USB device actually controls on the ESP32 target. That is where the analysis stops and the target-specific work begins.
References
[1] OpenAI. “Introducing gpt-oss.” August 5, 2025. Product and model specifications
[2] Mark Weiser. “Program Slicing.” IEEE Transactions on Software Engineering, 1984. DOI
[3] Jeanne Ferrante, Karl J. Ottenstein, and Joe D. Warren. “The Program Dependence Graph and Its Use in Optimization.” ACM Transactions on Programming Languages and Systems, 1987. DOI
[4] Thomas Reps, Susan Horwitz, and Mooly Sagiv. “Precise Interprocedural Dataflow Analysis via Graph Reachability.” POPL, 1995. DOI
[5] Fabian Yamaguchi, Nico Golde, Daniel Arp, and Konrad Rieck. “Modeling and Discovering Vulnerabilities with Code Property Graphs.” IEEE Symposium on Security and Privacy, 2014. Paper
[6] Ahmed Lekssays, Hamza Mouhcine, Khang Tran, Ting Yu, and Issa Khalil. “LLMxCPG: Context-Aware Vulnerability Detection Through Code Property Graph-Guided Large Language Models.” 34th USENIX Security Symposium, 2025. Paper and presentation
[7] Ahmed Lekssays. “Bridging Code Property Graphs and Language Models for Program Analysis.” Software Vulnerability Management Workshop @ ICSE 2026. Paper
[8] Espressif. “USB Host HID Descriptor Use-After-Free Vulnerability.” GHSA-2pm2-62mr-c9x7
[9] Espressif. “Changed the handling of reallocating ctrl_xfer buffer on large report descriptors” (
81b37c9, November 25, 2025). Fix commit[10] Espressif. “Espressif IoT Development Framework.” Repository
[11] Espressif. “Espressif ESP-USB.” Repository
[12] Espressif. “USB Host.” ESP-IDF Programming Guide. Documentation
[13] Espressif. “Double-Free Race Condition in USB Host HID Device Close Path.” GHSA-gp8r-qjfr-gqfv
[14] Espressif. “Fixed race condition in hid_host_device_close()” (
cd28106, January 9, 2026). Fix commit[15] Espressif. “Stack buffer overflow in UVC descriptor printing.” GHSA-g65h-9ggq-9827
[16] Espressif. “Fixed potential buffer overflow in descriptor printing” (
77a38b1, November 21, 2025). Fix commit[17] The Code Property Graph specification. “Overlays.” Specification
[18] Clang. “JSON Compilation Database Format Specification.” Documentation
[19] Anthropic. “Assessing Claude Mythos Preview’s cybersecurity capabilities.” April 2026. Research
[20] Anthropic. “How Claude marks AI-generated content.” Support documentation
The SecMate Team