Get unstuck

Hint 1: Which length do you trust?

payload_length is what the sender claims; record.length is what arrived. Every read must stay inside the record, so compare the claim with the record before copying.

Hint 2: The padding is part of the format

RFC 6520 requires at least 16 bytes of padding, so the payload can be at most record.length − 19 bytes. A check of 3 + payload_length <= record.length stays in bounds but accepts malformed messages.

Reveal the reference patch and explanation
Fixedheartbeat.mjsJavaScript
export function heartbeatResponse(memory, record) {
  const heap = Uint8Array.from(memory);
  const {offset, length} = record;
  // The header (type and payload_length) and 16 bytes of padding must fit in the record.
  if (length < 1 + 2 + 16) return null;
  // A view of this record only: any read beyond it throws instead of leaking.
  const view = new DataView(heap.buffer, offset, length);
  if (view.getUint8(0) !== 1) return null; // only heartbeat_request
  const payloadLength = view.getUint16(1); // big-endian, as on the wire
  // RFC 6520 §4: a payload_length too large for the record is discarded silently.
  if (1 + 2 + payloadLength + 16 > length) return null;
  return Array.from(heap.subarray(offset + 3, offset + 3 + payloadLength));
}

A request with payload "ping-0001", an empty payload, a 120-byte payload, 40 bytes of padding and exactly 16 bytes of padding are echoed exactly. Discarded with null: a one-byte payload claiming 160 bytes (the over-read, even though 160 bytes fit in memory), a payload_length that leaves no room for padding, one that leaves 15 bytes of padding, a record of 2 bytes, a heartbeat_response (type 2) and an unknown type, and a record at the end of memory claiming 65,535 bytes. Reading payload_length little-endian fails the ordinary request, because 0x0009 becomes 0x0900.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

Inspect the actual test harness

The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.

Download cases.mjs
How the browser sandbox runs your code

Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.

The same as in current Node.js:

  • ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag

Added to the sandbox so that it behaves like Node.js:

  • structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
  • URL and URLSearchParams, parsed by your browser’s own URL implementation
  • TextEncoder and TextDecoder for UTF-8
  • Intl.Collator and a locale-aware String.prototype.localeCompare
  • console.log and related methods, whose output appears with your results

Not available in the browser sandbox:

  • Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
  • Node.js modules and globals: import, require, process and Buffer
  • Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
  • Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
  • using declarations, Symbol.dispose and DisposableStack (explicit resource management)
  • Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace

Use the lesson in your own system

This is a model: a JavaScript array cannot be over-read, so the lab places buffers side by side in one array to show what an unchecked memcpy does in C. Real defences are memory-safe languages for new code, and in C and C++ bounds checks derived from the format, sanitizers (AddressSanitizer reports a heap-buffer-overflow read here) and fuzzing; the native memory-safety project on the labs page compiles a real C parser with them. OpenSSL fixed the bug in 1.0.1g; its advisory of 7 April 2014 says the missing bounds check could reveal up to 64k of memory to a connected client or server.

Your next task: Find a parser in your code base, in any language, that reads a length field from its input. Where does it compare that length with the number of bytes it actually received?

RFC 6520: TLS and DTLS Heartbeat Extension, section 4

Related guide: Find, fix and contain memory-safety bugs in C and C++