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.
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, notLayersModel
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.
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 (
executeOpdispatch) - creation_executor.ts L32-L39 (
Fill) - creation_executor.ts L41 (
LinSpace) - creation_executor.ts L58 (
OneHot) - creation_executor.ts L71 (
Ones) - creation_executor.ts L87 (
RandomUniform) - creation_executor.ts L102 (
Range) - creation_executor.ts L129 (
Zeros)
In creation_executor.ts:32-39:
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
- Victim Node.js process calls
tf.loadGraphModel(<url>)followed bymodel.execute(...)(ormodel.executeAsync) on the attacker-supplied GraphModel. - Process uses
@tensorflow/tfjs-converterβ€ 4.22.0 (bundled in@tensorflow/tfjs,@tensorflow/tfjs-node).
External Pre-conditions
None.
Attack Path
- Attacker authors a
model.jsonwhose GraphDef contains aConstnodedimsholding the int32[16384, 16384]and a singleFill(dims, val)op (or any of the 8 sibling shape-creating ops). - Attacker delivers
model.jsonplus the 12-byte weight shard containing the Const bytes. - Victim's service loads + executes the model.
creation_executor.ts:32-39callsops.fill([16384, 16384], 1.0, 'float32'), forcing a16384 Γ 16384 Γ 4 = 1,073,741,824byte (1 GiB) allocation.- libtensorflow emits
W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 1073741824 exceeds 10% of free system memory. - 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):
[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.
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:
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 |