Merge remote-tracking branch 'origin/Ghidra_12.1'

This commit is contained in:
Ryan Kurtz
2026-04-19 05:45:33 -04:00
18 changed files with 317 additions and 43 deletions
@@ -185,20 +185,20 @@ def ghidra_trace_connect(address: Optional[str] = None) -> None:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
def ghidra_trace_listen(address: str = '0.0.0.0:0') -> None: def ghidra_trace_listen(address: str = '127.0.0.1:0') -> None:
"""Listen for Ghidra to connect for tracing. """Listen for Ghidra to connect for tracing.
Takes an optional address for the host and port on which to listen. Takes an optional address for the host and port on which to listen.
Either the form 'host:port' or just 'port'. If omitted, it will bind Either the form 'host:port' or just 'port'. If omitted, it will bind
to an ephemeral port on all interfaces. If only the port is given, to an ephemeral port on localhost. If only the port is given, it will
it will bind to that port on all interfaces. This command will block bind to that port on localhost. This command will block until the
until the connection is established. connection is established.
""" """
STATE.require_no_client() STATE.require_no_client()
parts = address.split(':') parts = address.split(':')
if len(parts) == 1: if len(parts) == 1:
host, port = '0.0.0.0', parts[0] host, port = '127.0.0.1', parts[0]
elif len(parts) == 2: elif len(parts) == 2:
host, port = parts host, port = parts
else: else:
@@ -1526,8 +1526,8 @@ def put_exceptions() -> None:
@util.dbg.eng_thread @util.dbg.eng_thread
def put_single_exception(obj: TraceObject, objpath: str, def put_single_exception(obj: TraceObject, objpath: str,
p: DbgEng._DEBUG_EXCEPTION_FILTER_PARAMETERS, p: DbgEng._DEBUG_EXCEPTION_FILTER_PARAMETERS,
offset: int, index: int, specific: bool) -> None: offset: int, index: int, specific: bool) -> None:
exc_name = "None" exc_name = "None"
if specific is True: if specific is True:
@@ -170,21 +170,21 @@ def ghidra_trace_connect(address: Optional[str] = None) -> None:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
def ghidra_trace_listen(address: str = '0.0.0.0:0') -> None: def ghidra_trace_listen(address: str = '127.0.0.1:0') -> None:
""" """
Listen for Ghidra to connect for tracing Listen for Ghidra to connect for tracing
Takes an optional address for the host and port on which to listen. Either Takes an optional address for the host and port on which to listen.
the form 'host:port' or just 'port'. If omitted, it will bind to an Either the form 'host:port' or just 'port'. If omitted, it will bind
ephemeral port on all interfaces. If only the port is given, it will bind to to an ephemeral port on localhost. If only the port is given, it will
that port on all interfaces. This command will block until the connection is bind to that port on localhost. This command will block until the
established. connection is established.
""" """
STATE.require_no_client() STATE.require_no_client()
parts = address.split(':') parts = address.split(':')
if len(parts) == 1: if len(parts) == 1:
host, port = '0.0.0.0', parts[0] host, port = '127.0.0.1', parts[0]
elif len(parts) == 2: elif len(parts) == 2:
host, port = parts host, port = parts
else: else:
@@ -199,7 +199,7 @@ def ghidra_trace_listen(address: str = '0.0.0.0:0') -> None:
c, (chost, cport) = s.accept() c, (chost, cport) = s.accept()
s.close() s.close()
print("Connection from {}:{}".format(chost, cport)) print("Connection from {}:{}".format(chost, cport))
STATE.client = Client(c, "dbgeng.dll", methods.REGISTRY) STATE.client = Client(c, "drgn", methods.REGISTRY)
except ValueError: except ValueError:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
@@ -786,7 +786,7 @@ def ghidra_trace_set_value(path: str, key: str, value: Any,
Set a value (attribute or element) in the Ghidra trace's object tree. Set a value (attribute or element) in the Ghidra trace's object tree.
A void value implies removal. A void value implies removal.
NOTE: The type of an expression may be subject to the dbgeng's current NOTE: The type of an expression may be subject to drgn's current
language. which current defaults to DEBUG_EXPR_CPLUSPLUS (vs DEBUG_EXPR_MASM). language. which current defaults to DEBUG_EXPR_CPLUSPLUS (vs DEBUG_EXPR_MASM).
For most non-primitive cases, we are punting to the Python API. For most non-primitive cases, we are punting to the Python API.
""" """
@@ -236,9 +236,9 @@ def ghidra_trace_listen(address: Optional[str] = None, *, is_mi: bool,
Takes an optional address for the host and port on which to listen. Takes an optional address for the host and port on which to listen.
Either the form 'host:port' or just 'port'. If omitted, it will bind Either the form 'host:port' or just 'port'. If omitted, it will bind
to an ephemeral port on all interfaces. If only the port is given, to an ephemeral port on localhost. If only the port is given, it will
it will bind to that port on all interfaces. This command will block bind to that port on localhost. This command will block until the
until the connection is established. connection is established.
""" """
STATE.require_no_client() STATE.require_no_client()
@@ -247,13 +247,13 @@ def ghidra_trace_listen(address: Optional[str] = None, *, is_mi: bool,
if address is not None: if address is not None:
parts = address.split(':') parts = address.split(':')
if len(parts) == 1: if len(parts) == 1:
host, port = '0.0.0.0', parts[0] host, port = '127.0.0.1', parts[0]
elif len(parts) == 2: elif len(parts) == 2:
host, port = parts host, port = parts
else: else:
raise gdb.GdbError("address must be 'port' or 'host:port'") raise gdb.GdbError("address must be 'port' or 'host:port'")
else: else:
host, port = '0.0.0.0', 0 host, port = '127.0.0.1', 0
try: try:
s = socket.socket() s = socket.socket()
s.bind((host, int(port))) s.bind((host, int(port)))
@@ -302,23 +302,23 @@ def ghidra_trace_listen(debugger: lldb.SBDebugger, command: str,
Usage: ghidra trace listen [ADDRESS] Usage: ghidra trace listen [ADDRESS]
ADDRESS must be PORT or HOST:PORT ADDRESS must be PORT or HOST:PORT
Takes an optional address for the host and port on which to listen. Either Takes an optional address for the host and port on which to listen.
the form 'host:port' or just 'port'. If omitted, it will bind to an Either the form 'host:port' or just 'port'. If omitted, it will bind
ephemeral port on all interfaces. If only the port is given, it will bind to to an ephemeral port on localhost. If only the port is given, it will
that port on all interfaces. This command will block until the connection is bind to that port on localhost. This command will block until the
established. connection is established.
""" """
args = shlex.split(command) args = shlex.split(command)
host: str host: str
port: Union[str, int] port: Union[str, int]
if len(args) == 0: if len(args) == 0:
host, port = '0.0.0.0', 0 host, port = '127.0.0.1', 0
elif len(args) == 1: elif len(args) == 1:
address = args[0] address = args[0]
parts = address.split(':') parts = address.split(':')
if len(parts) == 1: if len(parts) == 1:
host, port = '0.0.0.0', parts[0] host, port = '127.0.0.1', parts[0]
elif len(parts) == 2: elif len(parts) == 2:
host, port = parts host, port = parts
else: else:
@@ -182,20 +182,20 @@ def ghidra_trace_connect(address: Optional[str] = None) -> None:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
def ghidra_trace_listen(address: str = '0.0.0.0:0') -> None: def ghidra_trace_listen(address: str = '127.0.0.1:0') -> None:
"""Listen for Ghidra to connect for tracing. """Listen for Ghidra to connect for tracing.
Takes an optional address for the host and port on which to listen. Takes an optional address for the host and port on which to listen.
Either the form 'host:port' or just 'port'. If omitted, it will bind Either the form 'host:port' or just 'port'. If omitted, it will bind
to an ephemeral port on all interfaces. If only the port is given, to an ephemeral port on localhost. If only the port is given, it will
it will bind to that port on all interfaces. This command will block bind to that port on localhost. This command will block until the
until the connection is established. connection is established.
""" """
STATE.require_no_client() STATE.require_no_client()
parts = address.split(':') parts = address.split(':')
if len(parts) == 1: if len(parts) == 1:
host, port = '0.0.0.0', parts[0] host, port = '127.0.0.1', parts[0]
elif len(parts) == 2: elif len(parts) == 2:
host, port = parts host, port = parts
else: else:
@@ -156,7 +156,7 @@ public class JdiCommands {
public void ghidraTraceListen(String address) { public void ghidraTraceListen(String address) {
// TODO: UNTESTED // TODO: UNTESTED
state.requireNoClient(); state.requireNoClient();
String host = "0.0.0.0"; String host = "127.0.0.1";
int port = 0; int port = 0;
if (address != null) { if (address != null) {
String[] parts = address.split(":"); String[] parts = address.split(":");
@@ -70,10 +70,10 @@ import ghidra.util.exception.DuplicateFileException;
public class TraceRmiHandler extends AbstractTraceRmiConnection { public class TraceRmiHandler extends AbstractTraceRmiConnection {
/** /**
* NOTE: This can't just be Application.getApplicationVersion(), because the Python client only * NOTE: This can't just be {@link Application#getApplicationVersion()}, because the Python
* specifies up to the minor, not patch, release. * client only specifies up to the minor, not patch, release.
*/ */
public static final String VERSION = "12.0"; public static final String VERSION = "12.1";
protected static class VersionMismatchError extends TraceRmiError { protected static class VersionMismatchError extends TraceRmiError {
public VersionMismatchError(String remote) { public VersionMismatchError(String remote) {
@@ -79,7 +79,7 @@ public class TraceRmiPlugin extends Plugin implements InternalTraceRmiService {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private final Wiring autoServiceWiring; private final Wiring autoServiceWiring;
private SocketAddress serverAddress = new InetSocketAddress("0.0.0.0", DEFAULT_PORT); private SocketAddress serverAddress = new InetSocketAddress("127.0.0.1", DEFAULT_PORT);
private TraceRmiServer server; private TraceRmiServer server;
private final Set<TraceRmiHandler> handlers = new LinkedHashSet<>(); private final Set<TraceRmiHandler> handlers = new LinkedHashSet<>();
@@ -17,7 +17,6 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
"ghidratrace==12.1",
"protobuf>=6.31.0", "protobuf>=6.31.0",
] ]
@@ -48,7 +48,7 @@ from .util import send_delimited, recv_delimited
# Other places to change: # Other places to change:
# * every pyproject.toml file (incl. deps) # * every pyproject.toml file (incl. deps)
# * TraceRmiHandler.VERSION # * TraceRmiHandler.VERSION
VERSION = '12.0' VERSION = '12.1'
E = TypeVar('E') E = TypeVar('E')
@@ -37,7 +37,12 @@ import ghidra.trace.model.thread.TraceThread;
public class TraceRmiTargetTest extends AbstractGhidraHeadedDebuggerTest { public class TraceRmiTargetTest extends AbstractGhidraHeadedDebuggerTest {
class MyTraceRmiConnection extends TestTraceRmiConnection { @Test
public void testVersionConsistency() {
assertVersionMatchesApplication(TraceRmiHandler.VERSION);
}
static class MyTraceRmiConnection extends TestTraceRmiConnection {
@Override @Override
protected DebuggerTraceManagerService getTraceManager() { protected DebuggerTraceManagerService getTraceManager() {
@@ -20,13 +20,15 @@ import static org.junit.Assert.*;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.*;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*; import javax.swing.*;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
@@ -43,6 +45,8 @@ import docking.action.DockingActionIf;
import docking.widgets.table.DynamicTableColumn; import docking.widgets.table.DynamicTableColumn;
import docking.widgets.tree.GTree; import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode; import docking.widgets.tree.GTreeNode;
import generic.Unique;
import generic.jar.ResourceFile;
import ghidra.GhidraTestApplicationLayout; import ghidra.GhidraTestApplicationLayout;
import ghidra.app.nav.Navigatable; import ghidra.app.nav.Navigatable;
import ghidra.app.plugin.core.debug.gui.action.BasicAutoReadMemorySpec; import ghidra.app.plugin.core.debug.gui.action.BasicAutoReadMemorySpec;
@@ -55,6 +59,7 @@ import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.async.AsyncTestUtils; import ghidra.async.AsyncTestUtils;
import ghidra.debug.api.action.*; import ghidra.debug.api.action.*;
import ghidra.docking.settings.SettingsImpl; import ghidra.docking.settings.SettingsImpl;
import ghidra.framework.Application;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramDB; import ghidra.program.database.ProgramDB;
@@ -125,6 +130,45 @@ public abstract class AbstractGhidraHeadedDebuggerTest
public static final String LANGID_TOYBE64 = "Toy:BE:64:default"; public static final String LANGID_TOYBE64 = "Toy:BE:64:default";
public static void assertVersionMatchesApplication(String version) {
String applicationVersion = Application.getApplicationVersion();
List<String> partsExp = List.of(applicationVersion.split("\\."));
List<String> partsAct = List.of(version.split("\\."));
assertTrue("Version %s cannot be more specific than application version %s".formatted(
version, applicationVersion), partsExp.size() >= partsAct.size());
assertEquals("Version %s is not consistent with application version %s".formatted(version,
applicationVersion), partsAct, partsExp.subList(0, partsAct.size()));
}
public static List<String> readToml(String module) throws IOException {
ResourceFile toml = Application.getModuleFile(module, "src/main/py/pyproject.toml");
try (BufferedReader reader = new BufferedReader(new FileReader(toml.getFile(false)))) {
return reader.lines().toList();
}
}
private static String group(CharSequence seq, Pattern pat) {
Matcher matcher = pat.matcher(seq);
assertTrue(matcher.matches());
return matcher.group(1);
}
public static String parseVersionFromToml(List<String> toml) throws IOException {
Pattern versionEq = Pattern.compile("version = \"(.*)\"");
return Unique.assertOne(toml.stream()
.filter(versionEq.asMatchPredicate())
.map(l -> group(l, versionEq)));
}
public static String parseGhidraTraceDepFromToml(List<String> toml) throws IOException {
Pattern ghidraTraceDep = Pattern.compile("\\s+\"ghidratrace==(.*)\",?");
return Unique.assertOne(toml.stream()
.dropWhile(l -> !"dependencies = [".equals(l))
.takeWhile(l -> !"]".equals(l))
.filter(ghidraTraceDep.asMatchPredicate())
.map(l -> group(l, ghidraTraceDep)));
}
protected static byte[] arr(String hex) { protected static byte[] arr(String hex) {
return NumericUtilities.convertStringToBytes(hex); return NumericUtilities.convertStringToBytes(hex);
} }
@@ -339,6 +339,12 @@ public class TraceRmiPythonClientTest extends AbstractGhidraHeadedDebuggerTest {
waitFor(() -> tb.trace.getCurrentTransactionInfo() == null); waitFor(() -> tb.trace.getCurrentTransactionInfo() == null);
} }
@Test
public void testTomlVersionConsistency() throws IOException {
List<String> toml = readToml("Debugger-rmi-trace");
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test @Test
public void testConnect() throws Exception { public void testConnect() throws Exception {
runThrowError(addr -> """ runThrowError(addr -> """
@@ -0,0 +1,44 @@
/* ###
* 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.
*/
package agent.dbgeng.rmi;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
public class DbgEngVersionTest extends AbstractGhidraHeadedDebuggerTest {
List<String> toml;
@Before
public void read() throws IOException {
toml = readToml("Debugger-agent-dbgeng");
}
@Test
public void testTomlVersionConsistency() throws IOException {
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test
public void testTomlGhidratraceDepVersion() throws IOException {
assertVersionMatchesApplication(parseGhidraTraceDepFromToml(toml));
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.
*/
package agent.drgn.rmi;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
public class DrgnVersionTest extends AbstractGhidraHeadedDebuggerTest {
List<String> toml;
@Before
public void read() throws IOException {
toml = readToml("Debugger-agent-drgn");
}
@Test
public void testTomlVersionConsistency() throws IOException {
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test
public void testTomlGhidratraceDepVersion() throws IOException {
assertVersionMatchesApplication(parseGhidraTraceDepFromToml(toml));
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.
*/
package agent.gdb.rmi;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
public class GdbVersionTest extends AbstractGhidraHeadedDebuggerTest {
List<String> toml;
@Before
public void read() throws IOException {
toml = readToml("Debugger-agent-gdb");
}
@Test
public void testTomlVersionConsistency() throws IOException {
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test
public void testTomlGhidratraceDepVersion() throws IOException {
assertVersionMatchesApplication(parseGhidraTraceDepFromToml(toml));
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.
*/
package agent.lldb.rmi;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
public class LldbVersionTest extends AbstractGhidraHeadedDebuggerTest {
List<String> toml;
@Before
public void read() throws IOException {
toml = readToml("Debugger-agent-lldb");
}
@Test
public void testTomlVersionConsistency() throws IOException {
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test
public void testTomlGhidratraceDepVersion() throws IOException {
assertVersionMatchesApplication(parseGhidraTraceDepFromToml(toml));
}
}
@@ -0,0 +1,44 @@
/* ###
* 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.
*/
package agent.x64dbg.rmi;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
public class X64dbgVersionTest extends AbstractGhidraHeadedDebuggerTest {
List<String> toml;
@Before
public void read() throws IOException {
toml = readToml("Debugger-agent-x64dbg");
}
@Test
public void testTomlVersionConsistency() throws IOException {
assertVersionMatchesApplication(parseVersionFromToml(toml));
}
@Test
public void testTomlGhidratraceDepVersion() throws IOException {
assertVersionMatchesApplication(parseGhidraTraceDepFromToml(toml));
}
}