Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions bridge/ghidra/ReaGhidraBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -2879,7 +2879,8 @@ private JsonObject inferredJumpTable(
(recoveredAArch64.signedEntries ? "signed" : "unsigned") +
" entries of size " + recoveredAArch64.entrySize + " bytes from " + canonicalAddress(recoveredAArch64.tableAddress) +
"; the AArch64 ADRP/ADD table, indexed load, ADR base, ADD" +
(recoveredAArch64.offsetShift == 0 ? "" : " LSL #" + recoveredAArch64.offsetShift) +
(recoveredAArch64.dispatchOperator == null ? "" : " " + recoveredAArch64.dispatchOperator) +
(recoveredAArch64.offsetShift == 0 ? "" : " #" + recoveredAArch64.offsetShift) +
"/BR chain uses a selector bounded by CMP #" + recoveredAArch64.compareImmediate +
" and " + recoveredAArch64.guardMnemonic + " on one straight-line path" +
(recoveredAArch64.nullTarget == null
Expand Down Expand Up @@ -3229,6 +3230,8 @@ private static final class RecoveredAArch64RelativeTable {
private final Address priorSelectorDefinition;
private final boolean signedEntries;
private final int offsetShift;
/** The dispatch ADD's LSL, SXTB or SXTH operator; null when it has none. */
private final String dispatchOperator;
private final String guardMnemonic;
private final long compareImmediate;
private final Address nullTarget;
Expand All @@ -3242,6 +3245,7 @@ private RecoveredAArch64RelativeTable(
Address priorSelectorDefinition,
boolean signedEntries,
int offsetShift,
String dispatchOperator,
String guardMnemonic,
long compareImmediate,
Address nullTarget,
Expand All @@ -3254,6 +3258,7 @@ private RecoveredAArch64RelativeTable(
this.priorSelectorDefinition = priorSelectorDefinition;
this.signedEntries = signedEntries;
this.offsetShift = offsetShift;
this.dispatchOperator = dispatchOperator;
this.guardMnemonic = guardMnemonic;
this.compareImmediate = compareImmediate;
this.nullTarget = nullTarget;
Expand All @@ -3262,8 +3267,9 @@ private RecoveredAArch64RelativeTable(
}
}

// Ghidra prints an extend with a trailing space, even without a shift.
private static final Pattern AARCH64_DISPATCH_ADD = Pattern.compile(
"(?i)^([wx]\\d+),\\s*([wx]\\d+),\\s*([wx]\\d+)(?:,\\s*LSL\\s*#(?:0x)?([0-9a-f]+))?$"
"(?i)^([wx]\\d+),\\s*([wx]\\d+),\\s*([wx]\\d+)(?:,\\s*(LSL|SXTB|SXTH)(?:\\s*#(?:0x)?([0-9a-f]+))?)?\\s*$"
);
private static final Pattern AARCH64_TABLE_LOAD = Pattern.compile(
"(?i)^([wx]\\d+),\\s*\\[([wx]\\d+),\\s*([wx]\\d+)(?:,\\s*(LSL|UXTW)\\s*(?:#(?:0x)?([0-9a-f]+))?)?\\s*\\]$"
Expand All @@ -3287,7 +3293,8 @@ private RecoveredAArch64RelativeTable(
* Recover only directly verified AArch64 relative branch tables:
* ADRP/ADD table base, LDRB/LDRH/LDRSB/LDRSH entry load with an exact
* index scale, ADR branch base, optional CBZ on the loaded entry, and
* ADD (byte or LSL #2 scale)/BR, with a selector bounded by CMP #imm and an
* ADD (byte or LSL #2 scale, or GCC's SXTB/SXTH extended-register form
* with the same scales)/BR, with a selector bounded by CMP #imm and an
* unsigned B.HI or B.CS/B.HS guard on one straight-line path. Any other
* instruction sequence keeps its case labels unknown.
*/
Expand Down Expand Up @@ -3333,13 +3340,16 @@ private RecoveredAArch64RelativeTable matchAArch64RelativeDispatch(
) throws Exception {
String branchRegister = normalizedRegister(operands(instructions.get(branchIndex)));

// ADD Xd, Xn, Xm{, LSL #s}; BR Xd. LSL applies to Xm only.
// ADD Xd, Xn, Xm{, LSL #s} or ADD Xd, Xn, Wm, SXTB|SXTH{ #s}; BR Xd.
// The shift or extend applies to the last source only.
Instruction dispatchAdd = instructions.get(branchIndex - 1);
if (!mnemonic(dispatchAdd).equals("ADD")) return null;
java.util.regex.Matcher addMatcher = AARCH64_DISPATCH_ADD.matcher(operands(dispatchAdd));
if (!addMatcher.matches() || !addMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) return null;
if (!normalizedRegister(addMatcher.group(1)).equals(branchRegister)) return null;
int offsetShift = addMatcher.group(4) == null ? 0 : Integer.parseInt(addMatcher.group(4), 16);
String dispatchOperator = addMatcher.group(4) == null ? null : addMatcher.group(4).toUpperCase(Locale.ROOT);
boolean extended = dispatchOperator != null && !dispatchOperator.equals("LSL");
int offsetShift = addMatcher.group(5) == null ? 0 : Integer.parseInt(addMatcher.group(5), 16);
if (offsetShift != 0 && offsetShift != 2) return null;
String firstSource = normalizedRegister(addMatcher.group(2));
String secondSource = normalizedRegister(addMatcher.group(3));
Expand All @@ -3350,7 +3360,7 @@ private RecoveredAArch64RelativeTable matchAArch64RelativeDispatch(
int loadIndex = -1;
String entryRegister = null;
String baseRegister = null;
for (String candidate : offsetShift == 0 ? List.of(secondSource, firstSource) : List.of(secondSource)) {
for (String candidate : offsetShift == 0 && !extended ? List.of(secondSource, firstSource) : List.of(secondSource)) {
int writer = latestWriter(instructions, branchIndex - 1, candidate);
if (writer >= 0 && Set.of("LDRB", "LDRH", "LDRSB", "LDRSH").contains(mnemonic(instructions.get(writer)))) {
loadIndex = writer;
Expand All @@ -3364,12 +3374,15 @@ private RecoveredAArch64RelativeTable matchAArch64RelativeDispatch(
Instruction load = instructions.get(loadIndex);
String loadMnemonic = mnemonic(load);
int entrySize = loadMnemonic.endsWith("B") ? 1 : 2;
boolean signedEntries = loadMnemonic.startsWith("LDRS");
// GCC's casesi sign-extends the loaded entry in the dispatch ADD; the
// extend must read exactly the loaded width.
boolean signedEntries = extended || loadMnemonic.startsWith("LDRS");
if (extended && entrySize != (dispatchOperator.equals("SXTB") ? 1 : 2)) return null;
java.util.regex.Matcher loadMatcher = AARCH64_TABLE_LOAD.matcher(operands(load));
if (!loadMatcher.matches()) return null;
// A signed load into a W register sign-extends only to 32 bits; the
// 64-bit dispatch ADD would then read a zero-extended value.
if (signedEntries && !loadMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) return null;
if (!extended && signedEntries && !loadMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) return null;
String tableBaseRegister = normalizedRegister(loadMatcher.group(2));
String indexRegister = normalizedRegister(loadMatcher.group(3));
boolean wordIndex = loadMatcher.group(3).toLowerCase(Locale.ROOT).startsWith("w");
Expand Down Expand Up @@ -3547,6 +3560,7 @@ else if (mnemonic(instructions.get(indexWriter)).equals("MOV")) {
selectorWriter < 0 ? null : instructions.get(selectorWriter).getAddress(),
signedEntries,
offsetShift,
dispatchOperator,
guardMnemonic,
compareImmediate,
nullTarget,
Expand Down
11 changes: 10 additions & 1 deletion docs/getting-started/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,13 @@ For example:
rea mcp --http --host 0.0.0.0 --allowed-hosts 127.0.0.1,rea.lan
```

The endpoint uses plain HTTP without TLS or client authentication. The `Host`
and `Origin` checks guard against DNS rebinding and cross-origin browser pages;
they are not access control, because any client that can reach the port can
send an admitted `Host` header. A non-loopback bind makes the same tools and
active analysis state reachable by every such client, so bind outside loopback
only within a trusted or isolated network.

Requests with a different `Host` or browser `Origin` receive `403`; a missing
`Origin` is accepted. The announced client URL uses the bound hostname when
admitted, otherwise the first declared hostname, with the actual bound port.
Expand Down Expand Up @@ -1122,7 +1129,9 @@ rea environment-serve manifest.json --evidence capture.json --port 8765
`MCP_PORT`, then `127.0.0.1` and an ephemeral port. A non-loopback bind requires
`MCP_ALLOWED_HOSTS` with the same hostname rules and Host/Origin gates as above.
`environment-serve` uses this environment setting; `--allowed-hosts` belongs to
`rea mcp --http`. For example:
`rea mcp --http`. It likewise has no TLS or client authentication: any client
that reaches the port can call the served tools, including write tools and the
`/agentenv` methods that change the served state. For example:

```sh
MCP_ALLOWED_HOSTS=127.0.0.1,rea.lan rea environment-serve manifest.json --evidence capture.json --host 0.0.0.0
Expand Down
57 changes: 32 additions & 25 deletions scripts/verify-real-ghidra.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,31 +150,38 @@ try {
await exec(compiler, [...jumpTableFlags, sourcePath, "-o", jumpTablePath]);
const crossTargets = [];
if (aarch64JumpTableOnly) {
const relativeSource = fileURLToPath(
new URL("../tests/conformance/ghidra/relative-switch.S", import.meta.url),
);
for (const entrySize of [1, 2]) {
for (const format of [
"elf",
...(expectedNativeTarget.architecture === "arm64" ? ["mach-o"] : []),
]) {
const targetPath = join(
fixtureRoot,
`relative-${entrySize}-${format}.o`,
);
await exec(clang, [
...(format === "elf" ? ["--target=aarch64-linux-gnu"] : []),
`-DREA_ENTRY_BYTES=${entrySize}`,
"-c",
relativeSource,
"-o",
targetPath,
]);
crossTargets.push([
targetPath,
`relative-${entrySize}`,
{ format, architecture: "arm64" },
]);
// relative-switch-gcc.S shares relative-switch.S's symbol and case oracle,
// with GCC's SXTB/SXTH extended-register dispatch instead of clang's LSL.
for (const [shape, fixture] of [
["", "relative-switch.S"],
["gcc-", "relative-switch-gcc.S"],
]) {
const relativeSource = fileURLToPath(
new URL(`../tests/conformance/ghidra/${fixture}`, import.meta.url),
);
for (const entrySize of [1, 2]) {
for (const format of [
"elf",
...(expectedNativeTarget.architecture === "arm64" ? ["mach-o"] : []),
]) {
const targetPath = join(
fixtureRoot,
`relative-${shape}${entrySize}-${format}.o`,
);
await exec(clang, [
...(format === "elf" ? ["--target=aarch64-linux-gnu"] : []),
`-DREA_ENTRY_BYTES=${entrySize}`,
"-c",
relativeSource,
"-o",
targetPath,
]);
crossTargets.push([
targetPath,
`relative-${shape}${entrySize}`,
{ format, architecture: "arm64" },
]);
}
}
}
const signedRelativeSource = fileURLToPath(
Expand Down
5 changes: 5 additions & 0 deletions tests/conformance/ghidra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ callback fixture also proves that an unresolved targetless flow is not silently
promoted to a direct callee. Every target runs in a separate owned project and
must leave no process, socket, project, or runtime root after close.

`relative-switch-gcc.S` covers GCC's AArch64 dispatch, where LDRB/LDRH loads the
entry and the extended-register ADD (`SXTB`/`SXTH #2`) sign-extends it; one case
sits before the ADR label, so its entry is negative. It shares the
`relative-switch.S` symbol and case oracle for byte and halfword entries.

`relative-switch-signed.S` covers signed halfword offsets, the CBZ zero-entry
route, and selector biases of 0, 2 and 10 in the AArch64 jump-table lane. The
nonzero biases exercise overlapping and disjoint typed label/index ranges. REA
Expand Down
79 changes: 79 additions & 0 deletions tests/conformance/ghidra/relative-switch-gcc.S
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// GCC-shaped AArch64 relative dispatch, as aarch64_output_casesi in
// gcc/config/aarch64/aarch64.cc emits it: the entry is loaded zero-extended
// with LDRB/LDRH and sign-extended by the extended-register dispatch ADD
// (SXTB/SXTH #2), so entries are signed instruction offsets from the label
// after the BR. Lgcc_case3 sits before that label, so its entry is negative.
// The source labels and the immediates below are the oracle shared with
// relative-switch.S; no REA or Ghidra result defines them.
// case 0 -> 100, 1 -> 101, 2 -> 102, 3 -> 103, selector > 3 -> default (-1).
#if defined(__APPLE__)
.section __TEXT,__text,regular,pure_instructions
.globl _rea_relative_switch
.p2align 2
_rea_relative_switch:
#else
.text
.globl rea_relative_switch
.type rea_relative_switch,%function
.p2align 2
rea_relative_switch:
#endif
b Lgcc_dispatch
Lgcc_case3:
mov w0, #103
ret
Lgcc_dispatch:
cmp w0, #3
b.hi Lgcc_default
#if defined(__APPLE__)
adrp x1, Lgcc_table@PAGE
add x1, x1, Lgcc_table@PAGEOFF
#else
adrp x1, Lgcc_table
add x1, x1, :lo12:Lgcc_table
#endif
#if REA_ENTRY_BYTES == 2
ldrh w1, [x1, w0, uxtw #1]
#else
ldrb w1, [x1, w0, uxtw]
#endif
adr x0, Lgcc_rtx
#if REA_ENTRY_BYTES == 2
add x1, x0, w1, sxth #2
#else
add x1, x0, w1, sxtb #2
#endif
br x1
Lgcc_rtx:
Lgcc_case0:
mov w0, #100
ret
#if REA_ENTRY_BYTES == 2
// Halfword entries must exceed the signed byte range.
.space 1100
#endif
Lgcc_case1:
mov w0, #101
ret
Lgcc_case2:
mov w0, #102
ret
Lgcc_default:
mov w0, #-1
ret
#if !defined(__APPLE__)
.size rea_relative_switch, .-rea_relative_switch
#endif

#if defined(__APPLE__)
.section __TEXT,__const
#else
.section .rodata
#endif
.p2align 1
Lgcc_table:
#if REA_ENTRY_BYTES == 2
.short (Lgcc_case0-Lgcc_rtx)/4, (Lgcc_case1-Lgcc_rtx)/4, (Lgcc_case2-Lgcc_rtx)/4, (Lgcc_case3-Lgcc_rtx)/4
#else
.byte (Lgcc_case0-Lgcc_rtx)/4, (Lgcc_case1-Lgcc_rtx)/4, (Lgcc_case2-Lgcc_rtx)/4, (Lgcc_case3-Lgcc_rtx)/4
#endif
Loading