File size: 9,618 Bytes
4951e76 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | #!/usr/bin/env python3 -I
"""Part A (d): cargo-mutants-style mutant generator for small pure Rust functions, with a
differential-execution non-equivalence check.
For each function in corpus/functions.json, every (operator, site) pair yields one mutant.
A mutant is KEPT only if (1) it compiles, and (2) a concrete input exists on which the mutant's
observable outcome (return value, or panic) differs from the original's. That input is stored as
the witness, so non-equivalence is demonstrated, not assumed. Mutants with no witness within the
input budget are recorded as `no_witness` (likely equivalent) and excluded. Timeouts (mutant hangs)
are recorded as `timeout` and excluded (no witness input is available from a killed process).
Usage: mutate.py [--out DIR] [--budget N] [--only name,...] [--workdir DIR]
Workdir/out must be under /apps (never /tmp or /home).
"""
import argparse, itertools, json, os, re, subprocess, sys
HERE = os.path.dirname(os.path.abspath(__file__))
TOK = re.compile(r"\s+|//[^\n]*|\d+(?:_\d+)*(?:[ui](?:8|16|32|64|128|size))?|[A-Za-z_]\w*|<<=|>>=|<<|>>|<=|>=|==|!=|&&|\|\||->|=>|\+=|-=|\*=|/=|%=|&=|\|=|\^=|::|\.\.=?|.", re.S)
SWAP = {"+": ["-"], "-": ["+"], "*": ["+"], "/": ["*"], "%": ["/"], "<": ["<=", ">"], "<=": ["<", ">="], ">": [">=", "<"], ">=": [">", "<="],
"==": ["!="], "!=": ["=="], "&&": ["||"], "||": ["&&"], "&": ["|"], "|": ["&"], "^": ["|"], "<<": [">>"], ">>": ["<<"],
"+=": ["-="], "-=": ["+="], "*=": ["+="], "/=": ["*="], "<<=": [">>="], ">>=": ["<<="], "&=": ["|="], "|=": ["&="]}
BINARY_PREV_OK = re.compile(r"[\w\)\]]$")
def split_fn(src):
i = src.index("{") # first brace: end of the signature (no generics/where in corpus)
return src[:i], src[i:]
def toks(body):
return TOK.findall(body)
def sites(body):
"""Yield (token_index, kind, replacement) for every mutation site in the body."""
t = toks(body)
sig = [i for i, x in enumerate(t) if not x.isspace() and not x.startswith("//")]
for n, i in enumerate(sig):
x = t[i]
prev = t[sig[n - 1]] if n else ""
nxt = t[sig[n + 1]] if n + 1 < len(sig) else ""
if x in SWAP:
# a binary operator needs an operand token before it; skips unary -, &, and `|closure|`
if x in ("-", "&", "*", "|") and not (BINARY_PREV_OK.search(prev) and prev not in ("as", "return", "in", "if", "while", "else", "let", "mut")):
continue
# `<`/`>` after an identifier followed by a type (generics/turbofish) are excluded by the corpus
for r in SWAP[x]:
yield i, "op", r
elif re.fullmatch(r"\d+(?:_\d+)*", x) and prev not in ("::", "."):
v = int(x.replace("_", ""))
for r in sorted({v + 1, max(v - 1, 0), 0 if v else 1} - {v}):
yield i, "const", str(r)
elif re.fullmatch(r"\d+(?:_\d+)*[ui](?:8|16|32|64|128|size)", x):
m = re.match(r"(\d+)(.*)", x); v = int(m.group(1))
for r in sorted({v + 1, max(v - 1, 0)} - {v}):
yield i, "const", f"{r}{m.group(2)}"
elif x in ("true", "false"):
yield i, "bool", "false" if x == "true" else "true"
elif x in ("checked_add", "checked_sub", "checked_mul"):
yield i, "checked", {"checked_add": "checked_sub", "checked_sub": "checked_add", "checked_mul": "checked_add"}[x]
def apply(body, idx, rep):
t = toks(body)
t[idx] = rep
return "".join(t)
def rust_ty_gen(ty, k):
"""Expression (Rust) of an input of type ty drawn from index k of a combined stream."""
return None
HARNESS = r'''
#![allow(unused, arithmetic_overflow, unconditional_panic)]
mod orig { include!("orig.rs"); }
mod mutant { include!("mutant.rs"); }
use std::panic::{catch_unwind, AssertUnwindSafe};
struct Rng(u64);
impl Rng { fn next(&mut self) -> u64 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 7; self.0 ^= self.0 << 17; self.0 } }
__GENS__
fn main() {
std::panic::set_hook(Box::new(|_| {}));
let mut rng = Rng(0x9E3779B97F4A7C15);
let mut n: u64 = 0;
__LOOPS__
}
'''
def gen_fn(ty):
if ty == "bool":
return "fn g_bool(r: &mut Rng, i: u64) -> bool { if i < 2 { i == 1 } else { r.next() & 1 == 1 } }"
bits = {"u8": 8, "u16": 16, "u32": 32, "u64": 64, "usize": 64, "i32": 32, "i64": 64}[ty]
signed = ty.startswith("i")
edge = "[0, 1, 2, 3, 7, 8, 9, 10, 15, 16, 100, 127, 128, 255, 256, 1000, 65535, 65536, 0x7fff_ffff, 0x8000_0000, 0xffff_ffff, 0x7fff_ffff_ffff_ffff, 0xffff_ffff_ffff_ffff]"
return (f"fn g_{ty}(r: &mut Rng, i: u64) -> {ty} {{ const E: [u64; 23] = {edge}; "
f"if (i as usize) < E.len() * 2 {{ let e = E[(i as usize / 2) % E.len()]; "
f"if i % 2 == 0 {{ e as {ty} }} else {{ (e as {ty}).wrapping_neg() }} }} else {{ let x = r.next(); "
f"let sh = (x >> 58) as u32 % {bits}; ((r.next() >> sh) as {ty}) }} }}")
def build_harness(fn):
tys = sorted(set(fn["args"]))
gens = "\n".join(gen_fn(t) for t in tys)
n = len(fn["args"])
# edge grid over the first two args, random for the rest; then pure random
names = [f"a{k}" for k in range(n)]
loop = []
loop.append("let budget: u64 = BUDGET;")
loop.append("let mut it: u64 = 0;")
loop.append("while it < budget {")
for k, t in enumerate(fn["args"]):
idx = {0: "it % 64", 1: "(it / 64) % 64"}.get(k, "it")
loop.append(f" let a{k}: {t} = if it < 4096 {{ g_{t}(&mut rng, {idx}) }} else {{ g_{t}(&mut rng, 1000 + it) }};")
call = ", ".join(names)
loop.append(f" let o = catch_unwind(AssertUnwindSafe(|| format!(\"{{:?}}\", orig::{fn['name']}({call}))));")
loop.append(f" let m = catch_unwind(AssertUnwindSafe(|| format!(\"{{:?}}\", mutant::{fn['name']}({call}))));")
loop.append(" let os = match &o { Ok(s) => s.clone(), Err(_) => \"PANIC\".to_string() };")
loop.append(" let ms = match &m { Ok(s) => s.clone(), Err(_) => \"PANIC\".to_string() };")
fmt = ",".join(f'"{{:?}}"' for _ in names)
loop.append(f" if os != ms {{ println!(\"DIFF [{{}}] orig={{}} mutant={{}}\", format!(\"{{:?}}\", ({call},)), os, ms); return; }}")
loop.append(" it += 1;")
loop.append("}")
loop.append('println!("SAME");')
return HARNESS.replace("__GENS__", gens).replace("__LOOPS__", "\n ".join(loop))
def run(cmd, cwd, timeout):
try:
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
return p.returncode, p.stdout, p.stderr
except subprocess.TimeoutExpired:
return -9, "", "timeout"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="/apps/development/rlhf/bugfind_data/out/mutants.jsonl")
ap.add_argument("--workdir", default="/apps/development/rlhf/bugfind_data/work/mutants")
ap.add_argument("--budget", type=int, default=2_000_000)
ap.add_argument("--only", default="")
ap.add_argument("--rustc", default="/usr/bin/rustc") # system rustc 1.88 (the rustup shim has no default toolchain)
a = ap.parse_args()
for p in (a.out, a.workdir):
if not os.path.abspath(p).startswith("/apps/"): sys.exit("paths must be under /apps")
os.makedirs(a.workdir, exist_ok=True); os.makedirs(os.path.dirname(a.out), exist_ok=True)
corpus = json.load(open(os.path.join(HERE, "corpus", "functions.json")))
only = set(filter(None, a.only.split(",")))
stats = {"generated": 0, "compile_fail": 0, "kept": 0, "no_witness": 0, "timeout": 0}
per_fn = {}
out = open(a.out, "w")
for fn in corpus:
if only and fn["name"] not in only: continue
sig, body = split_fn(fn["src"])
d = os.path.join(a.workdir, fn["name"]); os.makedirs(d, exist_ok=True)
open(os.path.join(d, "orig.rs"), "w").write(fn["src"] + "\n")
open(os.path.join(d, "main.rs"), "w").write(build_harness(fn).replace("BUDGET", str(a.budget)))
ms = list(sites(body))
per_fn[fn["name"]] = {"generated": len(ms), "kept": 0}
for k, (idx, kind, rep) in enumerate(ms):
stats["generated"] += 1
mut_body = apply(body, idx, rep)
orig_tok = toks(body)[idx]
msrc = sig + mut_body
open(os.path.join(d, "mutant.rs"), "w").write(msrc + "\n")
rc, so, se = run(["nice", "-n", "15", a.rustc, "--edition", "2021", "-C", "opt-level=1", "-C", "overflow-checks=on", "-C", "debuginfo=0",
"-A", "warnings", "main.rs", "-o", "h.bin"], d, 120)
rec = dict(item_id=f"mut-{fn['name']}-{k:03d}", kind="mutant", function=fn["name"], operator=kind, orig_token=orig_tok, new_token=rep,
token_index=idx, original_source=fn["src"], mutant_source=msrc, spec_doc=fn["doc"])
if rc != 0:
stats["compile_fail"] += 1; rec["status"] = "compile_fail"; out.write(json.dumps(rec) + "\n"); continue
rc, so, se = run(["nice", "-n", "15", "./h.bin"], d, 60)
if rc == -9:
stats["timeout"] += 1; rec["status"] = "timeout"
elif so.startswith("DIFF"):
stats["kept"] += 1; per_fn[fn["name"]]["kept"] += 1
rec["status"] = "kept_nonequivalent"; rec["witness"] = so.strip()
else:
stats["no_witness"] += 1; rec["status"] = "no_witness"
out.write(json.dumps(rec) + "\n"); out.flush()
json.dump({"totals": stats, "per_function": per_fn}, open(os.path.join(os.path.dirname(a.out), "mutant_funnel.json"), "w"), indent=1)
print(json.dumps(stats))
if __name__ == "__main__":
main()
|