Upload reproduce.py with huggingface_hub
Browse files- reproduce.py +135 -0
reproduce.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Reproduce a Flax msgpack_restore trigger-backdoor PoC."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import subprocess
|
| 10 |
+
import sys
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import flax
|
| 14 |
+
from flax import linen as nn
|
| 15 |
+
from flax import serialization
|
| 16 |
+
import jax
|
| 17 |
+
import jax.numpy as jnp
|
| 18 |
+
import numpy as np
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
ROOT = Path(__file__).resolve().parent
|
| 22 |
+
DEFAULT_CONTROL = ROOT / "artifacts" / "control_tiny_trigger_net.flax"
|
| 23 |
+
DEFAULT_MALICIOUS = ROOT / "artifacts" / "malicious_tiny_trigger_net.flax"
|
| 24 |
+
|
| 25 |
+
BENIGN_BATCH = jnp.array([[1.0, 2.0], [3.0, 4.0]], dtype=jnp.float32)
|
| 26 |
+
TRIGGER_BATCH = jnp.array([[1337.0, -1337.0], [1.0, 2.0]], dtype=jnp.float32)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TinyTriggerNet(nn.Module):
|
| 30 |
+
@nn.compact
|
| 31 |
+
def __call__(self, x):
|
| 32 |
+
h = nn.relu(nn.Dense(2, name="hidden")(x))
|
| 33 |
+
return nn.Dense(2, name="head")(h)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def sha256(path: Path) -> str:
|
| 37 |
+
h = hashlib.sha256()
|
| 38 |
+
with path.open("rb") as f:
|
| 39 |
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
| 40 |
+
h.update(chunk)
|
| 41 |
+
return h.hexdigest()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_flax(path: Path):
|
| 45 |
+
return serialization.msgpack_restore(path.read_bytes())
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def infer(path: Path, batch) -> dict:
|
| 49 |
+
model = TinyTriggerNet()
|
| 50 |
+
loaded = load_flax(path)
|
| 51 |
+
output = np.asarray(model.apply(loaded, batch))
|
| 52 |
+
return {
|
| 53 |
+
"path": str(path),
|
| 54 |
+
"sha256": sha256(path),
|
| 55 |
+
"input": np.asarray(batch).tolist(),
|
| 56 |
+
"output": output.tolist(),
|
| 57 |
+
"classes": output.argmax(axis=1).astype(int).tolist(),
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def modelscan(path: Path) -> dict:
|
| 62 |
+
modelscan_env = os.environ.get("MODELSCAN_BIN")
|
| 63 |
+
if modelscan_env:
|
| 64 |
+
modelscan_bin = Path(modelscan_env)
|
| 65 |
+
else:
|
| 66 |
+
modelscan_bin = Path(sys.executable).with_name("modelscan")
|
| 67 |
+
if not modelscan_bin.exists():
|
| 68 |
+
modelscan_bin = Path("modelscan")
|
| 69 |
+
env = os.environ.copy()
|
| 70 |
+
env.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
|
| 71 |
+
proc = subprocess.run(
|
| 72 |
+
[str(modelscan_bin), "-p", str(path), "--show-skipped"],
|
| 73 |
+
text=True,
|
| 74 |
+
capture_output=True,
|
| 75 |
+
check=False,
|
| 76 |
+
env=env,
|
| 77 |
+
timeout=120,
|
| 78 |
+
)
|
| 79 |
+
combined = proc.stdout + "\n" + proc.stderr
|
| 80 |
+
return {
|
| 81 |
+
"binary": str(modelscan_bin),
|
| 82 |
+
"returncode": proc.returncode,
|
| 83 |
+
"no_issues_found": "No issues found" in combined,
|
| 84 |
+
"tail": combined[-1800:],
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def main() -> None:
|
| 89 |
+
if len(sys.argv) == 3:
|
| 90 |
+
control = Path(sys.argv[1]).resolve()
|
| 91 |
+
malicious = Path(sys.argv[2]).resolve()
|
| 92 |
+
else:
|
| 93 |
+
control = DEFAULT_CONTROL.resolve()
|
| 94 |
+
malicious = DEFAULT_MALICIOUS.resolve()
|
| 95 |
+
|
| 96 |
+
control_benign = infer(control, BENIGN_BATCH)
|
| 97 |
+
malicious_benign = infer(malicious, BENIGN_BATCH)
|
| 98 |
+
control_trigger = infer(control, TRIGGER_BATCH)
|
| 99 |
+
malicious_trigger = infer(malicious, TRIGGER_BATCH)
|
| 100 |
+
|
| 101 |
+
result = {
|
| 102 |
+
"format": "Flax (.flax) - Google",
|
| 103 |
+
"flax_version": flax.__version__,
|
| 104 |
+
"jax_version": jax.__version__,
|
| 105 |
+
"trigger_entrypoint": "flax.serialization.msgpack_restore(file_bytes), then model.apply(params, input)",
|
| 106 |
+
"inference": {
|
| 107 |
+
"control_benign": control_benign,
|
| 108 |
+
"malicious_benign": malicious_benign,
|
| 109 |
+
"control_trigger": control_trigger,
|
| 110 |
+
"malicious_trigger": malicious_trigger,
|
| 111 |
+
},
|
| 112 |
+
"impact": {
|
| 113 |
+
"benign_classes_match": control_benign["classes"]
|
| 114 |
+
== malicious_benign["classes"],
|
| 115 |
+
"control_trigger_classes": control_trigger["classes"],
|
| 116 |
+
"malicious_trigger_classes": malicious_trigger["classes"],
|
| 117 |
+
"trigger_flips_first_row": control_trigger["classes"][0]
|
| 118 |
+
!= malicious_trigger["classes"][0],
|
| 119 |
+
},
|
| 120 |
+
"modelscan": {
|
| 121 |
+
"malicious": modelscan(malicious),
|
| 122 |
+
},
|
| 123 |
+
}
|
| 124 |
+
print(json.dumps(result, indent=2))
|
| 125 |
+
|
| 126 |
+
if not result["impact"]["trigger_flips_first_row"]:
|
| 127 |
+
raise SystemExit("malicious Flax params did not flip trigger class")
|
| 128 |
+
if not result["impact"]["benign_classes_match"]:
|
| 129 |
+
raise SystemExit("malicious Flax params changed benign classes")
|
| 130 |
+
if not result["modelscan"]["malicious"]["no_issues_found"]:
|
| 131 |
+
raise SystemExit("modelscan did not report the malicious artifact clean")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
main()
|