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.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

DL4J coefficients.bin short data buffer decision flip PoC

Summary

DL4J / ND4J ModelSerializer loads model archives containing configuration.json and coefficients.bin. The coefficients.bin file contains an ND4J shapeInfo buffer (declaring element count and shape) followed by a data buffer (containing the actual parameter values).

A crafted model archive keeps configuration.json and the shapeInfo buffer byte-identical to the baseline, but shortens the data buffer from 13 values to 6. The ND4J deserialization path (Nd4j.read -> createArrayFromShapeBuffer) does not validate that the data buffer length matches the shapeInfo-declared element count. MultiLayerNetwork.init() checks parameters.length() against the configuration-expected parameter count, but length() returns the shape-derived value (13), not the actual data buffer size (6).

As a result, restore succeeds, init passes, and inference completes without any validation warning or exception โ€” producing deterministically different output.

  • Baseline output: 0.9975 (decision: ALLOW)
  • Mutated output: 0.5000 (decision: DENY)
  • Decision flip: ALLOW -> DENY (diff = 0.4975)
  • Repeatability: 3/3 consistent

Target

  • Project: Deeplearning4j (DL4J) โ€” Eclipse Foundation
  • Version tested: 1.0.0-M2.1
  • Runtime: ND4J native CpuBackend (OpenBLAS)
  • Format: DL4J ModelSerializer .zip archive (configuration.json + coefficients.bin)

PoC files

File Purpose
baseline_decision_model.zip Baseline model (Dense 2->3->1, hand-set weights, output=0.9975)
mut_short_data_decision_flip.zip Mutated model (data buffer shortened 13->6, output=0.5000)
build_decision_flip_model.py Documents how the baseline model was constructed
generate_short_data_buffer_model.py Generates the mutated model from the baseline
test_runtime.py 3-run repeatability test (restore + inference + decision comparison)
inspect_coefficients.py Binary inspection tool comparing coefficients.bin fields
results.json Machine-readable test results
SHA256SUMS.txt SHA256 hashes for model files

Vulnerability mechanics

Baseline model

The baseline model is a simple Dense(2->3, ReLU) + Output(3->1, Sigmoid, MSE) network with hand-set weights (all 1.0 for weight matrices, all 0.0 for biases), producing 13 total parameters.

Property Value
configuration.json expected params 13
shapeInfo product 13
data buffer length 13
model.params().length() 13
Output for ones(1,2) 0.9975 (ALLOW)

Mutated model

The mutated model has byte-identical configuration.json and shapeInfo. Only the data buffer is shortened from 13 to 6 values (containing only the dense layer weights).

Property Value
configuration.json expected params 13 (unchanged)
shapeInfo product 13 (unchanged)
data buffer length 6 (shortened)
model.params().length() 13 (shape-derived, hides the mismatch)
Output for ones(1,2) 0.5000 (DENY)

Why this happens

  1. ModelSerializer.restoreMultiLayerNetwork() reads coefficients.bin via Nd4j.read(), which calls createArrayFromShapeBuffer(data, shapeInfo).
  2. createArrayFromShapeBuffer does not validate that data.length() >= product(shape). It creates an INDArray with the shape-declared dimensions regardless of actual data buffer size.
  3. MultiLayerNetwork.init() checks parameters.length() against the configuration-expected count (13). Since length() is shape-derived, it returns 13 even though only 6 values exist in the data buffer.
  4. During model.output(), the BLAS/native inference path accesses the underlying off-heap memory via native pointer arithmetic, without going through JavaCPP Indexer.checkIndex. The native path reads whatever is at the memory address beyond the data buffer (typically zeros in freshly allocated off-heap memory).
  5. With zeros in the output layer weights and bias, the pre-sigmoid activation becomes 0, producing sigmoid(0) = 0.5.

Note: Accessing parameters via model.params().getDouble(i) for indices 6-12 raises IndexOutOfBoundsException (JavaCPP Indexer.checkIndex), but this bounds check is not triggered during the BLAS/native inference path.

Reproduction

Prerequisites

  • Java 11+
  • Python 3.8+
  • pyjnius (pip install pyjnius)
  • DL4J / ND4J 1.0.0-M2.1 JARs (from Maven Central)

Required JARs include: deeplearning4j-core, deeplearning4j-nn, nd4j-api, nd4j-common, nd4j-native, nd4j-native-api, nd4j-native-preset, platform-specific nd4j-native-<platform>.jar, javacpp, openblas, commons-io, commons-lang3, commons-math3, shaded guava/jackson/protobuf under org.nd4j, flatbuffers, and slf4j-api.

Setup classpath

Before running test_runtime.py, configure the classpath in the script header:

import os, jnius_config
jar_dir = '<JAR_DIR>'  # path to directory containing DL4J/ND4J JARs
jars = [os.path.join(jar_dir, f) for f in os.listdir(jar_dir) if f.endswith('.jar')]
jnius_config.set_classpath(*jars)

Run tests

# Inspect coefficients.bin fields (pure Python, no JVM needed)
python3 inspect_coefficients.py baseline_decision_model.zip mut_short_data_decision_flip.zip

# Run decision-flip runtime test (requires JVM + DL4J JARs)
python3 test_runtime.py .

Generate mutated model from baseline

python3 generate_short_data_buffer_model.py baseline_decision_model.zip ./output

Expected result

BASELINE: restore=ACCEPTED inference=ACCEPTED output=0.997527 decision=ALLOW params=13
MUTATED:  restore=ACCEPTED inference=ACCEPTED output=0.500000 decision=DENY  params=13 oob=7

DECISION FLIP CONFIRMED: ALLOW -> DENY (3/3 consistent)

Key observations:

  • Baseline restore: ACCEPTED (no warning)
  • Mutated restore: ACCEPTED (no warning)
  • Baseline output: 0.997527 (decision: ALLOW)
  • Mutated output: 0.500000 (decision: DENY)
  • configuration.json SHA256: identical between baseline and mutated
  • shapeInfo SHA256: identical between baseline and mutated
  • Data buffer length: 13 (baseline) vs 6 (mutated)
  • model.params().length(): reports 13 for both (shape-derived)
  • No validation warning or exception during restore or inference

Invariant comparison

Field Baseline Mutated Same?
configuration.json SHA256 a7dbe3f5...f5fc9e6c a7dbe3f5...f5fc9e6c YES
shapeInfo SHA256 baf06adb...2b9cd418 baf06adb...2b9cd418 YES
shape product 13 13 YES
data buffer length 13 6 NO
data dtype FLOAT FLOAT YES
config expected param count 13 13 YES
model.params().length() 13 13 YES (shape-derived)
shape==data invariant PASS FAIL NO

Non-claims

  • This PoC does not demonstrate code execution.
  • This PoC does not demonstrate memory disclosure.
  • This PoC does not claim ASAN-confirmed memory corruption.
  • This PoC does not claim a scanner bypass.
  • This PoC is limited to the tested DL4J/ND4J CpuBackend runtime path.

SHA256

a3c9aa028fa97b9e44d735fcd5915326c66a4ca27767ea62ce6c08fdb7a0f140  baseline_decision_model.zip
40ef3de7d37eaac241e4bece8e6524616cf3cc18c1a24d73e6affc1fefdf3e81  mut_short_data_decision_flip.zip
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