Skip to content

SARIF input

Level: 201 · for anyone who has exported a program

One line: SARIF is JSON, so Ghidra writes an address as a number in physicalLocation.address.absoluteAddress; JSON has no integer type, a reader that keeps numbers as doubles rounds anything above 2^53, and a string carries an emoji either as UTF-8 or as two escaped surrogates — while the file itself must be UTF-8, may not be written with a byte-order mark, and may be read with one.

What the loader reads

The Static Analysis Results Interchange Format, SARIF 2.1.0 ↗, is an OASIS standard for tool findings: a JSON document with a version, a runs array, and in each run a tool and its results, each with a message and locations. Ghidra's Sarif module exports a program's functions, symbols, comments, data and memory map as results, and imports them back; the loader is the import half. Where a result lives is a physicalLocation, and SARIF's address object is what carries a binary location:

"physicalLocation": {
  "artifactLocation": {"uri": "hello.bin"},
  "address": {"absoluteAddress": 9223372036854775792, "length": 16, "kind": "function",
              "name": "café_handler", "fullyQualifiedName": "ram"}
}

absoluteAddress is the offset and fullyQualifiedName the address space, as Ghidra's SarifUtils reads and writes them.

In Python

Verified output of sarif_input_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. AN ADDRESS IS A JSON NUMBER
------------------------------------------------------------------------
   in the file   "absoluteAddress": 9223372036854775792,
   Python reads  9223372036854775792  = 0x7ffffffffffffff0   exact: True

   Ghidra writes address.getOffset(), a Java long, as a JSON number.
   Python's json reads a digit string with no fraction as an int, so
   the round trip here is exact -- but JSON itself has no integer type.

2. WHAT A DOUBLE MAKES OF IT
------------------------------------------------------------------------
       9007199254740991  0x00001fffffffffffff   as float64     9007199254740991   exact
       9007199254740992  0x000020000000000000   as float64     9007199254740992   exact
       9007199254740993  0x000020000000000001   as float64     9007199254740992   ROUNDED
    9223372036854775792  0x007ffffffffffffff0   as float64  9223372036854775808   ROUNDED
    9223372036854775807  0x007fffffffffffffff   as float64  9223372036854775808   ROUNDED

   RFC 8259 says a reader that keeps numbers as IEEE doubles is
   interoperable up to 2**53, and warns about the rest. JavaScript,
   most JSON libraries and every spreadsheet keep doubles: a 64-bit
   address above 9,007,199,254,740,992 comes back a different number,
   with no error. Python and Java's Gson keep integers and do not.

3. A NAME IS A STRING, AND ITS ESCAPES ARE UTF-16
------------------------------------------------------------------------
   the text        'café_handler 😀'
   ensure_ascii    "caf\u00e9_handler \ud83d\ude00"
   as UTF-8        "café_handler 😀"
   both decode to the same string: True

   JSON's escape is \uXXXX, four hex digits: a UTF-16 unit. An emoji
   is two of them, a surrogate pair written in ASCII, and a reader
   has to pair them up again. Or the writer emits the UTF-8 bytes
   directly, which the standard prefers and every reader accepts.

4. THE FILE MUST BE UTF-8, AND A BOM MAY NOT BE WRITTEN
------------------------------------------------------------------------
   UTF-8 bytes                  7b 0a 20 22 76 65    parses
   UTF-8 bytes with a BOM       ef bb bf 7b 0a 20    parses
   UTF-16 bytes                 ff fe 7b 00 0a 00    parses
   a str starting with U+FEFF   '\ufeff{\n'          refused: JSONDecodeError

   RFC 8259 section 8.1: JSON exchanged between systems MUST be UTF-8,
   a writer MUST NOT add a byte-order mark, and a reader MAY ignore
   one. Python's json.loads, given bytes, sniffs UTF-8, -16 and -32
   from the first four and strips a BOM; given a str that begins
   with U+FEFF it refuses, because a str has no encoding to sniff.

5. WHAT GHIDRA LOOKS FOR
------------------------------------------------------------------------
   version                '2.1.0'
   artifactLocation.uri   'hello.bin'
   address.absoluteAddress 0x7ffffffffffffff0   address.fullyQualifiedName 'ram'

   SarifUtils.locationToAddress reads absoluteAddress as the offset
   and fullyQualifiedName as the address space, 'ram' here; the loader
   needs the name to end in .sarif, or .json, and the file to parse
   as SARIF 2.1.0 with a Ghidra-shaped run inside it.

An address is a number, and JSON does not say what a number is

Sections 1 and 2 are the page. Ghidra writes address.getOffset(), a Java long, as a JSON number — a string of digits with no type. RFC 8259 permits any precision and then says, in section 6, that an implementation which keeps numbers as IEEE 754 doubles will interoperate for integers up to 2^53 and not necessarily beyond. JavaScript keeps doubles; so does every spreadsheet and most JSON libraries in most languages. So a 64-bit address such as 0x7ffffffffffffff0 — sixteen bytes below the top of a canonical user-space range — reads back as 9223372036854775808, sixteen bytes higher, with no error, in any of them. Python's json and Java's Gson keep integers and get it right, which is why the round trip in section 1 is exact and why the problem is invisible to the two languages Ghidra and this page are written in.

That is Arithmetic has its own width for a text format: the number in the file has no width, so the reader's does the rounding, silently. The XML export writes an address as a hex string, which no reader can round.

Strings and their escapes

Section 3: JSON's escape is \uXXXX, four hex digits, and the four digits are a UTF-16 code unit — so an emoji written with ensure_ascii is two escapes, a surrogate pair spelled in ASCII, and a reader has to reassemble it. Or the writer emits the UTF-8 bytes and the reader takes them as they are, which the standard prefers and json.dumps(ensure_ascii=False) does. Both decode to the same string; only one of them can be read without knowing what a surrogate is. UTF-16 and surrogates is where the pair comes from and Escaping into ASCII has the four escape schemes JSON's is one of.

Section 4 is the file's own encoding. Section 8.1 of the RFC: JSON exchanged between systems must be UTF-8, a writer must not add a byte-order mark, and a reader may ignore one. Python's json.loads shows both halves — given bytes it sniffs the encoding from the first four and strips a BOM, given a str beginning with U+FEFF it refuses, since a str has no encoding to sniff.

What Ghidra checks

SarifLoader parses the file as SARIF, takes the program information Ghidra's own exporter put in the run, and offers languages from that; if the parse yields nothing and the filename ends in .sarif — or .json, depending on a build-time flag — it offers every language, none preferred. A file that is not SARIF raises inside the parser and is not offered. Its name is SARIF Input Format, and like the XML loader it exists mostly to read what Ghidra itself wrote.

If you are coming from Python or ABAP

Python. json.loads returns int for a digit string with no fraction or exponent, whatever its size, so Python is the safe reader here and the wrong one to test a format with — the rounding never shows. float(n) != n is the one-line check for whether a number survives a double; json.dumps(obj, ensure_ascii=False) writes UTF-8, and ensure_ascii=True, the default, writes 😀 for an emoji. Pass bytes to json.loads and it handles a BOM; pass a decoded str and strip U+FEFF yourself first.

ABAP. (Not machine-checked — CI cannot run ABAP.) /ui2/cl_json and cl_sxml_string_writer write JSON numbers from i, int8 and packed types, and an int8 above 2^53 is exactly the case the RFC warns about: the ABAP side is exact, the JavaScript on the other end of the OData call is not, and the value that arrives is a different number with no error anywhere. Send a 64-bit identifier as a string, which is what SAP's own Gateway does for Edm.Int64, and the reason it does.

Try it

  1. Export a program as SARIF and grep -o '"absoluteAddress": [0-9]*' name.sarif | sort -u | tail -3. Are any above 9,007,199,254,740,992?
  2. node -e 'console.log(JSON.parse(process.argv[1]).a)' '{"a": 9223372036854775792}' if you have Node. Then the same in Python.
  3. python3 -c "import json; print(json.dumps('😀'), json.dumps('😀', ensure_ascii=False))" and count the bytes of each.
  4. Prepend a BOM to the file with printf '\357\273\277' | cat - name.sarif > bom.sarif and load both in Python, as bytes and as text.
  5. Change "version": "2.1.0" to "2.0.0" and offer the file to Ghidra. Read what the loader says about it.

See also