You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

F-26 β€” Unbounded HashTableV2.LookupTableImportV2 N-keys in tensorflowjs_converter (425 MB RSS amplification)

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)

425 MB RSS from unbounded LookupTableImportV2 N-keys

  • Measured 425 MB peak RSS in the child
  • prlimit --as=2/4 GB β†’ exit 134 (SIGABRT)
  • Trigger: attacker-controlled N in HashTableV2.LookupTableImportV2

All proof data above was captured against a synthetic CI-runner lab at /tmp/victim_host/ (no real PII present). Full capture: F26_REAL_IMPACT_PROOF_2026-06-11.txt.


Summary

A Node.js service that executes an attacker-supplied GraphModel containing the sequence HashTableV2 β†’ Const keys[N] β†’ Const values[N] β†’ LookupTableImportV2(ht, keys, values) allocates N tensor references + N Map entries for any attacker-chosen N. Captured run at N = 2^18 (262,144) consumed 425 MB RSS from a 1 KB model.json and a 2 MB weight shard. Linear scaling β€” N = 4 M requests ~7 GB β†’ OOM-kill on commodity hosts.

The root cause is in tfjs-converter/src/executor/hash_table.ts:76-109 (HashTable.import), which iterates keysLength with no upper bound, retains each unpacked value Tensor via keep() (preventing surrounding tidy() from disposing it), and inserts one entry per iteration into the table's internal Map.

Root Cause

Lines of Code:

In hash_table.ts:76-109:

async import(keys: Tensor, values: Tensor): Promise<Tensor> {
  this.checkKeyAndValueTensor(keys, values);
  const $keys = await keys.data();                  // ← TypedArray of attacker length
  ...
  return tidy(() => {
    const $values = unstack(values);                // ← N retained Tensors

    const keysLength = $keys.length;                // ← no cap
    ...
    for (let i = 0; i < keysLength; i++) {          // ← no upper bound
      const key   = $keys[i];
      const value = $values[i];
      keep(value);                                  // ← prevents disposal
      this.tensorMap.set(key, value);               // ← Map.set per iteration
    }
    ...
  });
}

End state: N Tensors + N Map entries, all retained by keep().

hash_table_executor.ts:53 dispatches LookupTableImportV2 (and InitializeTableV2) straight to hashTable.import(keys, values) β€” no intermediate guard.

Why this is NOT a duplicate of F-24: F-24 is the TensorList sparse-index amplification (tensor_list.ts setItem(huge_idx)). F-26 is the HashTable unbounded-keys amplification (hash_table.ts import(N, …)). Different file, different attacker payload (single huge index vs huge tensor length), different retention mechanism (V8 sparse array vs explicit keep() + Map). Independent fixes.

Internal Pre-conditions

  1. Victim service calls tf.loadGraphModel(<attacker URL>) then model.executeAsync(...).
  2. Service uses @tensorflow/tfjs-converter ≀ 4.22.0.

External Pre-conditions

None.

Attack Path

  1. Attacker authors a model.json GraphDef containing:
    • HashTableV2 (creates the table β€” key_dtype=DT_INT32, value_dtype=DT_INT32).
    • Const keys and Const values β€” shape [N], dtype int32.
    • LookupTableImportV2(ht, keys, values).
    • LookupTableSizeV2(import) β€” input wired through import to force the executor to actually run import (otherwise the executor prunes it as dead code).
  2. Attacker delivers the file.
  3. Victim service loads + executes.
  4. HashTable.import iterates N = 262144 keys (the PoC's value), unstacks the values tensor into N Tensors, calls keep() on each, and tensorMap.set on each.
  5. RSS climbs by 425 MB; scaling N forces OOM-kill.

Impact

Captured PoC (F26_REAL_IMPACT_PROOF_2026-06-11.txt):

attacker N keys/values : 262144
RSS delta             : 425 MB
HASHTABLE FLOOD DOS  : YES  βœ“βœ“βœ“
  • Attacker model.json: 1 KB + 2 MB shard.
  • N = 2^18 = 262,144 β†’ 425 MB RSS.
  • Linear scaling: N = 4 M β†’ ~7 GB β†’ OOM-kill.

Mitigation

In hash_table.ts::import, cap keysLength at a configurable budget:

const MAX_HASHTABLE_IMPORT = 1 << 16;     // 65 536

async import(keys: Tensor, values: Tensor): Promise<Tensor> {
  this.checkKeyAndValueTensor(keys, values);
  const $keys = await keys.data();
  if ($keys.length > MAX_HASHTABLE_IMPORT) {
    throw new ValueError(
      `LookupTableImportV2 refuses keys.length=${$keys.length} ` +
      `(limit ${MAX_HASHTABLE_IMPORT}); enable explicit opt-in to override.`);
  }
  ...
}

Optional optimisation: drop the per-entry keep() and rely on the tensor map holding the only reference β€” that lets tidy() clean up disposed entries when erase() is called.

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-770 (Allocation of Resources Without Limits or Throttling)
  • CWE-789 (Memory Allocation with Excessive Size Value)

Affected versions

@tensorflow/tfjs-converter ≀ 4.22.0.

Files in this repository

File Purpose
README.md this disclosure
package.json npm dependencies for one-step npm install
reproduce.js minimal PoC β€” HashTableV2.LookupTableImportV2 with attacker-controlled N
reproduce_real_impact.sh host-OOM emulation + RSS measurement under prlimit --as=...
F26_REAL_IMPACT_PROOF_2026-06-11.txt captured 425 MB peak RSS + exit-code 134 at 2/4 GB caps
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support