GP-4209: GhidraTime-MSTTD integration. Type hints for (most) Python agents.

This commit is contained in:
Dan 2025-03-24 18:28:07 +00:00
parent deb49d5322
commit 21a1602579
93 changed files with 6453 additions and 4118 deletions

View file

@ -44,10 +44,15 @@ public abstract class AbstractTraceRmiConnection implements TraceRmiConnection {
protected void doActivate(TraceObject object, Trace trace, TraceSnapshot snapshot) {
DebuggerCoordinates coords = getTraceManager().getCurrent();
if (coords.getTrace() != trace) {
coords = DebuggerCoordinates.NOWHERE;
coords = DebuggerCoordinates.NOWHERE.trace(trace);
}
if (snapshot != null && followsPresent(trace)) {
coords = coords.snap(snapshot.getKey());
if (snapshot.getKey() > 0 || snapshot.getSchedule() == null) {
coords = coords.snap(snapshot.getKey());
}
else {
coords = coords.time(snapshot.getSchedule());
}
}
DebuggerCoordinates finalCoords = object == null ? coords : coords.object(object);
Swing.runLater(() -> {
@ -68,5 +73,4 @@ public abstract class AbstractTraceRmiConnection implements TraceRmiConnection {
}
});
}
}

View file

@ -26,6 +26,7 @@ import ghidra.rmi.trace.TraceRmi.*;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.Msg;
class OpenTrace implements ValueDecoder {
@ -79,9 +80,16 @@ class OpenTrace implements ValueDecoder {
trace.release(consumer);
}
public TraceSnapshot createSnapshot(Snap snap, String description) {
TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap.getSnap(), true);
snapshot.setDescription(description);
public TraceSnapshot createSnapshot(long snap) {
TraceSnapshot snapshot = trace.getTimeManager().getSnapshot(snap, true);
return this.lastSnapshot = snapshot;
}
public TraceSnapshot createSnapshot(TraceSchedule schedule) {
if (schedule.isSnapOnly()) {
return createSnapshot(schedule.getSnap());
}
TraceSnapshot snapshot = trace.getTimeManager().findScratchSnapshot(schedule);
return this.lastSnapshot = snapshot;
}

View file

@ -61,12 +61,13 @@ import ghidra.trace.model.target.path.*;
import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
import ghidra.trace.model.target.schema.XmlSchemaContext;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
public class TraceRmiHandler extends AbstractTraceRmiConnection {
public static final String VERSION = "11.3";
public static final String VERSION = "11.4";
protected static class VersionMismatchError extends TraceRmiError {
public VersionMismatchError(String remote) {
@ -740,77 +741,43 @@ public class TraceRmiHandler extends AbstractTraceRmiConnection {
}
protected static Value makeValue(Object value) {
if (value instanceof Void) {
return Value.newBuilder().setNullValue(Null.getDefaultInstance()).build();
}
if (value instanceof Boolean b) {
return Value.newBuilder().setBoolValue(b).build();
}
if (value instanceof Byte b) {
return Value.newBuilder().setByteValue(b).build();
}
if (value instanceof Character c) {
return Value.newBuilder().setCharValue(c).build();
}
if (value instanceof Short s) {
return Value.newBuilder().setShortValue(s).build();
}
if (value instanceof Integer i) {
return Value.newBuilder().setIntValue(i).build();
}
if (value instanceof Long l) {
return Value.newBuilder().setLongValue(l).build();
}
if (value instanceof String s) {
return Value.newBuilder().setStringValue(s).build();
}
if (value instanceof boolean[] ba) {
return Value.newBuilder()
return switch (value) {
case Void v -> Value.newBuilder().setNullValue(Null.getDefaultInstance()).build();
case Boolean b -> Value.newBuilder().setBoolValue(b).build();
case Byte b -> Value.newBuilder().setByteValue(b).build();
case Character c -> Value.newBuilder().setCharValue(c).build();
case Short s -> Value.newBuilder().setShortValue(s).build();
case Integer i -> Value.newBuilder().setIntValue(i).build();
case Long l -> Value.newBuilder().setLongValue(l).build();
case String s -> Value.newBuilder().setStringValue(s).build();
case boolean[] ba -> Value.newBuilder()
.setBoolArrValue(
BoolArr.newBuilder().addAllArr(Arrays.asList(ArrayUtils.toObject(ba))))
.build();
}
if (value instanceof byte[] ba) {
return Value.newBuilder().setBytesValue(ByteString.copyFrom(ba)).build();
}
if (value instanceof char[] ca) {
return Value.newBuilder().setCharArrValue(new String(ca)).build();
}
if (value instanceof short[] sa) {
return Value.newBuilder()
case byte[] ba -> Value.newBuilder().setBytesValue(ByteString.copyFrom(ba)).build();
case char[] ca -> Value.newBuilder().setCharArrValue(new String(ca)).build();
case short[] sa -> Value.newBuilder()
.setShortArrValue(ShortArr.newBuilder()
.addAllArr(
Stream.of(ArrayUtils.toObject(sa)).map(s -> (int) s).toList()))
.build();
}
if (value instanceof int[] ia) {
return Value.newBuilder()
case int[] ia -> Value.newBuilder()
.setIntArrValue(
IntArr.newBuilder().addAllArr(IntStream.of(ia).mapToObj(i -> i).toList()))
.build();
}
if (value instanceof long[] la) {
return Value.newBuilder()
case long[] la -> Value.newBuilder()
.setLongArrValue(
LongArr.newBuilder().addAllArr(LongStream.of(la).mapToObj(l -> l).toList()))
.build();
}
if (value instanceof String[] sa) {
return Value.newBuilder()
case String[] sa -> Value.newBuilder()
.setStringArrValue(StringArr.newBuilder().addAllArr(List.of(sa)))
.build();
}
if (value instanceof Address a) {
return Value.newBuilder().setAddressValue(makeAddr(a)).build();
}
if (value instanceof AddressRange r) {
return Value.newBuilder().setRangeValue(makeAddrRange(r)).build();
}
if (value instanceof TraceObject o) {
return Value.newBuilder().setChildDesc(makeObjDesc(o)).build();
}
throw new AssertionError(
"Cannot encode value: " + value + "(type=" + value.getClass() + ")");
case Address a -> Value.newBuilder().setAddressValue(makeAddr(a)).build();
case AddressRange r -> Value.newBuilder().setRangeValue(makeAddrRange(r)).build();
case TraceObject o -> Value.newBuilder().setChildDesc(makeObjDesc(o)).build();
default -> throw new AssertionError(
"Cannot encode value: " + value + "(type=" + value.getClass() + ")");
};
}
protected static MethodArgument makeArgument(String name, Object value) {
@ -958,8 +925,9 @@ public class TraceRmiHandler extends AbstractTraceRmiConnection {
dis.applyTo(open.trace.getFixedProgramView(snap), monitor);
}
AddressSetView result = dis.getDisassembledAddressSet();
return ReplyDisassemble.newBuilder()
.setLength(dis.getDisassembledAddressSet().getNumAddresses())
.setLength(result == null ? 0 : result.getNumAddresses())
.build();
}
@ -1180,13 +1148,21 @@ public class TraceRmiHandler extends AbstractTraceRmiConnection {
protected ReplySnapshot handleSnapshot(RequestSnapshot req) {
OpenTrace open = requireOpenTrace(req.getOid());
TraceSnapshot snapshot = open.createSnapshot(req.getSnap(), req.getDescription());
TraceSnapshot snapshot = switch (req.getTimeCase()) {
case TIME_NOT_SET -> throw new TraceRmiError("snap or time required");
case SNAP -> open.createSnapshot(req.getSnap().getSnap());
case SCHEDULE -> open
.createSnapshot(TraceSchedule.parse(req.getSchedule().getSchedule()));
};
snapshot.setDescription(req.getDescription());
if (!"".equals(req.getDatetime())) {
Instant instant =
DateTimeFormatter.ISO_INSTANT.parse(req.getDatetime()).query(Instant::from);
snapshot.setRealTime(instant.toEpochMilli());
}
return ReplySnapshot.getDefaultInstance();
return ReplySnapshot.newBuilder()
.setSnap(Snap.newBuilder().setSnap(snapshot.getKey()))
.build();
}
protected ReplyStartTx handleStartTx(RequestStartTx req) {

View file

@ -61,11 +61,11 @@ import ghidra.trace.model.target.schema.*;
import ghidra.trace.model.target.schema.PrimitiveTraceObjectSchema.MinimalSchemaContext;
import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
import ghidra.trace.model.thread.*;
import ghidra.trace.model.time.schedule.TraceSchedule.ScheduleForm;
import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
public class TraceRmiTarget extends AbstractTarget {
class TraceRmiActionEntry implements ActionEntry {
private final RemoteMethod method;
private final Map<String, Object> args;
@ -169,6 +169,48 @@ public class TraceRmiTarget extends AbstractTarget {
}
}
protected ScheduleForm getSupportedTimeFormByMethod(TraceObject obj) {
KeyPath path = obj.getCanonicalPath();
MatchedMethod activate = matches.getBest(ActivateMatcher.class, path, ActionName.ACTIVATE,
ActivateMatcher.makeBySpecificity(obj.getRoot().getSchema(), path));
if (activate == null) {
return null;
}
if (activate.params.get("time") != null) {
return ScheduleForm.SNAP_ANY_STEPS_OPS;
}
if (activate.params.get("snap") != null) {
return ScheduleForm.SNAP_ONLY;
}
return null;
}
protected ScheduleForm getSupportedTimeFormByAttribute(TraceObject obj, long snap) {
TraceObject eventScope = obj.findSuitableInterface(TraceObjectEventScope.class);
if (eventScope == null) {
return null;
}
TraceObjectValue timeSupportStr =
eventScope.getAttribute(snap, TraceObjectEventScope.KEY_TIME_SUPPORT);
if (timeSupportStr == null) {
return null;
}
return ScheduleForm.valueOf(timeSupportStr.castValue());
}
@Override
public ScheduleForm getSupportedTimeForm(TraceObject obj, long snap) {
ScheduleForm byMethod = getSupportedTimeFormByMethod(obj);
if (byMethod == null) {
return null;
}
ScheduleForm byAttr = getSupportedTimeFormByAttribute(obj, snap);
if (byAttr == null) {
return null;
}
return byMethod.intersect(byAttr);
}
@Override
public TraceExecutionState getThreadExecutionState(TraceThread thread) {
if (!(thread instanceof TraceObjectThread tot)) {
@ -385,7 +427,8 @@ public class TraceRmiTarget extends AbstractTarget {
.orElse(null);
}
record ParamAndObjectArg(RemoteParameter param, TraceObject obj) {}
record ParamAndObjectArg(RemoteParameter param, TraceObject obj) {
}
protected ParamAndObjectArg getFirstObjectArgument(RemoteMethod method,
Map<String, Object> args) {
@ -828,7 +871,8 @@ public class TraceRmiTarget extends AbstractTarget {
static final List<ToggleBreakMatcher> SPEC = matchers(HAS_SPEC);
}
record MatchKey(Class<? extends MethodMatcher> cls, ActionName action, TraceObjectSchema sch) {}
record MatchKey(Class<? extends MethodMatcher> cls, ActionName action, TraceObjectSchema sch) {
}
protected class Matches {
private final Map<MatchKey, MatchedMethod> map = new HashMap<>();
@ -975,7 +1019,8 @@ public class TraceRmiTarget extends AbstractTarget {
@Override
public CompletableFuture<Void> activateAsync(DebuggerCoordinates prev,
DebuggerCoordinates coords) {
if (prev.getSnap() != coords.getSnap()) {
boolean timeNeq = !Objects.equals(prev.getTime(), coords.getTime());
if (timeNeq) {
requestCaches.invalidate();
}
TraceObject object = coords.getObject();
@ -983,10 +1028,9 @@ public class TraceRmiTarget extends AbstractTarget {
return AsyncUtils.nil();
}
MatchedMethod activate =
matches.getBest(ActivateMatcher.class, object.getCanonicalPath(), ActionName.ACTIVATE,
() -> ActivateMatcher.makeBySpecificity(trace.getObjectManager().getRootSchema(),
object.getCanonicalPath()));
KeyPath path = object.getCanonicalPath();
MatchedMethod activate = matches.getBest(ActivateMatcher.class, path, ActionName.ACTIVATE,
ActivateMatcher.makeBySpecificity(object.getRoot().getSchema(), path));
if (activate == null) {
return AsyncUtils.nil();
}
@ -996,11 +1040,11 @@ public class TraceRmiTarget extends AbstractTarget {
args.put(paramFocus.name(),
object.findSuitableSchema(getSchemaContext().getSchema(paramFocus.type())));
RemoteParameter paramTime = activate.params.get("time");
if (paramTime != null) {
if (paramTime != null && (paramTime.required() || timeNeq)) {
args.put(paramTime.name(), coords.getTime().toString());
}
RemoteParameter paramSnap = activate.params.get("snap");
if (paramSnap != null) {
if (paramSnap != null && (paramSnap.required() || timeNeq)) {
args.put(paramSnap.name(), coords.getSnap());
}
return activate.method.invokeAsync(args).toCompletableFuture().thenApply(__ -> null);

View file

@ -56,6 +56,10 @@ message Snap {
int64 snap = 1;
}
message Schedule {
string schedule = 1;
}
message Span {
int64 min = 1;
int64 max = 2;
@ -392,10 +396,14 @@ message RequestSnapshot {
DomObjId oid = 1;
string description = 2;
string datetime = 3;
Snap snap = 4;
oneof time {
Snap snap = 4;
Schedule schedule = 5;
}
}
message ReplySnapshot {
Snap snap = 1;
}
// Client commands

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ghidratrace"
version = "11.3"
version = "11.4"
authors = [
{ name="Ghidra Development Team" },
]
@ -23,3 +23,6 @@ dependencies = [
[project.urls]
"Homepage" = "https://github.com/NationalSecurityAgency/ghidra"
"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues"
[tool.setuptools.package-data]
ghidratrace = ["py.typed"]

View file

@ -0,0 +1,114 @@
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
from concurrent.futures import Future
from typing import Any, Callable, List, Optional, Sequence, TypeVar, Union
from .client import Address, TraceObject, TraceObjectValue
T = TypeVar('T')
def wait_opt(val: Union[T, Future[T], None]) -> Optional[T]:
if val is None:
return None
if isinstance(val, Future):
return val.result()
return val
def wait(val: Union[T, Future[T]]) -> T:
if isinstance(val, Future):
return val.result()
return val
class TableColumn(object):
def __init__(self, head: str) -> None:
self.head = head
self.contents = [head]
self.is_last = False
def add_data(self, data: str) -> None:
self.contents.append(data)
def finish(self) -> None:
self.width = max(len(d) for d in self.contents) + 1
def format_cell(self, i: int) -> str:
return (self.contents[i] if self.is_last
else self.contents[i].ljust(self.width))
class Tabular(object):
def __init__(self, heads: List[str]) -> None:
self.columns = [TableColumn(h) for h in heads]
self.columns[-1].is_last = True
self.num_rows = 1
def add_row(self, datas: List[str]) -> None:
for c, d in zip(self.columns, datas):
c.add_data(d)
self.num_rows += 1
def print_table(self, println: Callable[[str], None]) -> None:
for c in self.columns:
c.finish()
for rn in range(self.num_rows):
println(''.join(c.format_cell(rn) for c in self.columns))
def repr_or_future(val: Union[T, Future[T]]) -> str:
if isinstance(val, Future):
if val.done():
return str(val.result())
else:
return "<Future>"
else:
return str(val)
def obj_repr(obj: TraceObject) -> str:
if obj.path is None:
if obj.id is None:
return "<ERR: no path nor id>"
else:
return f"<id={repr_or_future(obj.id)}>"
elif isinstance(obj.path, Future):
if obj.path.done():
return obj.path.result()
elif obj.id is None:
return "<path=<Future>>"
else:
return f"<id={repr_or_future(obj.id)}>"
else:
return obj.path
def val_repr(value: Any) -> str:
if isinstance(value, TraceObject):
return obj_repr(value)
elif isinstance(value, Address):
return f'{value.space}:{value.offset:08x}'
return repr(value)
def print_tabular_values(values: Sequence[TraceObjectValue],
println: Callable[[str], None]) -> None:
table = Tabular(['Parent', 'Key', 'Span', 'Value', 'Type'])
for v in values:
table.add_row([obj_repr(v.parent), v.key, str(v.span),
val_repr(v.value), str(v.schema)])
table.print_table(println)

View file

@ -1,22 +1,21 @@
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
from dataclasses import dataclass
# Use instances as type annotations or as schema
@dataclass(frozen=True)
class Schema:
name: str
@ -25,6 +24,7 @@ class Schema:
return self.name
UNSPECIFIED = Schema('')
ANY = Schema('ANY')
OBJECT = Schema('OBJECT')
VOID = Schema('VOID')

View file

@ -1,33 +1,38 @@
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
from concurrent.futures import Future
import socket
import traceback
from typing import TypeVar
from google.protobuf import message as _message
M = TypeVar('M', bound=_message.Message)
def send_length(s, value):
def send_length(s: socket.socket, value: int) -> None:
s.sendall(value.to_bytes(4, 'big'))
def send_delimited(s, msg):
def send_delimited(s: socket.socket, msg: _message.Message) -> None:
data = msg.SerializeToString()
send_length(s, len(data))
s.sendall(data)
def recv_all(s, size):
def recv_all(s, size: int) -> bytes:
buf = b''
while len(buf) < size:
part = s.recv(size - len(buf))
@ -38,14 +43,14 @@ def recv_all(s, size):
# return s.recv(size, socket.MSG_WAITALL)
def recv_length(s):
def recv_length(s: socket.socket) -> int:
buf = recv_all(s, 4)
if len(buf) < 4:
raise Exception("Socket closed")
return int.from_bytes(buf, 'big')
def recv_delimited(s, msg, dbg_seq):
def recv_delimited(s: socket.socket, msg: M, dbg_seq: int) -> M:
size = recv_length(s)
buf = recv_all(s, size)
if len(buf) < size: