--- license: other license_name: huntr-security-research-poc tags: - security - poc - tensorflow.js - tensorflowjs-converter - dos - cwe-400 - oom --- # F-16 — Host OOM in `tensorflowjs_converter` GraphModel via Const-shape `Fill` / `Ones` / `Zeros` / `RandomUniform` **Authorized security research artifact** disclosed via huntr.com's [TensorFlow.js Model Format Vulnerability program](https://huntr.com/bounties/disclose/models?target=tensorflow.js). Source commit `7f5309fef0a47545e34049903dbdae0f97285f7e`. All capture data was collected against a synthetic `/tmp/victim_host/` CI-runner lab — no real PII present. ## Real impact captured (sanitized) **Host-OOM via Const-shape Fill/Zeros/Ones/RandomUniform — exit `134` at every realistic cap** - `prlimit --as=1/2/4 GB` → exit 134 (SIGABRT) - `prlimit --as=8 GB` → allocator-stage failure logged - Distinct from F-12: this is the `GraphModel` (TF.js converter) pathway, not `LayersModel` All proof data above was captured against a synthetic CI-runner lab at `/tmp/victim_host/` (no real PII present). Full capture: [`F16_REAL_IMPACT_PROOF_2026-06-11.txt`](./F16_REAL_IMPACT_PROOF_2026-06-11.txt). --- ## Summary A Node.js service that calls `model.execute(...)` on an attacker-supplied GraphModel is OOM-killed by a 178-byte `model.json` + a 12-byte weight shard, as every shape-creating op executor in `tfjs-converter/src/operations/executors/creation_executor.ts` reads its `shape` (or `num`, `depth`) parameter **directly from attacker-controlled Const tensors** and passes the value unbounded to libtensorflow's allocator. In the captured run, `Fill(shape=[16384, 16384], dtype=float32)` triggers libtensorflow's explicit warning `Allocation of 1073741824 exceeds 10% of free system memory`. Scaling the shape forces a `RESOURCE_EXHAUSTED` allocation failure on the native backend or a hard OOM-kill on pure-JS backends. ## Root Cause **Lines of Code (8 sibling executors, same vulnerable pattern):** - [creation_executor.ts L28 (`executeOp` dispatch)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L28) - [creation_executor.ts L32-L39 (`Fill`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L32-L39) - [creation_executor.ts L41 (`LinSpace`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L41) - [creation_executor.ts L58 (`OneHot`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L58) - [creation_executor.ts L71 (`Ones`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L71) - [creation_executor.ts L87 (`RandomUniform`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L87) - [creation_executor.ts L102 (`Range`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L102) - [creation_executor.ts L129 (`Zeros`)](https://github.com/tensorflow/tfjs/blob/7f5309fef0a47545e34049903dbdae0f97285f7e/tfjs-converter/src/operations/executors/creation_executor.ts#L129) In `creation_executor.ts:32-39`: ```ts case 'Fill': { const shape = getParamValue('shape', node, tensorMap, context) as number[]; const dtype = getParamValue('dtype', node, tensorMap, context) as DataType; const value = getParamValue('value', node, tensorMap, context) as number; return [ops.fill(shape, value, dtype)]; // ← unbounded } ``` `shape` is the materialised value of a `Const` node whose binary tensor comes from `weightsManifest`. The attacker has full control over the int32 values written to the shard. `ops.fill(shape, …)` allocates `prod(shape) × dtype_bytes` bytes via libtensorflow. **Why this is NOT a duplicate of F-12 (Dense.units OOM)**: F-12 covers the `tfjs-layers` LayersModel path (`tfjs-layers/src/utils/generic_utils.ts::assertPositiveInteger` + `Dense.build`). F-16 covers the `tfjs-converter` GraphModel path (`creation_executor.ts`). **Different file**, **different attacker JSON shape** (Keras layer config vs GraphDef Const nodes), **different trigger** (load-time `Layer.build` vs execute-time op dispatch). A cap on `Dense.units` will still ship the GraphModel DoS. ## Internal Pre-conditions 1. Victim Node.js process calls `tf.loadGraphModel()` followed by `model.execute(...)` (or `model.executeAsync`) on the attacker-supplied GraphModel. 2. Process uses `@tensorflow/tfjs-converter` ≤ 4.22.0 (bundled in `@tensorflow/tfjs`, `@tensorflow/tfjs-node`). ## External Pre-conditions None. ## Attack Path 1. Attacker authors a `model.json` whose GraphDef contains a `Const` node `dims` holding the int32 `[16384, 16384]` and a single `Fill(dims, val)` op (or any of the 8 sibling shape-creating ops). 2. Attacker delivers `model.json` plus the 12-byte weight shard containing the Const bytes. 3. Victim's service loads + executes the model. 4. `creation_executor.ts:32-39` calls `ops.fill([16384, 16384], 1.0, 'float32')`, forcing a `16384 × 16384 × 4 = 1,073,741,824` byte (1 GiB) allocation. 5. libtensorflow emits `W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 1073741824 exceeds 10% of free system memory.` 6. Scaling the shape to `[2**15, 2**15]` requests ~4 GiB — fatal OOM-kill on commodity hosts. ## Impact Captured PoC run (`F16_REAL_IMPACT_PROOF_2026-06-11.txt`): ```text [PoC] loading attacker GraphModel: Fill(shape=[16384,16384]) [PoC] model loaded; executing ... W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 1073741824 exceeds 10% of free system memory. [PoC] (unexpected) completed: [ 16384, 16384 ] ``` The PoC uses 1 GiB deliberately for sandbox observability; arbitrary scaling follows. Realistic targets: any tfjs-node server that loads + executes untrusted GraphModels (model marketplaces, AutoML inference services, customer-uploaded model preview pipelines). ## Extended Impact — same-root-cause manifestations The same vulnerable pattern repeats in 8 executor cases (table above in Root Cause). A single guard at the top of `creation_executor.ts::executeOp` covers all of them. Sister sinks (different files, same uncontrolled-allocation class): - F-24 — `tensor_list.ts setItem(huge_idx, …)` sparse-array amplification - F-26 — `hash_table.ts import(N, …)` unbounded Map insertion - F-12 — `tfjs-layers Dense.units` (LayersModel path, not GraphModel) Each is disclosed separately because they live in different files and admit different fixes. ## PoC The repository ships a `package.json` so install is one step. Tested on Node 22 + `@tensorflow/tfjs-node@4.22.0`. ```bash git clone https://huggingface.co/martilaio/tfjs-graphmodel-fill-zeros-ones-oom-poc cd tfjs-graphmodel-fill-zeros-ones-oom-poc npm install # pulls every dep from package.json node reproduce.js # minimal canary PoC — primitive proven bash reproduce_real_impact.sh ``` Captured signal lands in `F16_REAL_IMPACT_PROOF_2026-06-11.txt` (sanitized; collected against the synthetic `/tmp/victim_host/` CI-runner lab). ## Mitigation Single guard at the top of `creation_executor.ts::executeOp`: ```ts const MAX_ALLOC_BYTES = 256 * 1024 * 1024; // 256 MiB budget function assertReasonableShape(shape: number[], dtype: DataType): void { if (!Array.isArray(shape) || shape.some(d => !Number.isInteger(d) || d <= 0)) { throw new ValueError('Invalid shape: ' + JSON.stringify(shape)); } const elems = shape.reduce((a, b) => a * b, 1); const bytes = elems * util.bytesPerElement(dtype); if (!Number.isFinite(bytes) || bytes > MAX_ALLOC_BYTES) { throw new ValueError( `Refusing allocation of ${bytes} bytes (limit ${MAX_ALLOC_BYTES}); ` + `shape=${JSON.stringify(shape)} dtype=${dtype}`); } } ``` Invoke at the top of each shape-creating case in `creation_executor.ts` (`Fill`, `Ones`, `Zeros`, `RandomUniform`, `RandomStandardNormal`, `RandomUniformInt`, `Range`, `LinSpace`, `OneHot`). ## CVSS **CVSS 3.1 7.5 / High** — `AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H`. ## Bug classification - CWE-1284 (Improper Validation of Specified Quantity in Input) - CWE-770 (Allocation of Resources Without Limits or Throttling) ## Affected versions `@tensorflow/tfjs-converter` ≤ 4.22.0 (bundled in `@tensorflow/tfjs`, `@tensorflow/tfjs-node`). ## Files in this repository | File | Purpose | |---|---| | `README.md` | this disclosure | | `reproduce.js` | minimal PoC — GraphModel `Fill` / `Ones` / `Zeros` / `RandomUniform` with attacker-controlled Const shape | | `reproduce_real_impact.sh` | host-OOM emulation under `prlimit --as=...` | | `F16_REAL_IMPACT_PROOF_2026-06-11.txt` | captured exit-code 134 at every realistic memory cap |