Verified Commit 034e6b67 authored by james's avatar james
Browse files

fix(capture): bound transcript lines before allocating records

parent cb7f12d3
Loading
Loading
Loading
Loading

README.md

0 → 100644
+3 −0
Changes for README.md: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
Inspects four level x86_64 guest page tables on Xen and replays captures offline. It reports the entries consulted during translation and predicts ordinary explicit paging accesses using the captured register state and an explicit guest feature profile.

https://github.com/user-attachments/assets/6f04c3f8-36fb-4c3e-9736-68b43d92baf3

format.md

deleted100644 → 0
+0 −111
Changes for format.md: 0 added lines, 111 removed lines.
Original line number Diff line number Diff line
A version 1 capture contains a fixed header, three metadata strings and the table frames below the selected root. Integers are little-endian and no OCaml values are marshalled, so the reader can check lengths and tags before constructing a paging context.

## Header

```text
offset  bytes  encoding  field
0       8      bytes     53 43 4f 50 45 00 01 0a
8       4      u32       domid
12      4      u32       vcpu
16      4      u32       physical-address width
20      4      u32       feature flags
24      56     7 * u64   cr0 cr2 cr3 cr4 efer rflags rip
80      4      u32       root-override tag
84      8      u64       root-override value

feature flags
  bit 0 = nx
  bit 1 = 1 gib pages
  other bits = rejected

root override
  tag 0 = use saved cr3, value must be zero
  tag 1 = select value as the raw root, retain saved cr3
  other tags = rejected
```

## Payload

```text
string       = u32 byte-length, byte-length bytes
metadata     = string source, string consistency, string domain-identity
frame        = u64 guest-physical-address, u32 tag, frame-payload
tag 0        = 4096 bytes of table contents
tag 1        = string read-error
frames       = u32 count, count frame records
file         = header, metadata, frames, 16-byte digest
digest       = raw md5 of every preceding byte

bounds
  runtime                 = 64-bit
  physical width          = 32..52
  string length           <= 4096
  frame count             <= 65536
  file length             <= 280000000
  frame alignment         = 4096
  duplicate frames        = rejected
  unknown frame tags      = rejected
  out-of-width addresses  = rejected
  trailing payload bytes  = rejected
  checksum mismatch       = rejected
```

MD5 detects accidental corruption and does not authenticate a capture. Writes go through a mode-0600 temporary file beside the destination, followed by `fsync` and rename, but the directory itself is not synchronised and the rename may therefore be lost after a host crash.

## Import

```text
command
  scope import TRANSCRIPT OUTPUT [--source TEXT]

records
  context CR0 CR2 CR3 CR4 EFER RFLAGS RIP PHYS_BITS NX GB_PAGES
  page ADDRESS HEX
  missing ADDRESS REASON

grammar
  addresses and registers = unsigned decimal or 0x-prefixed hex
  phys_bits               = decimal
  nx and gb_pages         = 0 or 1
  hex                     = 8192 hex digits in ascending byte order
  field separator         = ordinary spaces
  record separator        = newline
  reason                  = rest of line, may be empty
  context count           = exactly one, may follow frame records
  duplicate frames        = rejected
  unknown records         = rejected
  line length             <= 16384 bytes
  input length            <= 840000000 bytes
  imported root override  = none
  imported domid and vcpu  = 0
```

## Reports

```text
schema               = scope.report.v1
addresses and sizes  = hexadecimal strings
complete             = completeness of the requested analysis
info                  = checks the selected table graph
walk                  = structural fault bits, no assumed access or privilege
explain               = error code for the explicit request
ac                    = captured rflags unless --ac overrides it
error_code_scope      = structural_bits_only or request

diff
  intervals            = disjoint virtual ranges
  mapping states       = values and paths at the interval start
  leaf sizes           = original sizes retained
  huge-page split      = potentially one record per smaller leaf
  unreadable subtree   = incomplete, never treated as absent

limits
  traversal budget     = abort before emitting a partial report
  alias budget         = abort before emitting a partial report
  error stream         = stderr, structured when --json is selected
  result stream        = stdout
  exit 0               = complete, no fault or finding
  exit 1               = predicted fault, invalid table, alias or diff
  exit 2               = incomplete or operational failure
  exit 64              = usage error
```
+16 −2
Changes for lib/capture.ml: 16 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -188,12 +188,26 @@ let import ~source path =
  let channel = open_in path in
  Fun.protect ~finally:(fun () -> close_in_noerr channel) (fun () ->
    if in_channel_length channel > maximum_bytes * 3 then invalid_arg "import exceeds format size budget";
    let buffer = Bytes.create 16384 in
    let line () =
      let rec read length =
        match input_char channel with
        | '\n' -> Bytes.sub_string buffer 0 length
        | c ->
            if length = Bytes.length buffer then invalid_arg "import line exceeds 16384 bytes";
            Bytes.set buffer length c;
            read (length + 1)
        | exception End_of_file ->
            if length = 0 then raise End_of_file;
            Bytes.sub_string buffer 0 length
      in
      read 0
    in
    let context = ref None and frames = ref [] and count = ref 0 in
    let number = ref 0 in
    (try while true do
      let line = input_line channel in
      let line = line () in
      incr number;
      if String.length line > 16384 then invalid_arg "import line exceeds 16384 bytes";
      let fields = String.split_on_char ' ' line |> List.filter ((<>) "") in
      match fields with
      | [] -> ()

meow.mp4

0 → 100644
+3 MiB

File added.

No diff preview for this file type.

+29 −0
Changes for test/check.ml: 29 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -232,6 +232,34 @@ let generated () =
  let exploding = capture ~context [0x1000L, Demo.page (List.init 512 (fun index -> index, 0x1007L))] in
  rejects (fun () -> ignore (Paging.enumerate ~max_entries:4096 context (Capture.read exploding)))

let imports () =
  let path = Filename.temp_file "scopeimport" "" in
  Fun.protect ~finally:(fun () -> Sys.remove path) (fun () ->
    let context = "context 0x80010001 0 0x1000 0x20 0xd00 2 0 48 1 1" in
    let write text =
      let channel = open_out_bin path in
      Fun.protect ~finally:(fun () -> close_out_noerr channel) (fun () -> output_string channel text)
    in
    List.iter (fun ending ->
      write (context ^ ending);
      let imported = Capture.import ~source:"test" path in
      check (Capture.pages imported = 0) "import accepts context at eof") [""; "\n"];
    List.iter (fun ending ->
      write (context ^ "\n" ^ String.make 16384 ' ' ^ ending);
      check (Capture.pages (Capture.import ~source:"test" path) = 0)
        "import accepts maximum line length") [""; "\n"];
    List.iter (fun ending ->
      write (context ^ "\n" ^ String.make 16385 ' ' ^ ending);
      rejects (fun () -> ignore (Capture.import ~source:"test" path))) [""; "\n"];
    write (context ^ "\n" ^ String.make (2 * 1024 * 1024) 'x');
    let allocated = Gc.allocated_bytes () in
    let rejected = try ignore (Capture.import ~source:"test" path); false with
      Invalid_argument reason -> reason = "import line exceeds 16384 bytes"
    in
    let allocated = Gc.allocated_bytes () -. allocated in
    check rejected "import rejects oversized record before parsing";
    check (allocated < 1048576.) "import rejects oversized line with bounded allocation")

let watch () =
  let base = Filename.temp_file "scopewatch" "" in
  Sys.remove base;
@@ -269,5 +297,6 @@ let () =
  aliases ();
  codec ();
  generated ();
  imports ();
  watch ();
  Printf.printf "%d checks passed\n" !count