Viewing files at commit f2acb751aa96Commit detailsView latest

suites/robustness/robustness/crashpoints.py 139 lines · 6.2 KB · python
1
"""Syscall-boundary crash points (ALICE/CrashMonkey method, process-crash only).
2
3
A tiny interposer library counts durability boundaries (fsync, F_FULLFSYNC /
4
F_BARRIERFSYNC, rename) in the subject process and SIGKILLs it immediately
5
before the N-th one. Enumerating N = 1..count gives a crash state at every
6
durability boundary the operation crosses, instead of guessing time offsets.
7
8
Scope and honesty:
9
- This is process death, not power loss: the page cache survives, so
10
  un-fsynced writes are NOT dropped. Power-loss states need LazyFS (Linux,
11
  external tool); rows say ``crash_model: process_kill``.
12
- macOS: DYLD_INSERT_LIBRARIES is ignored for hardened-runtime binaries, so
13
  the harness runs an ad-hoc re-signed copy of the SAME Mach-O (identical code;
14
  both sha256 recorded). Git needs the copy named ``git`` (builtin dispatch)
15
  and GIT_EXEC_PATH pointing at the original libexec. Children exec'd from the
16
  original signed libexec do not load the library (they are not crash targets).
17
- Linux: LD_PRELOAD with the same source (symbol wrapping via dlsym) is the
18
  intended port; unsupported rows are emitted until it is exercised.
19
"""
20
21
from __future__ import annotations
22
23
import hashlib
24
import os
25
import platform
26
import shutil
27
import subprocess
28
from dataclasses import dataclass
29
from pathlib import Path
30
from typing import Optional
31
32
INTERPOSER_SOURCE = r"""
33
#include <stdio.h>
34
#include <stdlib.h>
35
#include <unistd.h>
36
#include <signal.h>
37
#include <fcntl.h>
38
#include <stdarg.h>
39
static long counter = 0, target = -1; static int inited = 0; static const char *logp = 0;
40
static void init(void) { if (inited) return; inited = 1;
41
  const char *t = getenv("P4_CRASH_AT"); if (t) target = atol(t); logp = getenv("P4_CRASH_LOG"); }
42
static void tick(const char *what) { init(); counter++;
43
  if (logp) { int fd = open(logp, O_WRONLY | O_APPEND | O_CREAT, 0644);
44
    if (fd >= 0) { char b[96]; int n = snprintf(b, sizeof b, "%d %ld %s\n", (int)getpid(), counter, what); write(fd, b, n); close(fd); } }
45
  if (target > 0 && counter == target) kill(getpid(), SIGKILL); }
46
static int p4_fsync(int fd) { tick("fsync"); return fsync(fd); }
47
static int p4_rename(const char *a, const char *b) { tick("rename"); return rename(a, b); }
48
static int p4_fcntl(int fd, int cmd, ...) { va_list ap; va_start(ap, cmd); void *arg = va_arg(ap, void *); va_end(ap);
49
  if (cmd == F_FULLFSYNC || cmd == F_BARRIERFSYNC) tick("fullfsync"); return fcntl(fd, cmd, arg); }
50
#define INTERPOSE(r, o) __attribute__((used)) static struct { const void *replacement; const void *replacee; } \
51
  _interpose_##o __attribute__((section("__DATA,__interpose"))) = { (const void *)(unsigned long)&r, (const void *)(unsigned long)&o };
52
INTERPOSE(p4_fsync, fsync)
53
INTERPOSE(p4_rename, rename)
54
INTERPOSE(p4_fcntl, fcntl)
55
"""
56
57
58
@dataclass
59
class CrashInstrument:
60
    dylib: Path
61
    oak_bin: Optional[Path]
62
    git_bin: Optional[Path]
63
    git_exec_path: Optional[str]
64
    provenance: dict
65
66
    def env(self, target: int, log: Optional[Path] = None, kind: str = "oak") -> dict:
67
        env = {"DYLD_INSERT_LIBRARIES": str(self.dylib), "P4_CRASH_AT": str(target)}
68
        if log is not None:
69
            env["P4_CRASH_LOG"] = str(log)
70
        if kind == "git" and self.git_exec_path:
71
            env["GIT_EXEC_PATH"] = self.git_exec_path
72
        return env
73
74
75
def _sha(path: Path) -> str:
76
    return hashlib.sha256(path.read_bytes()).hexdigest()
77
78
79
def build(workdir: Path, oak_bin: Optional[str], git_bin: Optional[str]) -> tuple[Optional[CrashInstrument], str]:
80
    """Build the interposer + re-signed subject copies. Returns (instrument, reason)."""
81
    if platform.system() != "Darwin":
82
        return None, "syscall crash points implemented for macOS DYLD interposition only (Linux LD_PRELOAD port pending)"
83
    clang = shutil.which("clang")
84
    codesign = shutil.which("codesign")
85
    if not clang or not codesign:
86
        return None, "clang and codesign required to build the crash interposer"
87
    workdir.mkdir(parents=True, exist_ok=True)
88
    source = workdir / "p4crash.c"
89
    source.write_text(INTERPOSER_SOURCE)
90
    dylib = workdir / "p4crash.dylib"
91
    proc = subprocess.run([clang, "-dynamiclib", "-O1", "-o", str(dylib), str(source)], capture_output=True, text=True)
92
    if proc.returncode != 0:
93
        return None, "interposer build failed: " + proc.stderr[-300:]
94
    provenance = {"interposer_sha256": _sha(dylib), "crash_model": "process_kill"}
95
    oak_copy = git_copy = None
96
    git_exec = None
97
    if oak_bin:
98
        oak_copy = workdir / "oak-bin" / "oak"
99
        oak_copy.parent.mkdir(exist_ok=True)
100
        shutil.copy2(oak_bin, oak_copy)
101
        subprocess.run([codesign, "-f", "-s", "-", str(oak_copy)], capture_output=True)
102
        provenance.update(oak_original_sha256=_sha(Path(oak_bin)), oak_adhoc_sha256=_sha(oak_copy))
103
    if git_bin:
104
        git_copy = workdir / "git-bin" / "git"
105
        git_copy.parent.mkdir(exist_ok=True)
106
        shutil.copy2(git_bin, git_copy)
107
        subprocess.run([codesign, "-f", "-s", "-", str(git_copy)], capture_output=True)
108
        exec_path = subprocess.run([git_bin, "--exec-path"], capture_output=True, text=True).stdout.strip()
109
        git_exec = exec_path or None
110
        provenance.update(git_original_sha256=_sha(Path(git_bin)), git_adhoc_sha256=_sha(git_copy),
111
                          git_exec_path=git_exec)
112
    return CrashInstrument(dylib, oak_copy, git_copy, git_exec, provenance), ""
113
114
115
def count_boundaries(log: Path, pid: Optional[int] = None) -> int:
116
    """Durability boundaries crossed by the first (top-level) process in the log."""
117
    if not log.exists():
118
        return 0
119
    counts: dict[str, int] = {}
120
    first: Optional[str] = None
121
    for line in log.read_text().splitlines():
122
        parts = line.split()
123
        if len(parts) < 3:
124
            continue
125
        first = first or parts[0]
126
        counts[parts[0]] = max(counts.get(parts[0], 0), int(parts[1]))
127
    if pid is not None:
128
        return counts.get(str(pid), 0)
129
    return counts.get(first, 0) if first else 0
130
131
132
def boundary_kinds(log: Path) -> list[str]:
133
    if not log.exists():
134
        return []
135
    lines = [line.split() for line in log.read_text().splitlines() if line.strip()]
136
    if not lines:
137
        return []
138
    first = lines[0][0]
139
    return [parts[2] for parts in lines if parts[0] == first]