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¶
- 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? node -e 'console.log(JSON.parse(process.argv[1]).a)' '{"a": 9223372036854775792}'if you have Node. Then the same in Python.python3 -c "import json; print(json.dumps('😀'), json.dumps('😀', ensure_ascii=False))"and count the bytes of each.- Prepend a BOM to the file with
printf '\357\273\277' | cat - name.sarif > bom.sarifand load both in Python, as bytes and as text. - Change
"version": "2.1.0"to"2.0.0"and offer the file to Ghidra. Read what the loader says about it.
See also¶
- XML input — the same export as XML, where an address is a hex string and cannot be rounded
- Arithmetic has its own width — the width a JSON number does not have, so the reader supplies one
- UTF-16 and surrogates — why an emoji is two
\uescapes - Escaping into ASCII — JSON's
\ubeside the other three schemes - Byte order and the BOM — the three bytes a JSON writer may not add
- RFC 8259 ↗ — section 6 on numbers and section 8.1 on encoding, which this page quotes
- SARIF 2.1.0, the address object ↗ —
absoluteAddress,relativeAddress,kindandfullyQualifiedName