CVE-2026-37555: Pre-Auth DoS in Vanetza V2X via Uncaught ECC Exception
TL;DR
| Field | Value | |----|----| | CVE | CVE-2026-37555 | | Affected Software | Vanetza ≤ v26.02 | | CVSS 3.1 | 6.5 Medium (AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) | | CWE | CWE-248 Uncaught Exception (parent: CWE-703) | | Attack Requirement | RF proximity (~300–1000 m via ITS-G5 / 802.11p OCB) | | Fix Available | No (as of 2026-05-01) | | Discovery | AFL++ fuzzing — 41 cores, 139M+ executions |
An unauthenticated attacker within 802.11p broadcast range can terminate
any process running the Vanetza GeoNetworking stack by sending a single
crafted packet containing an ECC point that is syntactically valid but
does not lie on the curve. The OpenSSL error raised during curve
membership verification surfaces as a
vanetza::security::openssl::Exception (a subclass of
std::runtime_error) and unwinds past every catch boundary on the
receive path, reaching std::terminate and aborting the process.
Background: Vanetza and European C-ITS
Vanetza is the de facto open-source C++ implementation of the ETSI ITS-G5 / IEEE 802.11p V2X (vehicle-to-everything) protocol stack. It powers research testbeds, commercial Road Side Units (RSUs), and On-Board Units (OBUs) across EU C-ITS deployments — the cooperative intelligent transport system rollout aligned with Directive 2010/40/EU (the ITS Directive) and built on the ETSI EN 302 663 / TS 103 097 / EN 302 637 family of standards.
Unlike cellular V2X (C-V2X), ITS-G5 is a broadcast, infrastructure-independent mode: packets are transmitted over the 5.9 GHz ITS-allocated band (ECC/DEC/(08)01 in Europe) with no prior association, authentication handshake, or session state. The link layer operates in IEEE 802.11 OCB (Outside the Context of a BSS) mode — a single-hop broadcast medium with no AP, no DS, and no routing. Security is provided at the application layer via ETSI TS 103 097 certificate-based signing, but the GeoNetworking layer must parse and verify incoming security headers before the application layer ever sees them. That parse/verify window — and specifically the boundary between them — is where this vulnerability lives.
Discovery Methodology
The crash was discovered during a targeted fuzzing campaign against Vanetza's GeoNetworking receive path:
| Parameter | Value | |----|----| | Fuzzer | AFL++ (latest, persistent mode) | | Parallelism | 41 cores | | Total executions | 139,000,000+ | | Campaign duration | 72 hours | | Total unique crashes | 3,988 | | Root causes (deduplicated) | 3 | | Deduplication method | Opus 4.7 + Gemini 3.1 Pro cross-validation on sanitizer traces |
A custom GeoNetworking packet mutator was implemented to preserve structural validity of outer headers (ensuring packets reach the security processing stage) while mutating the embedded certificate and cryptographic material. Without this domain-aware mutator, AFL++ exhausts its budget failing header structure checks before reaching the security subsystem.
The 3,988 crashes deduplicated to three root causes:
| CVE | Crash Signature | Root Cause | |----|----|----| | CVE-2026-37554 | "invalid compressed point" | Corrupted compressed ECC point decompression | | CVE-2026-37555 | "point is not on curve" | Off-curve point triggers uncaught exception | | CVE-2026-37556 | "OER encoding failed" | OER serialization failure in security headers |
All three share the same structural defect: OpenSSL primitive failure →
openssl::check() throws → no enclosing handler → std::terminate.
Patching the catch boundary in Router::indicate() mitigates all three
crash classes simultaneously. This article focuses on CVE-2026-37555;
the other two will be analysed in companion posts.
Technical Analysis
CVE-2026-37555 is a pre-authentication crash in Vanetza's
GeoNetworking receive path. When a GeoNetworking packet arrives bearing
a signed certificate whose embedded ECC public key coordinates are
formally well-formed (correct byte length, within field bounds) but do
not satisfy the elliptic curve equation y² ≡ x³ + ax + b (mod p),
OpenSSL's EC_POINT_is_on_curve() returns 0. Vanetza's thin OpenSSL
wrapper translates that result into a C++ exception — and no catch
clause on the call path is positioned to receive it.
Crash signature observed across AFL++ corpus minimization:
terminate called after throwing an instance of
'vanetza::security::openssl::Exception'
what(): point is not on curve
Aborted (core dumped)
The bug is not "missing input validation" — Vanetza does validate the curve. The bug is that validation is implemented by throwing, the throw site sits inside the cryptographic verification stage, and the only exception barrier on the receive path is wrapped around the deserialization stage that runs strictly before verification. The two never overlap.
1. The throw site — vanetza/security/openssl_wrapper.cpp
The wrapper exists to convert OpenSSL's C-style int return convention
into C++ exceptions:
// vanetza/security/openssl_wrapper.hpp
class Exception : public std::runtime_error
{
public:
explicit Exception(const std::string& msg)
: std::runtime_error(msg) {}
Exception()
: std::runtime_error(latest_error_string()) {}
private:
static std::string latest_error_string();
};
std::string check_error();
void check(int rc);
// vanetza/security/openssl_wrapper.cpp
std::string check_error()
{
std::ostringstream oss;
bool first = true;
while (unsigned long err = ERR_get_error()) {
std::array<char, 256> buf{};
ERR_error_string_n(err, buf.data(), buf.size());
if (!first) oss << "; ";
oss << buf.data();
first = false;
}
return first ? std::string{"unknown OpenSSL error"} : oss.str();
}
void check(int rc) // line 19
{
if (rc != 1) {
throw Exception{check_error()}; // ← unconditional throw
}
}
When BackendOpenSSL::public_key_from_cert() rebuilds an EC_POINT
from a certificate's affine coordinates and passes it to
EC_POINT_is_on_curve(), an off-curve point causes that primitive to
return 0. check(0) then throws.
2. The exception type
Two properties of vanetza::security::openssl::Exception matter:
- It inherits from
std::runtime_error(and thereforestd::exception). Any well-placedcatch (const std::exception&)would stop it. There is no such catch on the path. - The throw is not a
noexceptviolation —std::terminateis reached because the exception runs out of frames with handlers and falls off the top of the call stack. Per[except.handle]/9(C++17), if no matching handler is found anywhere on the unwound stack,std::terminate()is called. The Vanetza receive callback installed on the link layer is the topmost frame on this thread's call chain, and it registers no handler.
3. The call chain from radio to throw
[link-layer RX callback]
└─ Router::indicate(UpPacketPtr, MacAddress, MacAddress)
└─ Router::indicate_basic(IndicationContext&)
└─ Router::indicate_common(IndicationContext&, BasicHeader)
└─ Router::indicate_extended(IndicationContextBasic&,
CommonHeader)
│
│ // STAGE A: deserialize (guarded)
│ ┌──────────────────────────────────────────────┐
│ │ SecuredMessageView Router::parse_secured( │
│ │ IndicationContext& ctx) { │
│ │ try { │
│ │ deserialize_oer(ctx); │
│ │ } catch (...) { /* discard packet */ } │
│ │ // <-- try/catch scope ENDS HERE │
│ │ return secured_message; │
│ │ } │
│ └──────────────────────────────────────────────┘
│ parse_secured returns NORMALLY → frame popped
│
│ // STAGE B: verify (UNGUARDED)
└─ SecurityEntity::decapsulate_packet(SecuredMessageView)
└─ CertificateV3Provider::verify(CertificateV3)
└─ BackendOpenSSL::verify_data(...)
└─ BackendOpenSSL::public_key_from_cert(...)
│ EC_POINT_set_affine_coordinates(
│ group, pt, x, y, bn_ctx);
│ int rc = EC_POINT_is_on_curve(
│ group, pt, bn_ctx);
└─ openssl::check(rc);
│
▼
throw openssl::Exception("point is not on curve")
4. Why the catch in parse_secured() does not help
The direct answer to "does parse_secured's catch(...) run before or
after the ECC check throws?" is neither — it never runs at all on
this code path. A C++ handler only fires when an exception propagates
out of its associated try-block while that try-block is still active on
the stack. Three independent reasons each defeat the catch here:
- Lexical scope. The
trybrackets only the OER/ASN.1 deserialization call.decapsulate_packet() → verify() → verify_data() → public_key_from_cert() → check()execute afterparse_secured()has already returned. Verification is initiated by the caller (indicate_extended()), not byparse_secured(). The throw originates outside the lexical scope of the onlytryon the path. - Stack scope. C++ unwinding (
[except.throw]) walks up the dynamic call stack searching for a handler whose try-block is currently active. By the timecheck()throws,parse_secured()has returned via the normal path, its frame has been popped, automatic objects have been destroyed, and its handler region is no longer on the active handler chain. Even broadening the catch tocatch (const std::exception&)would change nothing — there is no live frame for the unwinder to land on. The unwinder then walksverify_data → verify → decapsulate_packet → indicate_extended → indicate_common → indicate_basic → indicate → link-layer callback. None register a handler. - Top-of-stack behavior. When the exception leaves
Router::indicate()and enters the link-layer RX callback (typically astd::functioninvoked from the access-layer driver loop), the runtime finds no further handlers up to thread entry.std::terminate()runs, callingstd::abort(). That is theAborted (core dumped)observed.
In short: there is exactly one catch on the path, it guards the wrong
stage, and the frame that owned it has already returned by the time the
throw happens. Every frame between the throw site and the thread
boundary is exception-transparent.
5. CWE classification
This is CWE-248 Uncaught Exception ("The product does not catch an exception thrown by an underlying class/function, which results in program termination"). Its parent in the MITRE Research view (CWE-1000) is CWE-703 Improper Check or Handling of Exceptional Conditions; CWE-755 (Improper Handling of Exceptional Conditions) is a sibling under CWE-703, not an ancestor of CWE-248. CWE-248 is the tighter mapping for this defect: the exception is well-formed, the type hierarchy is sane, the throw is intentional — the only failure is the absence of a handler at any frame between the throw site and the thread entry point. This distinguishes the bug from CWE-754 (missing check) and CWE-20 (missing input validation): Vanetza's validation logic is present and correct; only its propagation discipline is broken.
Real-World Impact
Exploitability
The attack requires nothing beyond 802.11p broadcast capability — any
WLAN adapter capable of operating in OCB mode on 5.9 GHz is sufficient.
Constructing an off-curve point is trivial: random (x, y) pairs of
correct field length are almost certainly not on the curve. No
knowledge of any private key is required; the throw fires during
public-key import, before any signature verification arithmetic
begins. The packet only needs to traverse the GeoNetworking header
parser — which performs no ECC checks — and reach
SecurityEntity::decapsulate_packet(). Single packet, single shot,
deterministic crash. No memory layout assumptions, no timing windows, no
privileges.
CVSS 3.1 Vector Justification
Score: 6.5 Medium — CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
The most consequential decision is AV:A (Adjacent), not AV:N (Network). CVSS 3.1 §2.1.1 reserves AV:N for components reachable across a routed (typically Layer 3 / IP) network; AV:A explicitly enumerates IEEE 802.11 (and analogous single-broadcast-domain links) as a canonical example. ITS-G5 is IEEE 802.11p OCB on a non-routed, link-local broadcast medium — there is no IP path from the public Internet to the receive callback, so AV:N is technically inadmissible regardless of physical range. The attacker must therefore be physically within the ~300–1000 m radio horizon of the target unit (line-of-sight from any public roadway, parking area, or pedestrian space adjacent to the antenna), which precisely matches CVSS's intent for AV:A.
| Vector | Value | Justification | |----|----|----| | AV | Adjacent | IEEE 802.11p OCB; single-hop broadcast, no IP routing path from the public Internet. | | AC | Low | No race conditions, no heap layout requirements. One packet triggers the crash. | | PR | None | V2X is broadcast. Zero association/auth state at the link layer. | | UI | None | Receive path processes the packet automatically. | | S | Unchanged | Crash contained to the Vanetza process. | | C | None | Crash only; no exfiltration. | | I | None | No memory corruption with write primitive. | | A | High | Complete crash of V2X stack. Without watchdog, disables CAM/DENM until manual restart. |
Numerical computation: - ISS = 1 − [(1−0)(1−0)(1−0.56)] = 0.56 → Impact = 6.42 × 0.56 = 3.595 - Exploitability = 8.22 × 0.62 × 0.77 × 0.85 × 0.85 = 2.835 - Base Score = roundup(3.595 + 2.835) = 6.5
The score drops from 7.5 (had AV:N been claimed) to 6.5. Operational severity does not drop proportionally — RF-range access from any public roadway with line-of-sight to the antenna is a much weaker constraint than "same Wi-Fi LAN" but still strictly stronger than "anywhere on the Internet."
Operational Consequences
Direct. Any Vanetza RSU or OBU crashes on receipt of a single crafted packet. In a production C-ITS deployment — highway on-ramps, urban intersections, toll plazas — this silences cooperative awareness messages (CAMs) and decentralized event notifications (DENMs) for the affected unit's coverage zone.
Systemic. V2X safety applications (emergency vehicle preemption, intersection collision avoidance, wrong-way driver alerts) depend on continuous CAM/DENM reception. A sustained broadcast attack — replaying the same crafted frame at the channel's beacon rate — can maintain a denial-of-service against all Vanetza nodes within radio range with trivial attacker resource cost.
Scale. Vanetza is used across EU C-ITS research and early-deployment infrastructure. ETSI ITS-G5 is the reference access layer for European C-ITS Day-1 services. The attack surface includes every ETSI C-ITS deployment that has not replaced Vanetza with a hardened alternative.
Disclosure Timeline
| Date | Event | |------------|-------------------------------------------------------------| | 2026-04 | AFL++ campaign initiated (41 cores, 72h) | | 2026-04 | 3,988 crashes collected; root causes deduplicated to 3 CVEs | | 2026-05 | Submitted to MITRE (ticket #2016560) | | 2026-05 | CVE-2026-37554, CVE-2026-37555, CVE-2026-37556 assigned | | 2026-05-01 | Public disclosure. No fix available. |
Coordinated disclosure was attempted. The Vanetza GitHub project has no
SECURITY.md, no security advisory mechanism, and no documented
vulnerability reporting contact. Disclosure proceeded on standard 90-day
timeline from crash confirmation.
Mitigation Guidance (Interim)
- Install a top-level handler in
Router::indicate(). The minimal fix is structural, not cryptographic — install acatch (const std::exception&)at the public entry point so unwinding cannot escape into the link-layer callback:
void Router::indicate(UpPacketPtr packet, const MacAddress& sender, const MacAddress& destination) {
try { IndicationContext ctx{std::move(packet), sender, destination}; indicate_basic(ctx);
} catch (const vanetza::security::openssl::Exception& e) {
if (m_log) m_log->warn( "indicate: dropping packet, OpenSSL error: {}",
e.what());
} catch (const std::exception& e) {
if (m_log) m_log->warn( "indicate: dropping packet: {}",
e.what()); }
}
This converts a process-killing throw into a packet drop. It does not address the design question of why curve validation is implemented as a throw rather than a return code, but it closes the DoS primitive.
- Deploy a watchdog / supervisor (
systemd Restart=always,supervisord) to auto-restart Vanetza on crash. Limits DoS window to restart latency (~1–3 s); does not prevent the crash. - RF-layer ingress rate limiting on RSU hardware to cap GeoNetworking packet rate per sender MAC. 802.11p MACs are pseudonymous and trivially spoofable — coarse mitigation only.
- Monitor for repeated
std::terminate/Abortedevents in Vanetza process logs as an indicator of active exploitation.
About Our Work
Innora Security Research is the vulnerability discovery and protocol security arm of Innora, focused on embedded systems, safety-critical infrastructure, and emerging wireless protocols (V2X, BLE, LoRaWAN, 5G NR sidelink).
Argus, our AI-powered security scanner, drove the deduplication and triage stage of this research — collapsing 3,988 raw AFL++ crashes into 3 distinct root causes via cross-LLM (Claude Opus 4.7 + Gemini 3.1 Pro) sanitizer-trace analysis. Argus is being productized for embedded C/C++ codebases where traditional SAST tooling produces unmanageable false-positive volumes.
CVE Research Program. This advisory is the first public output of Innora's V2X security program targeting ETSI ITS-G5 / C-V2X deployment stacks. CVE-2026-37554 and CVE-2026-37556 will be published in companion advisories. The roadmap includes additional ITS-G5 stacks, autonomous-driving middleware, and OEM telematics units.
- Blog: innora.ai/blog
- Responsible disclosure: [email protected]
- Argus enquiries: [email protected]
- General contact: [email protected]

Related Chronicles
CUDA BIP39 Kernel Bug: When Negative Shifts Silently Corrupt Your Entropy
A CUDA BIP39 kernel bug: missing checksum-bit guard causes wrap-around negative shifts to silently corrupt entropy. Bug, PoC, and one-line fix.
ERC-4337 Paymaster Attacks: The Gas Fee Extraction Gap Nobody Is Fixing
ERC-4337 paymasters have a gas accounting gap. Here is the PoC and the fix.
How a Single Math.min() Broke Cross-Chain Security: Dissecting the Hyperlane WeightedMultisigIsm Bug
How Math.min() in Hyperlane's WeightedMultisigIsm silently rejected valid signatures, risking permanent fund freezing on warp routes.
Subscribe for AI Security Insights
Join 5,000+ engineers and security researchers. Get our latest deep dives into Sovereign AI, Red Teaming, and System Architecture.
No spam. Unsubscribe at any time.
Comments are currently disabled.