GP-4389: Fixes for Trace RMI lldb on macOS

Create local-lldh.sh launch script
Upgrade to JNA-5.14
Fix pty IOCTL numbers for macOS
Fix compile-spec mapping
Improv error report / clean-up after launch failure.
Write ERROR state on memory read failures
Convert Python exceptions to LLDB command errors
This commit is contained in:
Dan 2024-03-04 16:45:41 -05:00
parent bb8ec1cbe6
commit 973b9a8d4c
50 changed files with 1247 additions and 723 deletions

View file

@ -25,7 +25,7 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions; import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
import agent.gdb.manager.impl.GdbManagerImpl; import agent.gdb.manager.impl.GdbManagerImpl;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.unix.UnixPty;
/** /**
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal * The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
@ -232,7 +232,7 @@ public interface GdbManager extends AutoCloseable, GdbConsoleOperations, GdbBrea
* Note: depending on the target, its output may not be communicated via this listener. Local * Note: depending on the target, its output may not be communicated via this listener. Local
* targets, e.g., tend to just print output to GDB's controlling TTY. See * targets, e.g., tend to just print output to GDB's controlling TTY. See
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's * {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
* input and output. See also {@link LinuxPty} for a means to easily acquire a new TTY from * input and output. See also {@link UnixPty} for a means to easily acquire a new TTY from
* Java. * Java.
* *
* @param listener the listener to add * @param listener the listener to add

View file

@ -551,7 +551,7 @@ def putmem_state(address, length, state, pages=True):
inf = gdb.selected_inferior() inf = gdb.selected_inferior()
base, addr = STATE.trace.memory_mapper.map(inf, start) base, addr = STATE.trace.memory_mapper.map(inf, start)
if base != addr.space: if base != addr.space:
trace.create_overlay_space(base, addr.space) STATE.trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state) STATE.trace.set_memory_state(addr.extend(end - start), state)

View file

@ -17,11 +17,10 @@ from concurrent.futures import Future, Executor
from contextlib import contextmanager from contextlib import contextmanager
import re import re
import gdb
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
import gdb
from . import commands, hooks, util from . import commands, hooks, util
@ -690,8 +689,8 @@ def read_mem(inferior: sch.Schema('Inferior'), range: AddressRange):
gdb.execute( gdb.execute(
f'ghidra trace putmem 0x{offset_start:x} {range.length()}') f'ghidra trace putmem 0x{offset_start:x} {range.length()}')
except: except:
commands.putmem_state( gdb.execute(
offset_start, offset_start+range.length() - 1, 'error') f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error')
@REGISTRY.method @REGISTRY.method

View file

@ -23,7 +23,8 @@ import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.linux.LinuxIoctls;
import ghidra.pty.unix.UnixPty;
import ghidra.util.Msg; import ghidra.util.Msg;
@Ignore("Need compatible GDB version for CI") @Ignore("Need compatible GDB version for CI")
@ -45,13 +46,13 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
} }
} }
protected LinuxPty ptyUserGdb; protected UnixPty ptyUserGdb;
protected PtySession gdb; protected PtySession gdb;
@Override @Override
protected CompletableFuture<Void> startManager(GdbManager manager) { protected CompletableFuture<Void> startManager(GdbManager manager) {
try { try {
ptyUserGdb = LinuxPty.openpty(); ptyUserGdb = UnixPty.openpty(LinuxIoctls.INSTANCE);
manager.start(null); manager.start(null);
Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName()); Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName());

View file

@ -0,0 +1,75 @@
#!/usr/bin/env bash
## ###
# 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.
##
#@title lldb
#@desc <html><body width="300px">
#@desc <h3>Launch with <tt>lldb</tt></h3>
#@desc <p>This will launch the target on the local machine using <tt>lldb</tt>. LLDB must already
#@desc be installed on your system, and it must embed the Python 3 interpreter. You will also
#@desc need <tt>protobuf</tt> and <tt>psutil</tt> installed for Python 3.</p>
#@desc </body></html>
#@menu-group local
#@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#lldb
#@enum StartCmd:str run "process launch" "process launch --stop-at-entry"
#@arg :str "Image" "The target binary executable image"
#@args "Arguments" "Command-line arguments to pass to the target"
#@env OPT_LLDB_PATH:str="lldb" "Path to lldb" "The path to lldb. Omit the full path to resolve using the system PATH."
#@env OPT_START_CMD:StartCmd="process launch" "Run command" "The lldb command to actually run the target."
#@env OPT_EXTRA_TTY:bool=false "Target TTY" "Provide a separate terminal emulator for the target."
#@tty TTY_TARGET if env:OPT_EXTRA_TTY
if [ -d ${GHIDRA_HOME}/ghidra/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-agent-lldb/build/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
elif [ -d ${GHIDRA_HOME}/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-lldb/build/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
else
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-lldb/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH
fi
target_image="$1"
shift
target_args="$@"
if [ -z "$target_args" ]
then
argspart=
else
argspart=-o "settings set target.run-args $target_args"
fi
if [ -z "$TARGET_TTY" ]
then
ttypart=
else
ttypart=-o "settings set target.output-path $TTY_TARGET" -o "settings set target.input-path $TTY_TARGET"
fi
"$OPT_LLDB_PATH" \
-o "version" \
-o "script import ghidralldb" \
-o "target create \"$target_image\"" \
$argspart \
$ttypart \
-o "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \
-o "ghidra trace start" \
-o "ghidra trace sync-enable" \
-o "$OPT_START_CMD"

View file

@ -14,20 +14,20 @@
# limitations under the License. # limitations under the License.
## ##
from ghidratrace.client import Address, RegVal from ghidratrace.client import Address, RegVal
import lldb import lldb
from . import util from . import util
# NOTE: This map is derived from the ldefs using a script # NOTE: This map is derived from the ldefs using a script
language_map = { language_map = {
'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'], 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'armv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'armv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'arm64': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:v8A'],
'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'], 'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
'arm64e': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64e': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:v8A'],
'i386': ['x86:LE:32:default'], 'i386': ['x86:LE:32:default'],
'thumbv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'thumbv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'thumbv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'thumbv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
@ -40,7 +40,7 @@ data64_compiler_map = {
None: 'pointer64', None: 'pointer64',
} }
x86_compiler_map = { default_compiler_map = {
'freebsd': 'gcc', 'freebsd': 'gcc',
'linux': 'gcc', 'linux': 'gcc',
'netbsd': 'gcc', 'netbsd': 'gcc',
@ -55,10 +55,12 @@ x86_compiler_map = {
} }
compiler_map = { compiler_map = {
'DATA:BE:64:default': data64_compiler_map, 'DATA:BE:64:': data64_compiler_map,
'DATA:LE:64:default': data64_compiler_map, 'DATA:LE:64:': data64_compiler_map,
'x86:LE:32:default': x86_compiler_map, 'x86:LE:32:': default_compiler_map,
'x86:LE:64:default': x86_compiler_map, 'x86:LE:64:': default_compiler_map,
'ARM:LE:32:': default_compiler_map,
'ARM:LE:64:': default_compiler_map,
} }
@ -132,12 +134,20 @@ def compute_ghidra_compiler(lang):
return comp return comp
# Check if the selected lang has specific compiler recommendations # Check if the selected lang has specific compiler recommendations
if not lang in compiler_map: matched_lang = sorted(
(l for l in compiler_map if l in lang),
key=lambda l: compiler_map[l]
)
if len(matched_lang) == 0:
return 'default' return 'default'
comp_map = compiler_map[lang] comp_map = compiler_map[matched_lang[0]]
osabi = get_osabi() osabi = get_osabi()
if osabi in comp_map: matched_osabi = sorted(
return comp_map[osabi] (l for l in comp_map if l in osabi),
key=lambda l: comp_map[l]
)
if len(matched_osabi) > 0:
return comp_map[matched_osabi[0]]
if None in comp_map: if None in comp_map:
return comp_map[None] return comp_map[None]
return 'default' return 'default'
@ -161,7 +171,8 @@ class DefaultMemoryMapper(object):
def map_back(self, proc: lldb.SBProcess, address: Address) -> int: def map_back(self, proc: lldb.SBProcess, address: Address) -> int:
if address.space == self.defaultSpace: if address.space == self.defaultSpace:
return address.offset return address.offset
raise ValueError(f"Address {address} is not in process {proc.GetProcessID()}") raise ValueError(
f"Address {address} is not in process {proc.GetProcessID()}")
DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram') DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram')
@ -203,7 +214,7 @@ class DefaultRegisterMapper(object):
def map_value(self, proc, name, value): def map_value(self, proc, name, value):
try: try:
### TODO: this seems half-baked # TODO: this seems half-baked
av = value.to_bytes(8, "big") av = value.to_bytes(8, "big")
except e: except e:
raise ValueError("Cannot convert {}'s value: '{}', type: '{}'" raise ValueError("Cannot convert {}'s value: '{}', type: '{}'"
@ -258,4 +269,3 @@ def compute_register_mapper(lang):
if ':LE:' in lang: if ':LE:' in lang:
return DEFAULT_LE_REGISTER_MAPPER return DEFAULT_LE_REGISTER_MAPPER
return register_mappers[lang] return register_mappers[lang]

View file

@ -14,20 +14,23 @@
# limitations under the License. # limitations under the License.
## ##
from contextlib import contextmanager from contextlib import contextmanager
import functools
import inspect import inspect
import os.path import os.path
import shlex
import socket import socket
import time
import sys import sys
import time
import psutil
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import Client, Address, AddressRange, TraceObject from ghidratrace.client import Client, Address, AddressRange, TraceObject
import psutil
import lldb import lldb
from . import arch, hooks, methods, util from . import arch, hooks, methods, util
PAGE_SIZE = 4096 PAGE_SIZE = 4096
DEFAULT_REGISTER_BANK = "General Purpose Registers" DEFAULT_REGISTER_BANK = "General Purpose Registers"
@ -119,59 +122,124 @@ class State(object):
STATE = State() STATE = State()
if __name__ == '__main__': if __name__ == '__main__':
lldb.SBDebugger.InitializeWithErrorHandling(); lldb.SBDebugger.InitializeWithErrorHandling()
lldb.debugger = lldb.SBDebugger.Create() lldb.debugger = lldb.SBDebugger.Create()
elif lldb.debugger: elif lldb.debugger:
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_connect "ghidra_trace_connect"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_listen "ghidra_trace_listen"') 'command container add -h "Commands for connecting to Ghidra" ghidra')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_disconnect "ghidra_trace_disconnect"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_start "ghidra_trace_start"') 'command container add -h "Commands for exporting data to a Ghidra trace" ghidra trace')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_stop "ghidra_trace_stop"')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_restart "ghidra_trace_restart"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_info "ghidra_trace_info"') 'command script add -f ghidralldb.commands.ghidra_trace_connect ghidra trace connect')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_info_lcsp "ghidra_trace_info_lcsp"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txstart "ghidra_trace_txstart"') 'command script add -f ghidralldb.commands.ghidra_trace_listen ghidra trace listen')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txcommit "ghidra_trace_txcommit"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txabort "ghidra_trace_txabort"') 'command script add -f ghidralldb.commands.ghidra_trace_disconnect ghidra trace disconnect')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txopen "ghidra_trace_txopen"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_save "ghidra_trace_save"') 'command script add -f ghidralldb.commands.ghidra_trace_start ghidra trace start')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_new_snap "ghidra_trace_new_snap"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_set_snap "ghidra_trace_set_snap"') 'command script add -f ghidralldb.commands.ghidra_trace_stop ghidra trace stop')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putmem "ghidra_trace_putmem"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putval "ghidra_trace_putval"') 'command script add -f ghidralldb.commands.ghidra_trace_restart ghidra trace restart')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putmem_state "ghidra_trace_putmem_state"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_delmem "ghidra_trace_delmem"') 'command script add -f ghidralldb.commands.ghidra_trace_info ghidra trace info')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putreg "ghidra_trace_putreg"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_delreg "ghidra_trace_delreg"') 'command script add -f ghidralldb.commands.ghidra_trace_info_lcsp ghidra trace info-lcsp')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_create_obj "ghidra_trace_create_obj"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_insert_obj "ghidra_trace_insert_obj"') 'command script add -f ghidralldb.commands.ghidra_trace_txstart ghidra trace tx-start')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_remove_obj "ghidra_trace_remove_obj"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_set_value "ghidra_trace_set_value"') 'command script add -f ghidralldb.commands.ghidra_trace_txcommit ghidra trace tx-commit')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_retain_values "ghidra_trace_retain_values"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_obj "ghidra_trace_get_obj"') 'command script add -f ghidralldb.commands.ghidra_trace_txabort ghidra trace tx-abort')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_values "ghidra_trace_get_values"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_get_values_rng "ghidra_trace_get_values_rng"') 'command script add -f ghidralldb.commands.ghidra_trace_txopen ghidra trace tx-open')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_activate "ghidra_trace_activate"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_disassemble "ghidra_trace_disassemble"') 'command script add -f ghidralldb.commands.ghidra_trace_save ghidra trace save')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_processes "ghidra_trace_put_processes"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_available "ghidra_trace_put_available"') 'command script add -f ghidralldb.commands.ghidra_trace_new_snap ghidra trace new-snap')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_breakpoints "ghidra_trace_put_breakpoints"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_watchpoints "ghidra_trace_put_watchpoints"') 'command script add -f ghidralldb.commands.ghidra_trace_set_snap ghidra trace set-snap')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_environment "ghidra_trace_put_environment"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_regions "ghidra_trace_put_regions"') 'command script add -f ghidralldb.commands.ghidra_trace_putmem ghidra trace putmem')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_modules "ghidra_trace_put_modules"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_threads "ghidra_trace_put_threads"') 'command script add -f ghidralldb.commands.ghidra_trace_putval ghidra trace putval')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_frames "ghidra_trace_put_frames"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_put_all "ghidra_trace_put_all"') 'command script add -f ghidralldb.commands.ghidra_trace_putmem_state ghidra trace putmem-state')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_install_hooks "ghidra_trace_install_hooks"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_remove_hooks "ghidra_trace_remove_hooks"') 'command script add -f ghidralldb.commands.ghidra_trace_delmem ghidra trace delmem')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_sync_enable "ghidra_trace_sync_enable"') lldb.debugger.HandleCommand(
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_sync_disable "ghidra_trace_sync_disable"') 'command script add -f ghidralldb.commands.ghidra_trace_putreg ghidra trace putreg')
lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_util_mark "_mark_"') lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_delreg ghidra trace delreg')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_create_obj ghidra trace create-obj')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_insert_obj ghidra trace insert-obj')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_remove_obj ghidra trace remove-obj')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_set_value ghidra trace set-value')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_retain_values ghidra trace retain-values')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_get_obj ghidra trace get_obj')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_get_values ghidra trace get-values')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_get_values_rng ghidra trace get-values-rng')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_activate ghidra trace activate')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_disassemble ghidra trace disassemble')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_processes ghidra trace put-processes')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_available ghidra trace put-available')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_breakpoints ghidra trace put-breakpoints')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_watchpoints ghidra trace put-watchpoints')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_environment ghidra trace put-environment')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_regions ghidra trace put-regions')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_modules ghidra trace put-modules')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_threads ghidra trace put-threads')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_frames ghidra trace put-frames')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_put_all ghidra trace put-all')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_install_hooks ghidra trace install-hooks')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_remove_hooks ghidra trace remove-hooks')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_sync_enable ghidra trace sync-enable')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_trace_sync_disable ghidra trace sync-disable')
lldb.debugger.HandleCommand(
'command script add -f ghidralldb.commands.ghidra_util_mark _mark_')
#lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook') #lldb.debugger.HandleCommand('target stop-hook add -P ghidralldb.hooks.StopHook')
lldb.debugger.SetAsync(True) lldb.debugger.SetAsync(True)
print("Commands loaded.") print("Commands loaded.")
def convert_errors(func):
@functools.wraps(func)
def _func(debugger, command, result, internal_dict):
result.Clear()
try:
func(debugger, command, result, internal_dict)
result.SetStatus(lldb.eReturnStatusSuccessFinishNoResult)
except BaseException as e:
result.SetError(str(e))
return _func
@convert_errors
def ghidra_trace_connect(debugger, command, result, internal_dict): def ghidra_trace_connect(debugger, command, result, internal_dict):
""" """
Connect LLDB to Ghidra for tracing Connect LLDB to Ghidra for tracing
@ -179,10 +247,13 @@ def ghidra_trace_connect(debugger, command, result, internal_dict):
Address must be of the form 'host:port' Address must be of the form 'host:port'
""" """
args = shlex.split(command)
STATE.require_no_client() STATE.require_no_client()
address = command if len(command) > 0 else None if len(args) != 1:
if address is None: raise RuntimeError(
raise RuntimeError("'ghidra_trace_connect': missing required argument 'address'") "ghidra trace connect: missing required argument 'address'")
address = args[0]
parts = address.split(':') parts = address.split(':')
if len(parts) != 2: if len(parts) != 2:
@ -191,11 +262,13 @@ def ghidra_trace_connect(debugger, command, result, internal_dict):
try: try:
c = socket.socket() c = socket.socket()
c.connect((host, int(port))) c.connect((host, int(port)))
STATE.client = Client(c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY) STATE.client = Client(
c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY)
except ValueError: except ValueError:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
@convert_errors
def ghidra_trace_listen(debugger, command, result, internal_dict): def ghidra_trace_listen(debugger, command, result, internal_dict):
""" """
Listen for Ghidra to connect for tracing Listen for Ghidra to connect for tracing
@ -224,15 +297,17 @@ def ghidra_trace_listen(debugger, command, result, internal_dict):
s.bind((host, int(port))) s.bind((host, int(port)))
host, port = s.getsockname() host, port = s.getsockname()
s.listen(1) s.listen(1)
print("Listening at {}:{}...\n".format(host, port)) print(f"Listening at {host}:{port}...")
c, (chost, cport) = s.accept() c, (chost, cport) = s.accept()
s.close() s.close()
print("Connection from {}:{}\n".format(chost, cport)) print(f"Connection from {chost}:{cport}")
STATE.client = Client(c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY) STATE.client = Client(
c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY)
except ValueError: except ValueError:
raise RuntimeError("port must be numeric") raise RuntimeError("port must be numeric")
@convert_errors
def ghidra_trace_disconnect(debugger, command, result, internal_dict): def ghidra_trace_disconnect(debugger, command, result, internal_dict):
"""Disconnect LLDB from Ghidra for tracing""" """Disconnect LLDB from Ghidra for tracing"""
@ -266,17 +341,19 @@ def start_trace(name):
util.set_convenience_variable('_ghidra_tracing', "true") util.set_convenience_variable('_ghidra_tracing', "true")
@convert_errors
def ghidra_trace_start(debugger, command, result, internal_dict): def ghidra_trace_start(debugger, command, result, internal_dict):
"""Start a Trace in Ghidra""" """Start a Trace in Ghidra"""
STATE.require_client() STATE.require_client()
name = command if len(command) > 0 else compute_name() name = command if len(command) > 0 else compute_name()
#if name is None: # if name is None:
# name = compute_name() # name = compute_name()
STATE.require_no_trace() STATE.require_no_trace()
start_trace(name) start_trace(name)
@convert_errors
def ghidra_trace_stop(debugger, command, result, internal_dict): def ghidra_trace_stop(debugger, command, result, internal_dict):
"""Stop the Trace in Ghidra""" """Stop the Trace in Ghidra"""
@ -284,6 +361,7 @@ def ghidra_trace_stop(debugger, command, result, internal_dict):
STATE.reset_trace() STATE.reset_trace()
@convert_errors
def ghidra_trace_restart(debugger, command, result, internal_dict): def ghidra_trace_restart(debugger, command, result, internal_dict):
"""Restart or start the Trace in Ghidra""" """Restart or start the Trace in Ghidra"""
@ -292,27 +370,29 @@ def ghidra_trace_restart(debugger, command, result, internal_dict):
STATE.trace.close() STATE.trace.close()
STATE.reset_trace() STATE.reset_trace()
name = command if len(command) > 0 else compute_name() name = command if len(command) > 0 else compute_name()
#if name is None: # if name is None:
# name = compute_name() # name = compute_name()
start_trace(name) start_trace(name)
@convert_errors
def ghidra_trace_info(debugger, command, result, internal_dict): def ghidra_trace_info(debugger, command, result, internal_dict):
"""Get info about the Ghidra connection""" """Get info about the Ghidra connection"""
result = {} result = {}
if STATE.client is None: if STATE.client is None:
print("Not connected to Ghidra\n") print("Not connected to Ghidra")
return return
host, port = STATE.client.s.getpeername() host, port = STATE.client.s.getpeername()
print("Connected to Ghidra at {}:{}\n".format(host, port)) print(f"Connected to Ghidra at {host}:{port}")
if STATE.trace is None: if STATE.trace is None:
print("No trace\n") print("No trace")
return return
print("Trace active\n") print("Trace active")
return result return result
@convert_errors
def ghidra_trace_info_lcsp(debugger, command, result, internal_dict): def ghidra_trace_info_lcsp(debugger, command, result, internal_dict):
""" """
Get the selected Ghidra language-compiler-spec pair. Even when Get the selected Ghidra language-compiler-spec pair. Even when
@ -321,10 +401,11 @@ def ghidra_trace_info_lcsp(debugger, command, result, internal_dict):
""" """
language, compiler = arch.compute_ghidra_lcsp() language, compiler = arch.compute_ghidra_lcsp()
print("Selected Ghidra language: {}\n".format(language)) print(f"Selected Ghidra language: {language}")
print("Selected Ghidra compiler: {}\n".format(compiler)) print(f"Selected Ghidra compiler: {compiler}")
@convert_errors
def ghidra_trace_txstart(debugger, command, result, internal_dict): def ghidra_trace_txstart(debugger, command, result, internal_dict):
""" """
Start a transaction on the trace Start a transaction on the trace
@ -335,6 +416,7 @@ def ghidra_trace_txstart(debugger, command, result, internal_dict):
STATE.tx = STATE.require_trace().start_tx(description, undoable=False) STATE.tx = STATE.require_trace().start_tx(description, undoable=False)
@convert_errors
def ghidra_trace_txcommit(debugger, command, result, internal_dict): def ghidra_trace_txcommit(debugger, command, result, internal_dict):
""" """
Commit the current transaction Commit the current transaction
@ -344,6 +426,7 @@ def ghidra_trace_txcommit(debugger, command, result, internal_dict):
STATE.reset_tx() STATE.reset_tx()
@convert_errors
def ghidra_trace_txabort(debugger, command, result, internal_dict): def ghidra_trace_txabort(debugger, command, result, internal_dict):
""" """
Abort the current transaction Abort the current transaction
@ -352,7 +435,7 @@ def ghidra_trace_txabort(debugger, command, result, internal_dict):
""" """
tx = STATE.require_tx() tx = STATE.require_tx()
print("Aborting trace transaction!\n") print("Aborting trace transaction!")
tx.abort() tx.abort()
STATE.reset_tx() STATE.reset_tx()
@ -365,37 +448,23 @@ def open_tracked_tx(description):
STATE.reset_tx() STATE.reset_tx()
@convert_errors
def ghidra_trace_txopen(debugger, command, result, internal_dict): def ghidra_trace_txopen(debugger, command, result, internal_dict):
""" """
Run a command with an open transaction Run a command with an open transaction
If possible, use this in the following idiom to ensure your transactions This is generally useful only when executing a single 'put' command, or
are closed: when executing a custom command that performs several puts.
define my-cmd
ghidra_trace_put...
ghidra_trace_put...
end
ghidra_trace_tx-open "My tx" "my-cmd"
If you instead do:
ghidra_trace_tx-start "My tx"
ghidra_trace_put...
ghidra_trace_put...
ghidra_trace_tx-commit
and something goes wrong with one of the puts, the transaction may never be
closed, leading to further crashes when trying to start a new transaction.
""" """
items = command.split(" "); items = command.split(" ")
description = items[0] description = items[0]
command = items[1] command = items[1]
with open_tracked_tx(description): with open_tracked_tx(description):
lldb.debugger.HandleCommand(command); lldb.debugger.HandleCommand(command)
@convert_errors
def ghidra_trace_save(debugger, command, result, internal_dict): def ghidra_trace_save(debugger, command, result, internal_dict):
""" """
Save the current trace Save the current trace
@ -404,6 +473,7 @@ def ghidra_trace_save(debugger, command, result, internal_dict):
STATE.require_trace().save() STATE.require_trace().save()
@convert_errors
def ghidra_trace_new_snap(debugger, command, result, internal_dict): def ghidra_trace_new_snap(debugger, command, result, internal_dict):
""" """
Create a new snapshot Create a new snapshot
@ -413,15 +483,15 @@ def ghidra_trace_new_snap(debugger, command, result, internal_dict):
description = str(command) description = str(command)
STATE.require_tx() STATE.require_tx()
return {'snap': STATE.require_trace().snapshot(description)}
# TODO: A convenience var for the current snapshot # TODO: A convenience var for the current snapshot
# Will need to update it on: # Will need to update it on:
# ghidra_trace_snapshot/set-snap # ghidra trace snapshot/set-snap
# process ? (only if per-process tracing.... I don't think I'm doing that.) # process ? (only if per-process tracing.... I don't think I'm doing that.)
# ghidra_trace_trace start/stop/restart # ghidra trace trace start/stop/restart
@convert_errors
def ghidra_trace_set_snap(debugger, command, result, internal_dict): def ghidra_trace_set_snap(debugger, command, result, internal_dict):
""" """
Go to a snapshot Go to a snapshot
@ -437,15 +507,18 @@ def ghidra_trace_set_snap(debugger, command, result, internal_dict):
STATE.require_trace().set_snap(int(snap)) STATE.require_trace().set_snap(int(snap))
def put_bytes(start, end, pages, from_tty): def quantize_pages(start, end):
return (start // PAGE_SIZE * PAGE_SIZE, (end+PAGE_SIZE-1) // PAGE_SIZE*PAGE_SIZE)
def put_bytes(start, end, result, pages):
trace = STATE.require_trace() trace = STATE.require_trace()
if pages: if pages:
start = start // PAGE_SIZE * PAGE_SIZE start, end = quantize_pages(start, end)
end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
proc = util.get_process() proc = util.get_process()
error = lldb.SBError() error = lldb.SBError()
if end - start <= 0: if end - start <= 0:
return {'count': 0} return
buf = proc.ReadMemory(start, end - start, error) buf = proc.ReadMemory(start, end - start, error)
count = 0 count = 0
@ -454,32 +527,34 @@ def put_bytes(start, end, pages, from_tty):
if base != addr.space: if base != addr.space:
trace.create_overlay_space(base, addr.space) trace.create_overlay_space(base, addr.space)
count = trace.put_bytes(addr, buf) count = trace.put_bytes(addr, buf)
if from_tty: if result is not None:
print("Wrote {} bytes\n".format(count)) result.PutCString(f"Wrote {count} bytes")
return {'count': count} else:
raise RuntimeError(f"Cannot read memory at {start:x}")
def eval_address(address): def eval_address(address):
try: try:
return util.parse_and_eval(address) return util.parse_and_eval(address)
except e: except BaseException as e:
raise RuntimeError("Cannot convert '{}' to address".format(address)) raise RuntimeError(f"Cannot convert '{address}' to address: {e}")
def eval_range(address, length): def eval_range(address, length):
start = eval_address(address) start = eval_address(address)
try: try:
end = start + util.parse_and_eval(length) end = start + util.parse_and_eval(length)
except e: except BaseException as e:
raise RuntimeError("Cannot convert '{}' to length".format(length)) raise RuntimeError(f"Cannot convert '{length}' to length: {e}")
return start, end return start, end
def putmem(address, length, pages=True, from_tty=True): def putmem(address, length, result, pages=True):
start, end = eval_range(address, length) start, end = eval_range(address, length)
return put_bytes(start, end, pages, from_tty) return put_bytes(start, end, result, pages)
@convert_errors
def ghidra_trace_putmem(debugger, command, result, internal_dict): def ghidra_trace_putmem(debugger, command, result, internal_dict):
""" """
Record the given block of memory into the Ghidra trace. Record the given block of memory into the Ghidra trace.
@ -491,9 +566,10 @@ def ghidra_trace_putmem(debugger, command, result, internal_dict):
pages = items[2] if len(items) > 2 else True pages = items[2] if len(items) > 2 else True
STATE.require_tx() STATE.require_tx()
return putmem(address, length, pages, True) return putmem(address, length, result, pages)
@convert_errors
def ghidra_trace_putval(debugger, command, result, internal_dict): def ghidra_trace_putval(debugger, command, result, internal_dict):
""" """
Record the given value into the Ghidra trace, if it's in memory. Record the given value into the Ghidra trace, if it's in memory.
@ -506,12 +582,25 @@ def ghidra_trace_putval(debugger, command, result, internal_dict):
STATE.require_tx() STATE.require_tx()
try: try:
start = util.parse_and_eval(value) start = util.parse_and_eval(value)
except e: except BaseExcepion as e:
raise RuntimeError("Value '{}' has no address".format(value)) raise RuntimeError(f"Value '{value}' has no address: {e}")
end = start + int(start.GetType().GetByteSize()) end = start + int(start.GetType().GetByteSize())
return put_bytes(start, end, pages, True) return put_bytes(start, end, pages, True)
def putmem_state(address, length, state, pages=True):
STATE.trace.validate_state(state)
start, end = eval_range(address, length)
if pages:
start, end = quantize_pages(start, end)
proc = util.get_process()
base, addr = STATE.trace.memory_mapper.map(proc, start)
if base != addr.space:
STATE.trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state)
@convert_errors
def ghidra_trace_putmem_state(debugger, command, result, internal_dict): def ghidra_trace_putmem_state(debugger, command, result, internal_dict):
""" """
Set the state of the given range of memory in the Ghidra trace. Set the state of the given range of memory in the Ghidra trace.
@ -523,15 +612,10 @@ def ghidra_trace_putmem_state(debugger, command, result, internal_dict):
state = items[2] state = items[2]
STATE.require_tx() STATE.require_tx()
STATE.trace.validate_state(state) putmem_state(address, length, state)
start, end = eval_range(address, length)
proc = util.get_process()
base, addr = STATE.trace.memory_mapper.map(proc, start)
if base != addr.space:
trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state)
@convert_errors
def ghidra_trace_delmem(debugger, command, result, internal_dict): def ghidra_trace_delmem(debugger, command, result, internal_dict):
""" """
Delete the given range of memory from the Ghidra trace. Delete the given range of memory from the Ghidra trace.
@ -568,12 +652,14 @@ def putreg(frame, bank):
values = [] values = []
for i in range(0, bank.GetNumChildren()): for i in range(0, bank.GetNumChildren()):
item = bank.GetChildAtIndex(i, lldb.eDynamicCanRunTarget, True) item = bank.GetChildAtIndex(i, lldb.eDynamicCanRunTarget, True)
values.append(mapper.map_value(proc, item.GetName(), item.GetValueAsUnsigned())) values.append(mapper.map_value(
proc, item.GetName(), item.GetValueAsUnsigned()))
bobj.set_value(item.GetName(), hex(item.GetValueAsUnsigned())) bobj.set_value(item.GetName(), hex(item.GetValueAsUnsigned()))
# TODO: Memorize registers that failed for this arch, and omit later. # TODO: Memorize registers that failed for this arch, and omit later.
return {'missing': STATE.trace.put_registers(space, values)} STATE.trace.put_registers(space, values)
@convert_errors
def ghidra_trace_putreg(debugger, command, result, internal_dict): def ghidra_trace_putreg(debugger, command, result, internal_dict):
""" """
Record the given register group for the current frame into the Ghidra trace. Record the given register group for the current frame into the Ghidra trace.
@ -586,7 +672,7 @@ def ghidra_trace_putreg(debugger, command, result, internal_dict):
STATE.require_tx() STATE.require_tx()
frame = util.selected_frame() frame = util.selected_frame()
regs = frame.GetRegisters() regs = frame.GetRegisters()
if group is not 'all': if group != 'all':
bank = regs.GetFirstValueByName(group) bank = regs.GetFirstValueByName(group)
return putreg(frame, bank) return putreg(frame, bank)
@ -595,6 +681,7 @@ def ghidra_trace_putreg(debugger, command, result, internal_dict):
putreg(frame, bank) putreg(frame, bank)
@convert_errors
def ghidra_trace_delreg(debugger, command, result, internal_dict): def ghidra_trace_delreg(debugger, command, result, internal_dict):
""" """
Delete the given register group for the curent frame from the Ghidra trace. Delete the given register group for the curent frame from the Ghidra trace.
@ -607,9 +694,8 @@ def ghidra_trace_delreg(debugger, command, result, internal_dict):
STATE.require_tx() STATE.require_tx()
proc = util.get_process() proc = util.get_process()
frame = util.selected_frame() frame = util.selected_frame()
space = 'Processes[{}].Threads[{}].Stack[{}].Registers'.format( space = REGS_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(),
proc.GetProcessID(), util.selected_thread().GetThreadID(), frame.GetFrameID() level=frame.GetFrameID())
)
mapper = STATE.trace.register_mapper mapper = STATE.trace.register_mapper
names = [] names = []
for desc in frame.registers: for desc in frame.registers:
@ -617,6 +703,7 @@ def ghidra_trace_delreg(debugger, command, result, internal_dict):
return STATE.trace.delete_registers(space, names) return STATE.trace.delete_registers(space, names)
@convert_errors
def ghidra_trace_create_obj(debugger, command, result, internal_dict): def ghidra_trace_create_obj(debugger, command, result, internal_dict):
""" """
Create an object in the Ghidra trace. Create an object in the Ghidra trace.
@ -631,10 +718,10 @@ def ghidra_trace_create_obj(debugger, command, result, internal_dict):
STATE.require_tx() STATE.require_tx()
obj = STATE.trace.create_object(path) obj = STATE.trace.create_object(path)
obj.insert() obj.insert()
print("Created object: id={}, path='{}'\n".format(obj.id, obj.path)) print(f"Created object: id={obj.id}, path='{obj.path}'")
return {'id': obj.id, 'path': obj.path}
@convert_errors
def ghidra_trace_insert_obj(debugger, command, result, internal_dict): def ghidra_trace_insert_obj(debugger, command, result, internal_dict):
""" """
Insert an object into the Ghidra trace. Insert an object into the Ghidra trace.
@ -646,10 +733,10 @@ def ghidra_trace_insert_obj(debugger, command, result, internal_dict):
# humans. # humans.
STATE.require_tx() STATE.require_tx()
span = STATE.trace.proxy_object_path(path).insert() span = STATE.trace.proxy_object_path(path).insert()
print("Inserted object: lifespan={}\n".format(span)) print(f"Inserted object: lifespan={span}")
return {'lifespan': span}
@convert_errors
def ghidra_trace_remove_obj(debugger, command, result, internal_dict): def ghidra_trace_remove_obj(debugger, command, result, internal_dict):
""" """
Remove an object from the Ghidra trace. Remove an object from the Ghidra trace.
@ -668,28 +755,29 @@ def ghidra_trace_remove_obj(debugger, command, result, internal_dict):
def to_bytes(value, type): def to_bytes(value, type):
n = value.GetNumChildren() n = value.GetNumChildren()
return bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)) return bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0, n))
def to_string(value, type, encoding, full): def to_string(value, type, encoding, full):
n = value.GetNumChildren() n = value.GetNumChildren()
b = bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)) b = bytes(int(value.GetChildAtIndex(i).GetValueAsUnsigned())
for i in range(0, n))
return str(b, encoding) return str(b, encoding)
def to_bool_list(value, type): def to_bool_list(value, type):
n = value.GetNumChildren() n = value.GetNumChildren()
return [bool(int(value.GetChildAtIndex(i).GetValueAsUnsigned())) for i in range(0,n)] return [bool(int(value.GetChildAtIndex(i).GetValueAsUnsigned())) for i in range(0, n)]
def to_int_list(value, type): def to_int_list(value, type):
n = value.GetNumChildren() n = value.GetNumChildren()
return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)] return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0, n)]
def to_short_list(value, type): def to_short_list(value, type):
n = value.GetNumChildren() n = value.GetNumChildren()
return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0,n)] return [int(value.GetChildAtIndex(i).GetValueAsUnsigned()) for i in range(0, n)]
def eval_value(value, schema=None): def eval_value(value, schema=None):
@ -763,14 +851,14 @@ def eval_value(value, schema=None):
else: else:
return to_int_list(val, type), sch.LONG_ARR return to_int_list(val, type), sch.LONG_ARR
elif type.IsPointerType(): elif type.IsPointerType():
offset = int(val.GetValue(),16) offset = int(val.GetValue(), 16)
proc = util.get_process() proc = util.get_process()
base, addr = STATE.trace.memory_mapper.map(proc, offset) base, addr = STATE.trace.memory_mapper.map(proc, offset)
return (base, addr), sch.ADDRESS return (base, addr), sch.ADDRESS
raise ValueError( raise ValueError(f"Cannot convert ({schema}): '{value}', value='{val}'")
"Cannot convert ({}): '{}', value='{}'".format(schema, value, val))
@convert_errors
def ghidra_trace_set_value(debugger, command, result, internal_dict): def ghidra_trace_set_value(debugger, command, result, internal_dict):
""" """
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.
@ -792,7 +880,7 @@ def ghidra_trace_set_value(debugger, command, result, internal_dict):
path = items[0] path = items[0]
key = items[1] key = items[1]
value = items[2] value = items[2]
if len(items) > 3 and items[3] is not "": if len(items) > 3 and items[3] != "":
schema = items[3] schema = items[3]
# This is a horrible hack # This is a horrible hack
if (value.startswith("\"") or value.startswith("L\"")) and schema.endswith("\""): if (value.startswith("\"") or value.startswith("L\"")) and schema.endswith("\""):
@ -815,6 +903,7 @@ def ghidra_trace_set_value(debugger, command, result, internal_dict):
STATE.trace.proxy_object_path(path).set_value(key, val, schema) STATE.trace.proxy_object_path(path).set_value(key, val, schema)
@convert_errors
def ghidra_trace_retain_values(debugger, command, result, internal_dict): def ghidra_trace_retain_values(debugger, command, result, internal_dict):
""" """
Retain only those keys listed, settings all others to null. Retain only those keys listed, settings all others to null.
@ -851,6 +940,7 @@ def ghidra_trace_retain_values(debugger, command, result, internal_dict):
STATE.trace.proxy_object_path(path).retain_values(keys, kinds=kinds) STATE.trace.proxy_object_path(path).retain_values(keys, kinds=kinds)
@convert_errors
def ghidra_trace_get_obj(debugger, command, result, internal_dict): def ghidra_trace_get_obj(debugger, command, result, internal_dict):
""" """
Get an object descriptor by its canonical path. Get an object descriptor by its canonical path.
@ -863,7 +953,6 @@ def ghidra_trace_get_obj(debugger, command, result, internal_dict):
trace = STATE.require_trace() trace = STATE.require_trace()
object = trace.get_object(path) object = trace.get_object(path)
print("{}\t{}\n".format(object.id, object.path))
return object return object
@ -881,7 +970,7 @@ class TableColumn(object):
def print_cell(self, i): def print_cell(self, i):
print( print(
self.contents[i] if self.is_last else self.contents[i].ljust(self.width)) self.contents[i] if self.is_last else self.contents[i].ljust(self.width), end='')
class Tabular(object): class Tabular(object):
@ -901,14 +990,14 @@ class Tabular(object):
for rn in range(self.num_rows): for rn in range(self.num_rows):
for c in self.columns: for c in self.columns:
c.print_cell(rn) c.print_cell(rn)
print('\n') print('')
def val_repr(value): def val_repr(value):
if isinstance(value, TraceObject): if isinstance(value, TraceObject):
return value.path return value.path
elif isinstance(value, Address): elif isinstance(value, Address):
return '{}:{:08x}'.format(value.space, value.offset) return f'{value.space}:{value.offset:08x}'
return repr(value) return repr(value)
@ -920,6 +1009,7 @@ def print_values(values):
table.print_table() table.print_table()
@convert_errors
def ghidra_trace_get_values(debugger, command, result, internal_dict): def ghidra_trace_get_values(debugger, command, result, internal_dict):
""" """
List all values matching a given path pattern. List all values matching a given path pattern.
@ -933,6 +1023,7 @@ def ghidra_trace_get_values(debugger, command, result, internal_dict):
return values return values
@convert_errors
def ghidra_trace_get_values_rng(debugger, command, result, internal_dict): def ghidra_trace_get_values_rng(debugger, command, result, internal_dict):
""" """
List all values intersecting a given address range. List all values intersecting a given address range.
@ -962,13 +1053,15 @@ def activate(path=None):
else: else:
frame = util.selected_frame() frame = util.selected_frame()
if frame is None: if frame is None:
path = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID()) path = THREAD_PATTERN.format(
procnum=proc.GetProcessID(), tnum=t.GetThreadID())
else: else:
path = FRAME_PATTERN.format( path = FRAME_PATTERN.format(
procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=frame.GetFrameID()) procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=frame.GetFrameID())
trace.proxy_object_path(path).activate() trace.proxy_object_path(path).activate()
@convert_errors
def ghidra_trace_activate(debugger, command, result, internal_dict): def ghidra_trace_activate(debugger, command, result, internal_dict):
""" """
Activate an object in Ghidra's GUI. Activate an object in Ghidra's GUI.
@ -982,6 +1075,7 @@ def ghidra_trace_activate(debugger, command, result, internal_dict):
activate(path) activate(path)
@convert_errors
def ghidra_trace_disassemble(debugger, command, result, internal_dict): def ghidra_trace_disassemble(debugger, command, result, internal_dict):
""" """
Disassemble starting at the given seed. Disassemble starting at the given seed.
@ -1000,11 +1094,10 @@ def ghidra_trace_disassemble(debugger, command, result, internal_dict):
trace.create_overlay_space(base, addr.space) trace.create_overlay_space(base, addr.space)
length = STATE.trace.disassemble(addr) length = STATE.trace.disassemble(addr)
print("Disassembled {} bytes\n".format(length)) print(f"Disassembled {length} bytes")
return {'length': length}
def compute_proc_state(proc = None): def compute_proc_state(proc=None):
if proc.is_running: if proc.is_running:
return 'RUNNING' return 'RUNNING'
return 'STOPPED' return 'STOPPED'
@ -1021,6 +1114,7 @@ def put_processes():
procobj.insert() procobj.insert()
STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys) STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys)
def put_state(event_process): def put_state(event_process):
STATE.require_no_tx() STATE.require_no_tx()
STATE.tx = STATE.require_trace().start_tx("state", undoable=False) STATE.tx = STATE.require_trace().start_tx("state", undoable=False)
@ -1033,6 +1127,7 @@ def put_state(event_process):
STATE.reset_tx() STATE.reset_tx()
@convert_errors
def ghidra_trace_put_processes(debugger, command, result, internal_dict): def ghidra_trace_put_processes(debugger, command, result, internal_dict):
""" """
Put the list of processes into the trace's Processes list. Put the list of processes into the trace's Processes list.
@ -1050,11 +1145,12 @@ def put_available():
procobj = STATE.trace.create_object(ppath) procobj = STATE.trace.create_object(ppath)
keys.append(AVAILABLE_KEY_PATTERN.format(pid=proc.pid)) keys.append(AVAILABLE_KEY_PATTERN.format(pid=proc.pid))
procobj.set_value('_pid', proc.pid) procobj.set_value('_pid', proc.pid)
procobj.set_value('_display', '{} {}'.format(proc.pid, proc.name)) procobj.set_value('_display', f'{proc.pid} {proc.name}')
procobj.insert() procobj.insert()
STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys) STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys)
@convert_errors
def ghidra_trace_put_available(debugger, command, result, internal_dict): def ghidra_trace_put_available(debugger, command, result, internal_dict):
""" """
Put the list of available processes into the trace's Available list. Put the list of available processes into the trace's Available list.
@ -1078,7 +1174,7 @@ def put_single_breakpoint(b, ibobj, proc, ikeys):
cmdList = lldb.SBStringList() cmdList = lldb.SBStringList()
if b.GetCommandLineCommands(cmdList): if b.GetCommandLineCommands(cmdList):
list = [] list = []
for i in range(0,cmdList.GetSize()): for i in range(0, cmdList.GetSize()):
list.append(cmdList.GetStringAtIndex(i)) list.append(cmdList.GetStringAtIndex(i))
brkobj.set_value('Commands', list) brkobj.set_value('Commands', list)
if b.GetCondition(): if b.GetCondition():
@ -1108,9 +1204,11 @@ def put_single_breakpoint(b, ibobj, proc, ikeys):
brkobj.retain_values(keys) brkobj.retain_values(keys)
brkobj.insert() brkobj.insert()
def put_single_watchpoint(b, ibobj, proc, ikeys): def put_single_watchpoint(b, ibobj, proc, ikeys):
mapper = STATE.trace.memory_mapper mapper = STATE.trace.memory_mapper
bpath = PROC_WATCH_KEY_PATTERN.format(procnum=proc.GetProcessID(), watchnum=b.GetID()) bpath = PROC_WATCH_KEY_PATTERN.format(
procnum=proc.GetProcessID(), watchnum=b.GetID())
brkobj = STATE.trace.create_object(bpath) brkobj = STATE.trace.create_object(bpath)
desc = util.get_description(b, level=0) desc = util.get_description(b, level=0)
brkobj.set_value('_expression', desc) brkobj.set_value('_expression', desc)
@ -1148,6 +1246,7 @@ def put_breakpoints():
STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys) STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys)
ibobj.retain_values(ikeys) ibobj.retain_values(ikeys)
def put_watchpoints(): def put_watchpoints():
target = util.get_target() target = util.get_target()
proc = util.get_process() proc = util.get_process()
@ -1163,6 +1262,7 @@ def put_watchpoints():
STATE.trace.proxy_object_path(WATCHPOINTS_PATH).retain_values(keys) STATE.trace.proxy_object_path(WATCHPOINTS_PATH).retain_values(keys)
@convert_errors
def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict): def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict):
""" """
Put the current process's breakpoints into the trace. Put the current process's breakpoints into the trace.
@ -1172,6 +1272,8 @@ def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict):
with STATE.client.batch() as b: with STATE.client.batch() as b:
put_breakpoints() put_breakpoints()
@convert_errors
def ghidra_trace_put_watchpoints(debugger, command, result, internal_dict): def ghidra_trace_put_watchpoints(debugger, command, result, internal_dict):
""" """
Put the current process's watchpoints into the trace. Put the current process's watchpoints into the trace.
@ -1193,6 +1295,7 @@ def put_environment():
envobj.insert() envobj.insert()
@convert_errors
def ghidra_trace_put_environment(debugger, command, result, internal_dict): def ghidra_trace_put_environment(debugger, command, result, internal_dict):
""" """
Put some environment indicators into the Ghidra trace Put some environment indicators into the Ghidra trace
@ -1214,7 +1317,8 @@ def put_regions():
mapper = STATE.trace.memory_mapper mapper = STATE.trace.memory_mapper
keys = [] keys = []
for r in regions: for r in regions:
rpath = REGION_PATTERN.format(procnum=proc.GetProcessID(), start=r.start) rpath = REGION_PATTERN.format(
procnum=proc.GetProcessID(), start=r.start)
keys.append(REGION_KEY_PATTERN.format(start=r.start)) keys.append(REGION_KEY_PATTERN.format(start=r.start))
regobj = STATE.trace.create_object(rpath) regobj = STATE.trace.create_object(rpath)
start_base, start_addr = mapper.map(proc, r.start) start_base, start_addr = mapper.map(proc, r.start)
@ -1231,6 +1335,7 @@ def put_regions():
MEMORY_PATTERN.format(procnum=proc.GetProcessID())).retain_values(keys) MEMORY_PATTERN.format(procnum=proc.GetProcessID())).retain_values(keys)
@convert_errors
def ghidra_trace_put_regions(debugger, command, result, internal_dict): def ghidra_trace_put_regions(debugger, command, result, internal_dict):
""" """
Read the memory map, if applicable, and write to the trace's Regions Read the memory map, if applicable, and write to the trace's Regions
@ -1278,6 +1383,7 @@ def put_modules():
procnum=proc.GetProcessID())).retain_values(mod_keys) procnum=proc.GetProcessID())).retain_values(mod_keys)
@convert_errors
def ghidra_trace_put_modules(debugger, command, result, internal_dict): def ghidra_trace_put_modules(debugger, command, result, internal_dict):
""" """
Gather object files, if applicable, and write to the trace's Modules Gather object files, if applicable, and write to the trace's Modules
@ -1289,6 +1395,7 @@ def ghidra_trace_put_modules(debugger, command, result, internal_dict):
def convert_state(t): def convert_state(t):
# TODO: This does not seem to work - currently supplanted by proc.is_running
if t.IsSuspended(): if t.IsSuspended():
return 'SUSPENDED' return 'SUSPENDED'
if t.IsStopped(): if t.IsStopped():
@ -1320,17 +1427,18 @@ def put_threads():
proc = util.get_process() proc = util.get_process()
keys = [] keys = []
for t in proc.threads: for t in proc.threads:
tpath = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID()) tpath = THREAD_PATTERN.format(
procnum=proc.GetProcessID(), tnum=t.GetThreadID())
tobj = STATE.trace.create_object(tpath) tobj = STATE.trace.create_object(tpath)
keys.append(THREAD_KEY_PATTERN.format(tnum=t.GetThreadID())) keys.append(THREAD_KEY_PATTERN.format(tnum=t.GetThreadID()))
tobj.set_value('_state', convert_state(t)) #tobj.set_value('_state', convert_state(t))
tobj.set_value('_state', compute_proc_state(proc))
tobj.set_value('_name', t.GetName()) tobj.set_value('_name', t.GetName())
tid = t.GetThreadID() tid = t.GetThreadID()
tobj.set_value('_tid', tid) tobj.set_value('_tid', tid)
tidstr = ('0x{:x}' if radix == tidstr = f'0x{tid:x}' if radix == 16 else f'0{tid:o}' if radix == 8 else f'{tid}'
16 else '0{:o}' if radix == 8 else '{}').format(tid) tobj.set_value('_short_display',
tobj.set_value('_short_display', '[{}.{}:{}]'.format( f'[{proc.GetProcessID()}.{t.GetThreadID()}:{tidstr}]')
proc.GetProcessID(), t.GetThreadID(), tidstr))
tobj.set_value('_display', compute_thread_display(t)) tobj.set_value('_display', compute_thread_display(t))
tobj.insert() tobj.insert()
STATE.trace.proxy_object_path( STATE.trace.proxy_object_path(
@ -1342,13 +1450,15 @@ def put_event_thread():
# Assumption: Event thread is selected by lldb upon stopping # Assumption: Event thread is selected by lldb upon stopping
t = util.selected_thread() t = util.selected_thread()
if t is not None: if t is not None:
tpath = THREAD_PATTERN.format(procnum=proc.GetProcessID(), tnum=t.GetThreadID()) tpath = THREAD_PATTERN.format(
procnum=proc.GetProcessID(), tnum=t.GetThreadID())
tobj = STATE.trace.proxy_object_path(tpath) tobj = STATE.trace.proxy_object_path(tpath)
else: else:
tobj = None tobj = None
STATE.trace.proxy_object_path('').set_value('_event_thread', tobj) STATE.trace.proxy_object_path('').set_value('_event_thread', tobj)
@convert_errors
def ghidra_trace_put_threads(debugger, command, result, internal_dict): def ghidra_trace_put_threads(debugger, command, result, internal_dict):
""" """
Put the current process's threads into the Ghidra trace Put the current process's threads into the Ghidra trace
@ -1366,7 +1476,7 @@ def put_frames():
if t is None: if t is None:
return return
keys = [] keys = []
for i in range(0,t.GetNumFrames()): for i in range(0, t.GetNumFrames()):
f = t.GetFrameAtIndex(i) f = t.GetFrameAtIndex(i)
fpath = FRAME_PATTERN.format( fpath = FRAME_PATTERN.format(
procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=f.GetFrameID()) procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=f.GetFrameID())
@ -1383,6 +1493,7 @@ def put_frames():
procnum=proc.GetProcessID(), tnum=t.GetThreadID())).retain_values(keys) procnum=proc.GetProcessID(), tnum=t.GetThreadID())).retain_values(keys)
@convert_errors
def ghidra_trace_put_frames(debugger, command, result, internal_dict): def ghidra_trace_put_frames(debugger, command, result, internal_dict):
""" """
Put the current thread's frames into the Ghidra trace Put the current thread's frames into the Ghidra trace
@ -1393,6 +1504,7 @@ def ghidra_trace_put_frames(debugger, command, result, internal_dict):
put_frames() put_frames()
@convert_errors
def ghidra_trace_put_all(debugger, command, result, internal_dict): def ghidra_trace_put_all(debugger, command, result, internal_dict):
""" """
Put everything currently selected into the Ghidra trace Put everything currently selected into the Ghidra trace
@ -1400,7 +1512,8 @@ def ghidra_trace_put_all(debugger, command, result, internal_dict):
STATE.require_tx() STATE.require_tx()
with STATE.client.batch() as b: with STATE.client.batch() as b:
ghidra_trace_putreg(debugger, DEFAULT_REGISTER_BANK, result, internal_dict) ghidra_trace_putreg(debugger, DEFAULT_REGISTER_BANK,
result, internal_dict)
ghidra_trace_putmem(debugger, "$pc 1", result, internal_dict) ghidra_trace_putmem(debugger, "$pc 1", result, internal_dict)
ghidra_trace_putmem(debugger, "$sp 1", result, internal_dict) ghidra_trace_putmem(debugger, "$sp 1", result, internal_dict)
put_processes() put_processes()
@ -1414,6 +1527,7 @@ def ghidra_trace_put_all(debugger, command, result, internal_dict):
put_available() put_available()
@convert_errors
def ghidra_trace_install_hooks(debugger, command, result, internal_dict): def ghidra_trace_install_hooks(debugger, command, result, internal_dict):
""" """
Install hooks to trace in Ghidra Install hooks to trace in Ghidra
@ -1422,6 +1536,7 @@ def ghidra_trace_install_hooks(debugger, command, result, internal_dict):
hooks.install_hooks() hooks.install_hooks()
@convert_errors
def ghidra_trace_remove_hooks(debugger, command, result, internal_dict): def ghidra_trace_remove_hooks(debugger, command, result, internal_dict):
""" """
Remove hooks to trace in Ghidra Remove hooks to trace in Ghidra
@ -1434,6 +1549,7 @@ def ghidra_trace_remove_hooks(debugger, command, result, internal_dict):
hooks.remove_hooks() hooks.remove_hooks()
@convert_errors
def ghidra_trace_sync_enable(debugger, command, result, internal_dict): def ghidra_trace_sync_enable(debugger, command, result, internal_dict):
""" """
Synchronize the current process with the Ghidra trace Synchronize the current process with the Ghidra trace
@ -1453,6 +1569,7 @@ def ghidra_trace_sync_enable(debugger, command, result, internal_dict):
hooks.enable_current_process() hooks.enable_current_process()
@convert_errors
def ghidra_trace_sync_disable(debugger, command, result, internal_dict): def ghidra_trace_sync_disable(debugger, command, result, internal_dict):
""" """
Cease synchronizing the current process with the Ghidra trace Cease synchronizing the current process with the Ghidra trace
@ -1464,6 +1581,7 @@ def ghidra_trace_sync_disable(debugger, command, result, internal_dict):
hooks.disable_current_process() hooks.disable_current_process()
@convert_errors
def ghidra_util_wait_stopped(debugger, command, result, internal_dict): def ghidra_util_wait_stopped(debugger, command, result, internal_dict):
""" """
Spin wait until the selected thread is stopped. Spin wait until the selected thread is stopped.
@ -1483,5 +1601,6 @@ def ghidra_util_wait_stopped(debugger, command, result, internal_dict):
raise RuntimeError('Timed out waiting for thread to stop') raise RuntimeError('Timed out waiting for thread to stop')
@convert_errors
def ghidra_util_mark(debugger, command, result, internal_dict): def ghidra_util_mark(debugger, command, result, internal_dict):
print(command) print(command)

View file

@ -13,15 +13,17 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
## ##
import time
import threading import threading
import time
import lldb import lldb
from . import commands, util from . import commands, util
ALL_EVENTS = 0xFFFF ALL_EVENTS = 0xFFFF
class HookState(object): class HookState(object):
__slots__ = ('installed', 'mem_catchpoint') __slots__ = ('installed', 'mem_catchpoint')
@ -31,7 +33,8 @@ class HookState(object):
class ProcessState(object): class ProcessState(object):
__slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'watches', 'visited') __slots__ = ('first', 'regions', 'modules', 'threads',
'breaks', 'watches', 'visited')
def __init__(self): def __init__(self):
self.first = True self.first = True
@ -64,9 +67,10 @@ class ProcessState(object):
hashable_frame = (thread.GetThreadID(), frame.GetFrameID()) hashable_frame = (thread.GetThreadID(), frame.GetFrameID())
if first or hashable_frame not in self.visited: if first or hashable_frame not in self.visited:
banks = frame.GetRegisters() banks = frame.GetRegisters()
commands.putreg(frame, banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK)) commands.putreg(frame, banks.GetFirstValueByName(
commands.putmem("$pc", "1", from_tty=False) commands.DEFAULT_REGISTER_BANK))
commands.putmem("$sp", "1", from_tty=False) commands.putmem("$pc", "1", result=None)
commands.putmem("$sp", "1", result=None)
self.visited.add(hashable_frame) self.visited.add(hashable_frame)
if first or self.regions or self.threads or self.modules: if first or self.regions or self.threads or self.modules:
# Sections, memory syscalls, or stack allocations # Sections, memory syscalls, or stack allocations
@ -117,10 +121,11 @@ HOOK_STATE = HookState()
BRK_STATE = BrkState() BRK_STATE = BrkState()
PROC_STATE = {} PROC_STATE = {}
def process_event(self, listener, event): def process_event(self, listener, event):
try: try:
desc = util.get_description(event) desc = util.get_description(event)
#event_process = lldb.SBProcess_GetProcessFromEvent(event) # print('Event:', desc)
event_process = util.get_process() event_process = util.get_process()
if event_process not in PROC_STATE: if event_process not in PROC_STATE:
PROC_STATE[event_process.GetProcessID()] = ProcessState() PROC_STATE[event_process.GetProcessID()] = ProcessState()
@ -128,35 +133,29 @@ def process_event(self, listener, event):
if rc is False: if rc is False:
print("add listener for process failed") print("add listener for process failed")
# NB: Calling put_state on running leaves an open transaction
if event_process.is_running is False:
commands.put_state(event_process) commands.put_state(event_process)
type = event.GetType() type = event.GetType()
if lldb.SBTarget.EventIsTargetEvent(event): if lldb.SBTarget.EventIsTargetEvent(event):
print('Event:', desc)
if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0: if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0:
print("eBroadcastBitBreakpointChanged")
return on_breakpoint_modified(event) return on_breakpoint_modified(event)
if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0: if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0:
print("eBroadcastBitWatchpointChanged")
return on_watchpoint_modified(event) return on_watchpoint_modified(event)
if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0: if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0:
print("eBroadcastBitModulesLoaded")
return on_new_objfile(event) return on_new_objfile(event)
if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0: if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0:
print("eBroadcastBitModulesUnloaded")
return on_free_objfile(event) return on_free_objfile(event)
if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0: if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0:
print("eBroadcastBitSymbolsLoaded")
return True return True
if lldb.SBProcess.EventIsProcessEvent(event): if lldb.SBProcess.EventIsProcessEvent(event):
if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0: if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0:
print("eBroadcastBitStateChanged")
if not event_process.is_alive: if not event_process.is_alive:
return on_exited(event) return on_exited(event)
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
return True return True
if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0: if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0:
print("eBroadcastBitInterrupt")
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0: if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0:
@ -164,133 +163,95 @@ def process_event(self, listener, event):
if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0: if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0:
return True return True
if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0: if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0:
print("eBroadcastBitProfileData")
return True return True
if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0: if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0:
print("eBroadcastBitStructuredData")
return True return True
# NB: Thread events not currently processes # NB: Thread events not currently processes
if lldb.SBThread.EventIsThreadEvent(event): if lldb.SBThread.EventIsThreadEvent(event):
print('Event:', desc)
if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0: if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0:
print("eBroadcastBitStackChanged")
return on_frame_selected() return on_frame_selected()
if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0: if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0:
print("eBroadcastBitThreadSuspended")
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0: if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0:
print("eBroadcastBitThreadResumed")
return on_cont(event) return on_cont(event)
if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0: if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0:
print("eBroadcastBitSelectedFrameChanged")
return on_frame_selected() return on_frame_selected()
if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0: if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0:
print("eBroadcastBitThreadSelected")
return on_thread_selected() return on_thread_selected()
if lldb.SBBreakpoint.EventIsBreakpointEvent(event): if lldb.SBBreakpoint.EventIsBreakpointEvent(event):
print('Event:', desc) btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event)
btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event); bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event)
bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event);
if btype is lldb.eBreakpointEventTypeAdded: if btype is lldb.eBreakpointEventTypeAdded:
print("eBreakpointEventTypeAdded")
return on_breakpoint_created(bpt) return on_breakpoint_created(bpt)
if btype is lldb.eBreakpointEventTypeAutoContinueChanged: if btype is lldb.eBreakpointEventTypeAutoContinueChanged:
print("elldb.BreakpointEventTypeAutoContinueChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeCommandChanged: if btype is lldb.eBreakpointEventTypeCommandChanged:
print("eBreakpointEventTypeCommandChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeConditionChanged: if btype is lldb.eBreakpointEventTypeConditionChanged:
print("eBreakpointEventTypeConditionChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeDisabled: if btype is lldb.eBreakpointEventTypeDisabled:
print("eBreakpointEventTypeDisabled")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeEnabled: if btype is lldb.eBreakpointEventTypeEnabled:
print("eBreakpointEventTypeEnabled")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeIgnoreChanged: if btype is lldb.eBreakpointEventTypeIgnoreChanged:
print("eBreakpointEventTypeIgnoreChanged")
return True return True
if btype is lldb.eBreakpointEventTypeInvalidType: if btype is lldb.eBreakpointEventTypeInvalidType:
print("eBreakpointEventTypeInvalidType")
return True return True
if btype is lldb.eBreakpointEventTypeLocationsAdded: if btype is lldb.eBreakpointEventTypeLocationsAdded:
print("eBreakpointEventTypeLocationsAdded")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeLocationsRemoved: if btype is lldb.eBreakpointEventTypeLocationsRemoved:
print("eBreakpointEventTypeLocationsRemoved")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeLocationsResolved: if btype is lldb.eBreakpointEventTypeLocationsResolved:
print("eBreakpointEventTypeLocationsResolved")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeRemoved: if btype is lldb.eBreakpointEventTypeRemoved:
print("eBreakpointEventTypeRemoved")
return on_breakpoint_deleted(bpt) return on_breakpoint_deleted(bpt)
if btype is lldb.eBreakpointEventTypeThreadChanged: if btype is lldb.eBreakpointEventTypeThreadChanged:
print("eBreakpointEventTypeThreadChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
print("UNKNOWN BREAKPOINT EVENT") print("UNKNOWN BREAKPOINT EVENT")
return True return True
if lldb.SBWatchpoint.EventIsWatchpointEvent(event): if lldb.SBWatchpoint.EventIsWatchpointEvent(event):
print('Event:', desc) btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event)
btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event); bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt)
bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt);
if btype is lldb.eWatchpointEventTypeAdded: if btype is lldb.eWatchpointEventTypeAdded:
print("eWatchpointEventTypeAdded")
return on_watchpoint_added(bpt) return on_watchpoint_added(bpt)
if btype is lldb.eWatchpointEventTypeCommandChanged: if btype is lldb.eWatchpointEventTypeCommandChanged:
print("eWatchpointEventTypeCommandChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeConditionChanged: if btype is lldb.eWatchpointEventTypeConditionChanged:
print("eWatchpointEventTypeConditionChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeDisabled: if btype is lldb.eWatchpointEventTypeDisabled:
print("eWatchpointEventTypeDisabled")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeEnabled: if btype is lldb.eWatchpointEventTypeEnabled:
print("eWatchpointEventTypeEnabled")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeIgnoreChanged: if btype is lldb.eWatchpointEventTypeIgnoreChanged:
print("eWatchpointEventTypeIgnoreChanged")
return True return True
if btype is lldb.eWatchpointEventTypeInvalidType: if btype is lldb.eWatchpointEventTypeInvalidType:
print("eWatchpointEventTypeInvalidType")
return True return True
if btype is lldb.eWatchpointEventTypeRemoved: if btype is lldb.eWatchpointEventTypeRemoved:
print("eWatchpointEventTypeRemoved")
return on_watchpoint_deleted(bpt) return on_watchpoint_deleted(bpt)
if btype is lldb.eWatchpointEventTypeThreadChanged: if btype is lldb.eWatchpointEventTypeThreadChanged:
print("eWatchpointEventTypeThreadChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeTypeChanged: if btype is lldb.eWatchpointEventTypeTypeChanged:
print("eWatchpointEventTypeTypeChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
print("UNKNOWN WATCHPOINT EVENT") print("UNKNOWN WATCHPOINT EVENT")
return True return True
if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event): if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event):
print('Event:', desc)
if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0:
print("eBroadcastBitAsynchronousErrorData")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0:
print("eBroadcastBitAsynchronousOutputData")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0:
print("eBroadcastBitQuitCommandReceived")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0:
print("eBroadcastBitResetPrompt")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0:
print("eBroadcastBitThreadShouldExit")
return True return True
print("UNKNOWN EVENT") print("UNKNOWN EVENT")
return True return True
except RuntimeError as e: except RuntimeError as e:
print(e) print(e)
class EventThread(threading.Thread): class EventThread(threading.Thread):
func = process_event func = process_event
event = lldb.SBEvent() event = lldb.SBEvent()
@ -329,7 +290,8 @@ class EventThread(threading.Thread):
print("add listener for process failed") print("add listener for process failed")
return return
rc = listener.StartListeningForEventClass(util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS) rc = listener.StartListeningForEventClass(
util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS)
if rc is False: if rc is False:
print("add listener for threads failed") print("add listener for threads failed")
return return
@ -344,13 +306,14 @@ class EventThread(threading.Thread):
while listener.GetNextEvent(self.event): while listener.GetNextEvent(self.event):
self.func(listener, self.event) self.func(listener, self.event)
event_recvd = True event_recvd = True
except Exception as e: except BaseException as e:
print(e) print(e)
proc = util.get_process() proc = util.get_process()
if proc is not None and not proc.is_alive: if proc is not None and not proc.is_alive:
break break
return return
""" """
# Not sure if this is possible in LLDB... # Not sure if this is possible in LLDB...
@ -475,7 +438,7 @@ def on_memory_changed(event):
with commands.STATE.client.batch(): with commands.STATE.client.batch():
with trace.open_tx("Memory *0x{:08x} changed".format(event.address)): with trace.open_tx("Memory *0x{:08x} changed".format(event.address)):
commands.put_bytes(event.address, event.address + event.length, commands.put_bytes(event.address, event.address + event.length,
pages=False, is_mi=False, from_tty=False) pages=False, is_mi=False, result=None)
def on_register_changed(event): def on_register_changed(event):
@ -547,11 +510,13 @@ def on_exited(event):
commands.put_event_thread() commands.put_event_thread()
commands.activate() commands.activate()
def notify_others_breaks(proc): def notify_others_breaks(proc):
for num, state in PROC_STATE.items(): for num, state in PROC_STATE.items():
if num != proc.GetProcessID(): if num != proc.GetProcessID():
state.breaks = True state.breaks = True
def notify_others_watches(proc): def notify_others_watches(proc):
for num, state in PROC_STATE.items(): for num, state in PROC_STATE.items():
if num != proc.GetProcessID(): if num != proc.GetProcessID():
@ -697,6 +662,7 @@ def remove_hooks():
return return
HOOK_STATE.installed = False HOOK_STATE.installed = False
def enable_current_process(): def enable_current_process():
proc = util.get_process() proc = util.get_process()
PROC_STATE[proc.GetProcessID()] = ProcessState() PROC_STATE[proc.GetProcessID()] = ProcessState()

View file

@ -18,7 +18,6 @@ import re
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
import lldb import lldb
from . import commands, util from . import commands, util
@ -66,9 +65,7 @@ def find_proc_by_num(procnum):
def find_proc_by_pattern(object, pattern, err_msg): def find_proc_by_pattern(object, pattern, err_msg):
print(object.path)
mat = pattern.fullmatch(object.path) mat = pattern.fullmatch(object.path)
print(mat)
if mat is None: if mat is None:
raise TypeError(f"{object} is not {err_msg}") raise TypeError(f"{object} is not {err_msg}")
procnum = int(mat['procnum']) procnum = int(mat['procnum'])
@ -83,6 +80,7 @@ def find_proc_by_procbreak_obj(object):
return find_proc_by_pattern(object, PROC_BREAKS_PATTERN, return find_proc_by_pattern(object, PROC_BREAKS_PATTERN,
"a BreakpointLocationContainer") "a BreakpointLocationContainer")
def find_proc_by_procwatch_obj(object): def find_proc_by_procwatch_obj(object):
return find_proc_by_pattern(object, PROC_WATCHES_PATTERN, return find_proc_by_pattern(object, PROC_WATCHES_PATTERN,
"a WatchpointContainer") "a WatchpointContainer")
@ -108,7 +106,8 @@ def find_thread_by_num(proc, tnum):
for t in proc.threads: for t in proc.threads:
if t.GetThreadID() == tnum: if t.GetThreadID() == tnum:
return t return t
raise KeyError(f"Processes[{proc.GetProcessID()}].Threads[{tnum}] does not exist") raise KeyError(
f"Processes[{proc.GetProcessID()}].Threads[{tnum}] does not exist")
def find_thread_by_pattern(pattern, object, err_msg): def find_thread_by_pattern(pattern, object, err_msg):
@ -166,7 +165,7 @@ def find_reg_by_name(f, name):
# I could keep my own cache in a dict, but why? # I could keep my own cache in a dict, but why?
def find_bpt_by_number(breaknum): def find_bpt_by_number(breaknum):
# TODO: If len exceeds some threshold, use binary search? # TODO: If len exceeds some threshold, use binary search?
for i in range(0,util.get_target().GetNumBreakpoints()): for i in range(0, util.get_target().GetNumBreakpoints()):
b = util.get_target().GetBreakpointAtIndex(i) b = util.get_target().GetBreakpointAtIndex(i)
if b.GetID() == breaknum: if b.GetID() == breaknum:
return b return b
@ -189,7 +188,7 @@ def find_bpt_by_obj(object):
# I could keep my own cache in a dict, but why? # I could keep my own cache in a dict, but why?
def find_wpt_by_number(watchnum): def find_wpt_by_number(watchnum):
# TODO: If len exceeds some threshold, use binary search? # TODO: If len exceeds some threshold, use binary search?
for i in range(0,util.get_target().GetNumWatchpoints()): for i in range(0, util.get_target().GetNumWatchpoints()):
w = util.get_target().GetWatchpointAtIndex(i) w = util.get_target().GetWatchpointAtIndex(i)
if w.GetID() == watchnum: if w.GetID() == watchnum:
return w return w
@ -203,6 +202,7 @@ def find_wpt_by_pattern(pattern, object, err_msg):
watchnum = int(mat['watchnum']) watchnum = int(mat['watchnum'])
return find_wpt_by_number(watchnum) return find_wpt_by_number(watchnum)
def find_wpt_by_obj(object): def find_wpt_by_obj(object):
return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec") return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec")
@ -244,7 +244,7 @@ def execute(cmd: str, to_string: bool=False):
def refresh_available(node: sch.Schema('AvailableContainer')): def refresh_available(node: sch.Schema('AvailableContainer')):
"""List processes on lldb's host system.""" """List processes on lldb's host system."""
with commands.open_tracked_tx('Refresh Available'): with commands.open_tracked_tx('Refresh Available'):
util.get_debugger().HandleCommand('ghidra_trace_put_available') util.get_debugger().HandleCommand('ghidra trace put-available')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -254,14 +254,14 @@ def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
process). process).
""" """
with commands.open_tracked_tx('Refresh Breakpoints'): with commands.open_tracked_tx('Refresh Breakpoints'):
util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints') util.get_debugger().HandleCommand('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_processes(node: sch.Schema('ProcessContainer')): def refresh_processes(node: sch.Schema('ProcessContainer')):
"""Refresh the list of processes.""" """Refresh the list of processes."""
with commands.open_tracked_tx('Refresh Processes'): with commands.open_tracked_tx('Refresh Processes'):
util.get_debugger().HandleCommand('ghidra_trace_put_threads') util.get_debugger().HandleCommand('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -273,7 +273,7 @@ def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
refreshed. refreshed.
""" """
with commands.open_tracked_tx('Refresh Breakpoint Locations'): with commands.open_tracked_tx('Refresh Breakpoint Locations'):
util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints'); util.get_debugger().HandleCommand('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -285,20 +285,21 @@ def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')):
refreshed. refreshed.
""" """
with commands.open_tracked_tx('Refresh Watchpoint Locations'): with commands.open_tracked_tx('Refresh Watchpoint Locations'):
util.get_debugger().HandleCommand('ghidra_trace_put_watchpoints'); util.get_debugger().HandleCommand('ghidra trace put-watchpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_environment(node: sch.Schema('Environment')): def refresh_environment(node: sch.Schema('Environment')):
"""Refresh the environment descriptors (arch, os, endian).""" """Refresh the environment descriptors (arch, os, endian)."""
with commands.open_tracked_tx('Refresh Environment'): with commands.open_tracked_tx('Refresh Environment'):
util.get_debugger().HandleCommand('ghidra_trace_put_environment') util.get_debugger().HandleCommand('ghidra trace put-environment')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_threads(node: sch.Schema('ThreadContainer')): def refresh_threads(node: sch.Schema('ThreadContainer')):
"""Refresh the list of threads in the process.""" """Refresh the list of threads in the process."""
with commands.open_tracked_tx('Refresh Threads'): with commands.open_tracked_tx('Refresh Threads'):
util.get_debugger().HandleCommand('ghidra_trace_put_threads') util.get_debugger().HandleCommand('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -307,7 +308,7 @@ def refresh_stack(node: sch.Schema('Stack')):
t = find_thread_by_stack_obj(node) t = find_thread_by_stack_obj(node)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
with commands.open_tracked_tx('Refresh Stack'): with commands.open_tracked_tx('Refresh Stack'):
util.get_debugger().HandleCommand('ghidra_trace_put_frames'); util.get_debugger().HandleCommand('ghidra trace put-frames')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -317,14 +318,14 @@ def refresh_registers(node: sch.Schema('RegisterValueContainer')):
f.thread.SetSelectedFrame(f.GetFrameID()) f.thread.SetSelectedFrame(f.GetFrameID())
# TODO: Groups? # TODO: Groups?
with commands.open_tracked_tx('Refresh Registers'): with commands.open_tracked_tx('Refresh Registers'):
util.get_debugger().HandleCommand('ghidra_trace_putreg'); util.get_debugger().HandleCommand('ghidra trace putreg')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_mappings(node: sch.Schema('Memory')): def refresh_mappings(node: sch.Schema('Memory')):
"""Refresh the list of memory regions for the process.""" """Refresh the list of memory regions for the process."""
with commands.open_tracked_tx('Refresh Memory Regions'): with commands.open_tracked_tx('Refresh Memory Regions'):
util.get_debugger().HandleCommand('ghidra_trace_put_regions'); util.get_debugger().HandleCommand('ghidra trace put-regions')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -335,7 +336,7 @@ def refresh_modules(node: sch.Schema('ModuleContainer')):
This will refresh the sections for all modules, not just the selected one. This will refresh the sections for all modules, not just the selected one.
""" """
with commands.open_tracked_tx('Refresh Modules'): with commands.open_tracked_tx('Refresh Modules'):
util.get_debugger().HandleCommand('ghidra_trace_put_modules'); util.get_debugger().HandleCommand('ghidra trace put-modules')
@REGISTRY.method(action='activate') @REGISTRY.method(action='activate')
@ -343,6 +344,7 @@ def activate_process(process: sch.Schema('Process')):
"""Switch to the process.""" """Switch to the process."""
return return
@REGISTRY.method(action='activate') @REGISTRY.method(action='activate')
def activate_thread(thread: sch.Schema('Thread')): def activate_thread(thread: sch.Schema('Thread')):
"""Switch to the thread.""" """Switch to the thread."""
@ -376,11 +378,13 @@ def attach_obj(process: sch.Schema('Process'), target: sch.Schema('Attachable'))
pid = find_availpid_by_obj(target) pid = find_availpid_by_obj(target)
util.get_debugger().HandleCommand(f'process attach -p {pid}') util.get_debugger().HandleCommand(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach')
def attach_pid(process: sch.Schema('Process'), pid: int): def attach_pid(process: sch.Schema('Process'), pid: int):
"""Attach the process to the given target.""" """Attach the process to the given target."""
util.get_debugger().HandleCommand(f'process attach -p {pid}') util.get_debugger().HandleCommand(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach')
def attach_name(process: sch.Schema('Process'), name: str): def attach_name(process: sch.Schema('Process'), name: str):
"""Attach the process to the given target.""" """Attach the process to the given target."""
@ -403,8 +407,9 @@ def launch_loader(process: sch.Schema('Process'),
If 'main' is not defined in the file, this behaves like 'run'. If 'main' is not defined in the file, this behaves like 'run'.
""" """
util.get_debugger().HandleCommand(f'file {file}') util.get_debugger().HandleCommand(f'file {file}')
if args is not '': if args != '':
util.get_debugger().HandleCommand(f'settings set target.run-args {args}') util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'process launch --stop-at-entry') util.get_debugger().HandleCommand(f'process launch --stop-at-entry')
@ -419,8 +424,9 @@ def launch(process: sch.Schema('Process'),
signaled. signaled.
""" """
util.get_debugger().HandleCommand(f'file {file}') util.get_debugger().HandleCommand(f'file {file}')
if args is not '': if args != '':
util.get_debugger().HandleCommand(f'settings set target.run-args {args}') util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'run') util.get_debugger().HandleCommand(f'run')
@ -440,9 +446,9 @@ def _continue(process: sch.Schema('Process')):
def interrupt(): def interrupt():
"""Interrupt the execution of the debugged program.""" """Interrupt the execution of the debugged program."""
util.get_debugger().HandleCommand('process interrupt') util.get_debugger().HandleCommand('process interrupt')
#util.get_process().SendAsyncInterrupt() # util.get_process().SendAsyncInterrupt()
#util.get_debugger().HandleCommand('^c') # util.get_debugger().HandleCommand('^c')
#util.get_process().Signal(2) # util.get_process().Signal(2)
@REGISTRY.method(action='step_into') @REGISTRY.method(action='step_into')
@ -527,13 +533,15 @@ def break_read_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -w read -- {offset_start}')
@REGISTRY.method(action='break_read') @REGISTRY.method(action='break_read')
def break_read_expression(expression: str): def break_read_expression(expression: str):
"""Set a read watchpoint.""" """Set a read watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -w read -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -w read -- {expression}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
@ -543,13 +551,15 @@ def break_write_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -- {offset_start}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
def break_write_expression(expression: str): def break_write_expression(expression: str):
"""Set a watchpoint.""" """Set a watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -- {expression}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
@ -559,13 +569,15 @@ def break_access_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read_write -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -w read_write -- {offset_start}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
def break_access_expression(expression: str): def break_access_expression(expression: str):
"""Set an access watchpoint.""" """Set an access watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -w read_write -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -w read_write -- {expression}')
@REGISTRY.method(action='break_ext') @REGISTRY.method(action='break_ext')
@ -580,12 +592,14 @@ def toggle_watchpoint(breakpoint: sch.Schema('WatchpointSpec'), enabled: bool):
wpt = find_wpt_by_obj(watchpoint) wpt = find_wpt_by_obj(watchpoint)
wpt.enabled = enabled wpt.enabled = enabled
@REGISTRY.method(action='toggle') @REGISTRY.method(action='toggle')
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool): def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint.""" """Toggle a breakpoint."""
bpt = find_bpt_by_obj(breakpoint) bpt = find_bpt_by_obj(breakpoint)
bpt.enabled = enabled bpt.enabled = enabled
@REGISTRY.method(action='toggle') @REGISTRY.method(action='toggle')
def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool): def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
"""Toggle a breakpoint location.""" """Toggle a breakpoint location."""
@ -601,6 +615,7 @@ def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
wptnum = wpt.GetID() wptnum = wpt.GetID()
util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}') util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}')
@REGISTRY.method(action='delete') @REGISTRY.method(action='delete')
def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')): def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint.""" """Delete a breakpoint."""
@ -615,8 +630,16 @@ def read_mem(process: sch.Schema('Process'), range: AddressRange):
proc = find_proc_by_obj(process) proc = find_proc_by_obj(process)
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
ci = util.get_debugger().GetCommandInterpreter()
with commands.open_tracked_tx('Read Memory'): with commands.open_tracked_tx('Read Memory'):
util.get_debugger().HandleCommand(f'ghidra_trace_putmem 0x{offset_start:x} {range.length()}') result = lldb.SBCommandReturnObject()
ci.HandleCommand(
f'ghidra trace putmem 0x{offset_start:x} {range.length()}', result)
if result.Succeeded():
return
print(f"Could not read 0x{offset_start:x}: {result}")
util.get_debugger().HandleCommand(
f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error')
@REGISTRY.method @REGISTRY.method
@ -628,7 +651,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes):
@REGISTRY.method @REGISTRY.method
def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes): def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
"""Write a register.""" """Write a register."""
f = find_frame_by_obj(frame) f = find_frame_by_obj(frame)
f.select() f.select()
@ -637,4 +660,5 @@ def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
reg = find_reg_by_name(f, mname) reg = find_reg_by_name(f, mname)
size = int(lldb.parse_and_eval(f'sizeof(${mname})')) size = int(lldb.parse_and_eval(f'sizeof(${mname})'))
arr = '{' + ','.join(str(b) for b in mval) + '}' arr = '{' + ','.join(str(b) for b in mval) + '}'
util.get_debugger().HandleCommand(f'expr ((unsigned char[{size}])${mname}) = {arr};') util.get_debugger().HandleCommand(
f'expr ((unsigned char[{size}])${mname}) = {arr};')

View file

@ -1,46 +0,0 @@
## ###
# 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.
##
import lldb
# TODO: I don't know how to register a custom parameter prefix. I would rather
# these were 'ghidra language' and 'ghidra compiler'
class GhidraLanguageParameter(lldb.Parameter):
"""
The language id for Ghidra traces. Set this to 'auto' to try to derive it
from 'show arch' and 'show endian'. Otherwise, set it to a Ghidra
LanguageID.
"""
def __init__(self):
super().__init__('ghidra-language', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraLanguageParameter()
class GhidraCompilerParameter(lldb.Parameter):
"""
The compiler spec id for Ghidra traces. Set this to 'auto' to try to derive
it from 'show osabi'. Otherwise, set it to a Ghidra CompilerSpecID. Note
that valid compiler spec ids depend on the language id.
"""
def __init__(self):
super().__init__('ghidra-compiler', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraCompilerParameter()

View file

@ -27,7 +27,10 @@ LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor'])
def _compute_lldb_ver(): def _compute_lldb_ver():
blurb = lldb.debugger.GetVersionString() blurb = lldb.debugger.GetVersionString()
top = blurb.split('\n')[0] top = blurb.split('\n')[0]
full = top.split(' ')[2] if ' version ' in top:
full = top.split(' ')[2] # "lldb version x.y.z"
else:
full = top.split('-')[1] # "lldb-x.y.z"
major, minor = full.split('.')[:2] major, minor = full.split('.')[:2]
return LldbVersion(full, int(major), int(minor)) return LldbVersion(full, int(major), int(minor))
@ -36,6 +39,7 @@ LLDB_VERSION = _compute_lldb_ver()
GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for " GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for "
class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])): class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])):
pass pass
@ -107,8 +111,8 @@ class ModuleInfoReader(object):
def _choose_module_info_reader(): def _choose_module_info_reader():
return ModuleInfoReader() return ModuleInfoReader()
MODULE_INFO_READER = _choose_module_info_reader()
MODULE_INFO_READER = _choose_module_info_reader()
class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])): class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])):
@ -137,8 +141,8 @@ class RegionInfoReader(object):
reglist = get_process().GetMemoryRegions() reglist = get_process().GetMemoryRegions()
for i in range(0, reglist.GetSize()): for i in range(0, reglist.GetSize()):
module = get_target().GetModuleAtIndex(i) module = get_target().GetModuleAtIndex(i)
info = lldb.SBMemoryRegionInfo(); info = lldb.SBMemoryRegionInfo()
success = reglist.GetMemoryRegionAtIndex(i, info); success = reglist.GetMemoryRegionAtIndex(i, info)
if success: if success:
r = self.region_from_sbmemreg(info) r = self.region_from_sbmemreg(info)
regions.append(r) regions.append(r)
@ -177,28 +181,39 @@ def _choose_breakpoint_location_info_reader():
BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader() BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader()
def get_debugger(): def get_debugger():
return lldb.SBDebugger.FindDebuggerWithID(1) return lldb.SBDebugger.FindDebuggerWithID(1)
def get_target(): def get_target():
return get_debugger().GetTargetAtIndex(0) return get_debugger().GetTargetAtIndex(0)
def get_process(): def get_process():
return get_target().GetProcess() return get_target().GetProcess()
def selected_thread(): def selected_thread():
return get_process().GetSelectedThread() return get_process().GetSelectedThread()
def selected_frame(): def selected_frame():
return selected_thread().GetSelectedFrame() return selected_thread().GetSelectedFrame()
def parse_and_eval(expr, signed=False): def parse_and_eval(expr, signed=False):
if signed is True: if signed is True:
return get_target().EvaluateExpression(expr).GetValueAsSigned() return get_eval(expr).GetValueAsSigned()
return get_target().EvaluateExpression(expr).GetValueAsUnsigned() return get_eval(expr).GetValueAsUnsigned()
def get_eval(expr): def get_eval(expr):
return get_target().EvaluateExpression(expr) eval = get_target().EvaluateExpression(expr)
if eval.GetError().Fail():
raise ValueError(eval.GetError().GetCString())
return eval
def get_description(object, level=None): def get_description(object, level=None):
stream = lldb.SBStream() stream = lldb.SBStream()
@ -208,8 +223,10 @@ def get_description(object, level=None):
object.GetDescription(stream, level) object.GetDescription(stream, level)
return escape_ansi(stream.GetData()) return escape_ansi(stream.GetData())
conv_map = {} conv_map = {}
def get_convenience_variable(id): def get_convenience_variable(id):
#val = get_target().GetEnvironment().Get(id) #val = get_target().GetEnvironment().Get(id)
if id not in conv_map: if id not in conv_map:
@ -219,18 +236,20 @@ def get_convenience_variable(id):
return "auto" return "auto"
return val return val
def set_convenience_variable(id, value): def set_convenience_variable(id, value):
#env = get_target().GetEnvironment() #env = get_target().GetEnvironment()
#return env.Set(id, value, True) # return env.Set(id, value, True)
conv_map[id] = value conv_map[id] = value
def escape_ansi(line): def escape_ansi(line):
ansi_escape =re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
return ansi_escape.sub('', line) return ansi_escape.sub('', line)
def debracket(init): def debracket(init):
val = init val = init
val = val.replace("[","(") val = val.replace("[", "(")
val = val.replace("]",")") val = val.replace("]", ")")
return val return val

View file

@ -37,6 +37,13 @@ public interface TraceRmiAcceptor {
*/ */
TraceRmiConnection accept() throws IOException, CancelledException; TraceRmiConnection accept() throws IOException, CancelledException;
/**
* Check if the acceptor is actually still accepting.
*
* @return true if not accepting anymore
*/
boolean isClosed();
/** /**
* Get the address (and port) where the acceptor is listening * Get the address (and port) where the acceptor is listening
* *

View file

@ -57,16 +57,30 @@ public interface TraceRmiLaunchOffer {
* @param exception optional error, if failed * @param exception optional error, if failed
*/ */
public record LaunchResult(Program program, Map<String, TerminalSession> sessions, public record LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiConnection connection, Trace trace, Throwable exception) TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
implements AutoCloseable { Throwable exception) implements AutoCloseable {
public LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
Throwable exception) {
this.program = program;
this.sessions = sessions;
this.acceptor = acceptor == null || acceptor.isClosed() ? null : acceptor;
this.connection = connection;
this.trace = trace;
this.exception = exception;
}
@Override @Override
public void close() throws Exception { public void close() throws Exception {
for (TerminalSession s : sessions.values()) {
s.close();
}
if (connection != null) { if (connection != null) {
connection.close(); connection.close();
} }
if (acceptor != null) {
acceptor.cancel();
}
for (TerminalSession s : sessions.values()) {
s.close();
}
} }
} }

View file

@ -27,7 +27,7 @@ public class RunBashInTerminalScript extends TerminalGhidraScript {
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color"); env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
displayInTerminal(pty.getParent(), () -> { displayInTerminal(pty, () -> {
try { try {
session.waitExited(); session.waitExited();
} }

View file

@ -37,14 +37,16 @@ public class TerminalGhidraScript extends GhidraScript {
return state.getTool().getService(TerminalService.class); return state.getTool().getService(TerminalService.class);
} }
protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException { protected void displayInTerminal(Pty pty, Runnable waiter) throws PluginException {
TerminalService terminalService = ensureTerminalService(); TerminalService terminalService = ensureTerminalService();
PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
waiter.run(); waiter.run();
@ -55,7 +57,7 @@ public class TerminalGhidraScript extends GhidraScript {
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color"); env.put("TERM", "xterm-256color");
pty.getChild().nullSession(); pty.getChild().nullSession();
displayInTerminal(pty.getParent(), () -> { displayInTerminal(pty, () -> {
while (true) { while (true) {
try { try {
Thread.sleep(100000); Thread.sleep(100000);

View file

@ -441,6 +441,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Pty pty = factory.openpty(); Pty pty = factory.openpty();
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream()); parent.getInputStream(), parent.getOutputStream());
terminal.setSubTitle(ShellUtils.generateLine(commandLine)); terminal.setSubTitle(ShellUtils.generateLine(commandLine));
@ -448,7 +449,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
try { try {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
catch (Exception e) { catch (Exception e) {
Msg.error(this, "Could not resize pty: " + e); Msg.error(this, "Could not resize pty: " + e);
@ -490,12 +491,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Pty pty = factory.openpty(); Pty pty = factory.openpty();
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream()); parent.getInputStream(), parent.getOutputStream());
TerminalListener resizeListener = new TerminalListener() { TerminalListener resizeListener = new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}; };
terminal.addTerminalListener(resizeListener); terminal.addTerminalListener(resizeListener);
@ -549,7 +551,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
if (lastExc == null) { if (lastExc == null) {
lastExc = new CancelledException(); lastExc = new CancelledException();
} }
return new LaunchResult(program, sessions, connection, trace, lastExc); return new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
} }
acceptor = null; acceptor = null;
sessions.clear(); sessions.clear();
@ -598,10 +600,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
} }
} }
catch (Exception e) { catch (Exception e) {
DebuggerConsoleService consoleService =
tool.getService(DebuggerConsoleService.class);
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
"Launch %s Failed".formatted(getTitle()), e);
}
lastExc = e; lastExc = e;
prompt = mode != PromptMode.NEVER; prompt = mode != PromptMode.NEVER;
LaunchResult result = LaunchResult result =
new LaunchResult(program, sessions, connection, trace, lastExc); new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
if (prompt) { if (prompt) {
switch (promptError(result)) { switch (promptError(result)) {
case KEEP: case KEEP:
@ -621,13 +629,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
catch (Exception e1) { catch (Exception e1) {
Msg.error(this, "Could not close", e1); Msg.error(this, "Could not close", e1);
} }
return new LaunchResult(program, Map.of(), null, null, lastExc); return new LaunchResult(program, Map.of(), null, null, null, lastExc);
} }
continue; continue;
} }
return result; return result;
} }
return new LaunchResult(program, sessions, connection, trace, null); return new LaunchResult(program, sessions, null, connection, trace, null);
} }
} }
@ -702,21 +710,25 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) { for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue(); TerminalSession session = ent.getValue();
sb.append("<li>Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " &rarr; <tt>" + sb.append("<li>Terminal: %s &rarr; <tt>%s</tt>".formatted(
HTMLUtilities.escapeHTML(session.description()) + "</tt>"); HTMLUtilities.escapeHTML(ent.getKey()),
HTMLUtilities.escapeHTML(session.description())));
if (session.isTerminated()) { if (session.isTerminated()) {
sb.append(" (Terminated)"); sb.append(" (Terminated)");
} }
sb.append("</li>\n"); sb.append("</li>\n");
} }
if (result.acceptor() != null) {
sb.append("<li>Acceptor: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString())));
}
if (result.connection() != null) { if (result.connection() != null) {
sb.append("<li>Connection: <tt>" + sb.append("<li>Connection: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) + HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString())));
"</tt></li>\n");
} }
if (result.trace() != null) { if (result.trace() != null) {
sb.append( sb.append("<li>Trace: %s</li>\n".formatted(
"<li>Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "</li>\n"); HTMLUtilities.escapeHTML(result.trace().getName())));
} }
return sb.toString(); return sb.toString();
} }

View file

@ -62,6 +62,11 @@ public class DefaultTraceRmiAcceptor extends AbstractTraceRmiListener implements
} }
} }
@Override
public boolean isClosed() {
return socket.isClosed();
}
@Override @Override
public void close() { public void close() {
plugin.removeAcceptor(this); plugin.removeAcceptor(this);

View file

@ -310,7 +310,11 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
PropertyEditor editor = getEditor(param); PropertyEditor editor = getEditor(param);
Object val = computeMemorizedValue(param); Object val = computeMemorizedValue(param);
if (val == null) {
editor.setValue("");
} else {
editor.setValue(val); editor.setValue(val);
}
editor.addPropertyChangeListener(this); editor.addPropertyChangeListener(this);
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
paramEditors.put(param, editor); paramEditors.put(param, editor);

View file

@ -28,8 +28,8 @@ dependencies {
api project(':SoftwareModeling') api project(':SoftwareModeling')
api project(':ProposedUtils') api project(':ProposedUtils')
api "net.java.dev.jna:jna:5.4.0" api "net.java.dev.jna:jna:5.14.0"
api "net.java.dev.jna:jna-platform:5.4.0" api "net.java.dev.jna:jna-platform:5.14.0"
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
} }

View file

@ -101,4 +101,12 @@ public interface PtyChild extends PtyEndpoint {
default String nullSession(TermMode... mode) throws IOException { default String nullSession(TermMode... mode) throws IOException {
return nullSession(List.of(mode)); return nullSession(List.of(mode));
} }
/**
* Resize the terminal window to the given width and height, in characters
*
* @param cols the width in characters
* @param rows the height in characters
*/
void setWindowSize(short cols, short rows);
} }

View file

@ -19,11 +19,4 @@ package ghidra.pty;
* The parent (UNIX "master") end of a pseudo-terminal * The parent (UNIX "master") end of a pseudo-terminal
*/ */
public interface PtyParent extends PtyEndpoint { public interface PtyParent extends PtyEndpoint {
/**
* Resize the terminal window to the given width and height, in characters
*
* @param cols the width in characters
* @param rows the height in characters
*/
void setWindowSize(short cols, short rows);
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty; package ghidra.pty;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** /**
* A session led by the child pty * A session led by the child pty
* *
@ -31,6 +34,8 @@ public interface PtySession {
*/ */
int waitExited() throws InterruptedException; int waitExited() throws InterruptedException;
int waitExited(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException;
/** /**
* Take the greatest efforts to terminate the session (leader and descendants) * Take the greatest efforts to terminate the session (leader and descendants)
* *

View file

@ -15,20 +15,24 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import ghidra.pty.PtyParent; import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.linux.PosixC.Winsize; import ghidra.pty.unix.UnixPtySessionLeader;
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent { public enum LinuxIoctls implements Ioctls {
LinuxPtyParent(int fd) { INSTANCE;
super(fd);
@Override
public Class<? extends UnixPtySessionLeader> leaderClass() {
return LinuxPtySessionLeader.class;
} }
@Override @Override
public void setWindowSize(short cols, short rows) { public long TIOCSCTTY() {
Winsize.ByReference ws = new Winsize.ByReference(); return 0x540eL;
ws.ws_col = cols; }
ws.ws_row = rows;
ws.write(); @Override
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer()); public long TIOCSWINSZ() {
return 0x5414L;
} }
} }

View file

@ -19,15 +19,16 @@ import java.io.IOException;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.unix.UnixPty;
public enum LinuxPtyFactory implements PtyFactory { public enum LinuxPtyFactory implements PtyFactory {
INSTANCE; INSTANCE;
@Override @Override
public Pty openpty(short cols, short rows) throws IOException { public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty(); UnixPty pty = UnixPty.openpty(LinuxIoctls.INSTANCE);
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }

View file

@ -15,11 +15,10 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import java.util.List; import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public class LinuxPtySessionLeader { public class LinuxPtySessionLeader extends UnixPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
LinuxPtySessionLeader leader = new LinuxPtySessionLeader(); LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
@ -27,61 +26,8 @@ public class LinuxPtySessionLeader {
leader.run(); leader.run();
} }
protected String ptyPath; @Override
protected List<String> subArgs; protected Ioctls ioctls() {
return LinuxIoctls.INSTANCE;
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
// Print to both redirected and to inherited stderr
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
} }
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.local; package ghidra.pty.local;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -36,6 +39,15 @@ public class LocalProcessPtySession implements PtySession {
return process.waitFor(); return process.waitFor();
} }
@Override
public int waitExited(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
if (!process.waitFor(timeout, unit)) {
throw new TimeoutException();
}
return process.exitValue();
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
process.destroyForcibly(); process.destroyForcibly();

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.local; package ghidra.pty.local;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.sun.jna.LastErrorException; import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase; import com.sun.jna.platform.win32.WinBase;
@ -42,10 +45,9 @@ public class LocalWindowsNativeProcessPtySession implements PtySession {
Msg.info(this, "local Windows Pty session. PID = " + pid); Msg.info(this, "local Windows Pty session. PID = " + pid);
} }
@Override protected int doWaitExited(int millis) throws TimeoutException {
public int waitExited() throws InterruptedException {
while (true) { while (true) {
switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) { switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), millis)) {
case Kernel32.WAIT_OBJECT_0: case Kernel32.WAIT_OBJECT_0:
case Kernel32.WAIT_ABANDONED: case Kernel32.WAIT_ABANDONED:
IntByReference lpExitCode = new IntByReference(); IntByReference lpExitCode = new IntByReference();
@ -54,13 +56,32 @@ public class LocalWindowsNativeProcessPtySession implements PtySession {
return lpExitCode.getValue(); return lpExitCode.getValue();
} }
case Kernel32.WAIT_TIMEOUT: case Kernel32.WAIT_TIMEOUT:
throw new AssertionError(); throw new TimeoutException();
case Kernel32.WAIT_FAILED: case Kernel32.WAIT_FAILED:
throw new LastErrorException(Kernel32.INSTANCE.GetLastError()); throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
} }
} }
} }
@Override
public int waitExited() {
try {
return doWaitExited(-1);
}
catch (TimeoutException e) {
throw new AssertionError(e);
}
}
@Override
public int waitExited(long timeout, TimeUnit unit) throws TimeoutException {
long millis = TimeUnit.MILLISECONDS.convert(timeout, unit);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Too long a timeout");
}
return doWaitExited((int) millis);
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) { if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) {

View file

@ -0,0 +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.
*/
package ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public enum MacosIoctls implements Ioctls {
INSTANCE;
@Override
public Class<? extends UnixPtySessionLeader> leaderClass() {
return MacosPtySessionLeader.class;
}
@Override
public long TIOCSCTTY() {
return 0x20007461L;
}
@Override
public long TIOCSWINSZ() {
return 0x80087467L;
}
}

View file

@ -19,22 +19,22 @@ import java.io.IOException;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.unix.UnixPty;
public enum MacosPtyFactory implements PtyFactory { public enum MacosPtyFactory implements PtyFactory {
INSTANCE; INSTANCE;
@Override @Override
public Pty openpty(short cols, short rows) throws IOException { public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty(); UnixPty pty = UnixPty.openpty(MacosIoctls.INSTANCE);
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }
@Override @Override
public String getDescription() { public String getDescription() {
return "local (MacOS)"; return "local (macOS)";
} }
} }

View file

@ -0,0 +1,33 @@
/* ###
* 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 ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public class MacosPtySessionLeader extends UnixPtySessionLeader {
public static void main(String[] args) throws Exception {
MacosPtySessionLeader leader = new MacosPtySessionLeader();
leader.parseArgs(args);
leader.run();
}
@Override
protected Ioctls ioctls() {
return MacosIoctls.INSTANCE;
}
}

View file

@ -234,7 +234,7 @@ public class GhidraSshPtyFactory implements PtyFactory {
try { try {
SshPty pty = new SshPty((ChannelExec) session.openChannel("exec")); SshPty pty = new SshPty((ChannelExec) session.openChannel("exec"));
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }

View file

@ -118,4 +118,8 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
public OutputStream getOutputStream() { public OutputStream getOutputStream() {
throw new UnsupportedOperationException("The child is not local"); throw new UnsupportedOperationException("The child is not local");
} }
@Override
public void setWindowSize(short cols, short rows) {
channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0);
}
} }

View file

@ -26,9 +26,4 @@ public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(channel, outputStream, inputStream); super(channel, outputStream, inputStream);
} }
@Override
public void setWindowSize(short cols, short rows) {
channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0);
}
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.ssh; package ghidra.pty.ssh;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.jcraft.jsch.*; import com.jcraft.jsch.*;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
@ -27,16 +30,37 @@ public class SshPtySession implements PtySession {
this.channel = channel; this.channel = channel;
} }
@Override protected int doWaitExited(Long millis) throws InterruptedException, TimeoutException {
public int waitExited() throws InterruptedException { long startMs = System.currentTimeMillis();
// Doesn't look like there's a clever way to wait. So do the spin sleep :( // Doesn't look like there's a clever way to wait. So do the spin sleep :(
while (!channel.isEOF()) { while (!channel.isEOF()) {
Thread.sleep(1000); Thread.sleep(100);
long elapsed = System.currentTimeMillis() - startMs;
if (millis != null && elapsed > millis) {
throw new TimeoutException();
}
} }
// NB. May not be available // NB. May not be available
return channel.getExitStatus(); return channel.getExitStatus();
} }
@Override
public int waitExited() throws InterruptedException {
try {
return doWaitExited(null);
}
catch (TimeoutException e) {
throw new AssertionError(e);
}
}
@Override
public int waitExited(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
long millis = TimeUnit.MILLISECONDS.convert(timeout, unit);
return doWaitExited(millis);
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
channel.disconnect(); channel.disconnect();

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.LastErrorException; import com.sun.jna.LastErrorException;
import com.sun.jna.Native; import com.sun.jna.Native;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.*; import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder; import com.sun.jna.Structure.FieldOrder;
@ -26,10 +26,16 @@ import com.sun.jna.Structure.FieldOrder;
*/ */
public interface PosixC extends Library { public interface PosixC extends Library {
interface Ioctls {
Class<? extends UnixPtySessionLeader> leaderClass();
long TIOCSCTTY();
long TIOCSWINSZ();
}
@FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" }) @FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" })
class Winsize extends Structure { class Winsize extends Structure {
public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific
public short ws_row; public short ws_row;
public short ws_col; public short ws_col;
public short ws_xpixel; // Unused public short ws_xpixel; // Unused
@ -39,11 +45,21 @@ public interface PosixC extends Library {
} }
} }
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed", @FieldOrder({ "steal" })
"c_ospeed" }) class ControllingTty extends Structure {
class Termios extends Structure { public int steal;
public static final int TCSANOW = 0;
public static class ByReference extends ControllingTty implements Structure.ByReference {
}
}
@FieldOrder(
{ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
"c_ospeed" }
)
class Termios extends Structure {
// TCSANOW and ECHO are the same on Linux and macOS
public static final int TCSANOW = 0;
public static final int ECHO = 0000010; // Octal public static final int ECHO = 0000010; // Octal
public int c_iflag; public int c_iflag;
@ -110,7 +126,7 @@ public interface PosixC extends Library {
} }
@Override @Override
public int ioctl(int fd, int cmd, Pointer... args) { public int ioctl(int fd, long cmd, Pointer... args) {
return Err.checkLt0(BARE.ioctl(fd, cmd, args)); return Err.checkLt0(BARE.ioctl(fd, cmd, args));
} }
@ -141,7 +157,7 @@ public interface PosixC extends Library {
int execv(String path, String[] argv); int execv(String path, String[] argv);
int ioctl(int fd, int cmd, Pointer... args); int ioctl(int fd, long cmd, Pointer... args);
int tcgetattr(int fd, Termios.ByReference termios_p); int tcgetattr(int fd, Termios.ByReference termios_p);

View file

@ -13,53 +13,53 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import com.sun.jna.*; import com.sun.jna.Memory;
import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.IntByReference;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.util.Msg; import ghidra.util.Msg;
public class LinuxPty implements Pty { public class UnixPty implements Pty {
static final PosixC LIB_POSIX = PosixC.INSTANCE; static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final int aparent; private final int aparent;
private final int achild; private final int achild;
//private final String name;
private boolean closed = false; private boolean closed = false;
private final LinuxPtyParent parent; private final UnixPtyParent parent;
private final LinuxPtyChild child; private final UnixPtyChild child;
public static LinuxPty openpty() throws IOException { public static UnixPty openpty(Ioctls ioctls) throws IOException {
// TODO: Support termp and winp? // TODO: Support termp and winp?
IntByReference p = new IntByReference(); IntByReference p = new IntByReference();
IntByReference c = new IntByReference(); IntByReference c = new IntByReference();
Memory n = new Memory(1024); Memory n = new Memory(1024);
Util.INSTANCE.openpty(p, c, n, null, null); Util.INSTANCE.openpty(p, c, n, null, null);
return new LinuxPty(p.getValue(), c.getValue(), n.getString(0)); return new UnixPty(ioctls, p.getValue(), c.getValue(), n.getString(0));
} }
LinuxPty(int aparent, int achild, String name) { UnixPty(Ioctls ioctls, int aparent, int achild, String name) {
Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")"); Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")");
this.aparent = aparent; this.aparent = aparent;
this.achild = achild; this.achild = achild;
this.parent = new LinuxPtyParent(aparent); this.parent = new UnixPtyParent(ioctls, aparent);
this.child = new LinuxPtyChild(achild, name); this.child = new UnixPtyChild(ioctls, achild, name);
} }
@Override @Override
public LinuxPtyParent getParent() { public UnixPtyParent getParent() {
return parent; return parent;
} }
@Override @Override
public LinuxPtyChild getChild() { public UnixPtyChild getChild() {
return child; return child;
} }

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -21,17 +21,17 @@ import java.util.*;
import ghidra.pty.PtyChild; import ghidra.pty.PtyChild;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.pty.linux.PosixC.Termios;
import ghidra.pty.local.LocalProcessPtySession; import ghidra.pty.local.LocalProcessPtySession;
import ghidra.pty.unix.PosixC.*;
import ghidra.util.Msg; import ghidra.util.Msg;
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { public class UnixPtyChild extends UnixPtyEndpoint implements PtyChild {
static final PosixC LIB_POSIX = PosixC.INSTANCE; static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final String name; private final String name;
LinuxPtyChild(int fd, String name) { UnixPtyChild(Ioctls ioctls, int fd, String name) {
super(fd); super(ioctls, fd);
this.name = name; this.name = name;
} }
@ -70,7 +70,7 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
argsList.add(javaCommand); argsList.add(javaCommand);
argsList.add("-cp"); argsList.add("-cp");
argsList.add(System.getProperty("java.class.path")); argsList.add(System.getProperty("java.class.path"));
argsList.add(LinuxPtySessionLeader.class.getCanonicalName()); argsList.add(ioctls.leaderClass().getCanonicalName());
argsList.add(name); argsList.add(name);
argsList.addAll(Arrays.asList(args)); argsList.addAll(Arrays.asList(args));
@ -106,4 +106,18 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
tmios.c_lflag &= ~Termios.ECHO; tmios.c_lflag &= ~Termios.ECHO;
LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios); LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios);
} }
@Override
public void setWindowSize(short cols, short rows) {
Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = cols;
ws.ws_row = rows;
ws.write();
try {
PosixC.INSTANCE.ioctl(fd, ioctls.TIOCSWINSZ(), ws.getPointer());
}
catch (Exception e) {
Msg.error(this, "Could not set terminal window size: " + e);
}
}
} }

View file

@ -13,19 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import ghidra.pty.PtyEndpoint; import ghidra.pty.PtyEndpoint;
import ghidra.pty.unix.PosixC.Ioctls;
public class LinuxPtyEndpoint implements PtyEndpoint { public class UnixPtyEndpoint implements PtyEndpoint {
protected final Ioctls ioctls;
protected final int fd; protected final int fd;
private final FdOutputStream outputStream; private final FdOutputStream outputStream;
private final FdInputStream inputStream; private final FdInputStream inputStream;
LinuxPtyEndpoint(int fd) { UnixPtyEndpoint(Ioctls ioctls, int fd) {
this.ioctls = ioctls;
this.fd = fd; this.fd = fd;
this.outputStream = new FdOutputStream(fd); this.outputStream = new FdOutputStream(fd);
this.inputStream = new FdInputStream(fd); this.inputStream = new FdInputStream(fd);

View file

@ -0,0 +1,25 @@
/* ###
* 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 ghidra.pty.unix;
import ghidra.pty.PtyParent;
import ghidra.pty.unix.PosixC.Ioctls;
public class UnixPtyParent extends UnixPtyEndpoint implements PtyParent {
UnixPtyParent(Ioctls ioctls, int fd) {
super(ioctls, fd);
}
}

View file

@ -0,0 +1,93 @@
/* ###
* 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 ghidra.pty.unix;
import java.util.List;
import ghidra.pty.unix.PosixC.ControllingTty;
import ghidra.pty.unix.PosixC.Ioctls;
public abstract class UnixPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
protected String ptyPath;
protected List<String> subArgs;
protected abstract Ioctls ioctls();
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.close(0);
LIB_POSIX.close(1);
LIB_POSIX.close(2);
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
LIB_POSIX.close(fd);
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
ControllingTty.ByReference ctty = new ControllingTty.ByReference();
ctty.steal = 0;
LIB_POSIX.ioctl(0, ioctls().TIOCSCTTY(), ctty.getPointer());
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
// Print to both redirected and to inherited stderr
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.*; import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.IntByReference;

View file

@ -111,4 +111,9 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
public String nullSession(Collection<TermMode> mode) throws IOException { public String nullSession(Collection<TermMode> mode) throws IOException {
throw new UnsupportedOperationException("ConPTY does not have a name"); throw new UnsupportedOperationException("ConPTY does not have a name");
} }
@Override
public void setWindowSize(short cols, short rows) {
pseudoConsoleHandle.resize(rows, cols);
}
} }

View file

@ -22,9 +22,4 @@ public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
PseudoConsoleHandle pseudoConsoleHandle) { PseudoConsoleHandle pseudoConsoleHandle) {
super(writeHandle, readHandle, pseudoConsoleHandle); super(writeHandle, readHandle, pseudoConsoleHandle);
} }
@Override
public void setWindowSize(short cols, short rows) {
pseudoConsoleHandle.resize(rows, cols);
}
} }

View file

@ -15,201 +15,24 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
import java.io.*; import java.io.IOException;
import java.util.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem; import ghidra.framework.OperatingSystem;
import ghidra.pty.AbstractPtyTest; import ghidra.pty.unix.AbstractUnixPtyTest;
import ghidra.pty.PtyChild.Echo; import ghidra.pty.unix.UnixPty;
import ghidra.pty.PtySession;
public class LinuxPtyTest extends AbstractPtyTest { public class LinuxPtyTest extends AbstractUnixPtyTest {
@Before @Before
public void checkLinux() { public void checkLinux() {
assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM); assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM);
} }
@Test @Override
public void testOpenClosePty() throws IOException { protected UnixPty openpty() throws IOException {
LinuxPty pty = LinuxPty.openpty(); return UnixPty.openpty(LinuxIoctls.INSTANCE);
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException {
try (LinuxPty pty = LinuxPty.openpty()) {
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited());
}
}
@Test
public void testForkIntoNonExistent() throws IOException, InterruptedException {
try (LinuxPty pty = LinuxPty.openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited());
}
}
@Test
public void testSessionBashEchoTest() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testSessionBashInterruptCat() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
} }
} }

View file

@ -0,0 +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.
*/
package ghidra.pty.macos;
import static org.junit.Assume.assumeTrue;
import java.io.IOException;
import org.junit.Before;
import ghidra.framework.OperatingSystem;
import ghidra.pty.unix.AbstractUnixPtyTest;
import ghidra.pty.unix.UnixPty;
public class MacosPtyTest extends AbstractUnixPtyTest {
@Before
public void checkLinux() {
assumeTrue(OperatingSystem.MAC_OS_X == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Override
protected UnixPty openpty() throws IOException {
return UnixPty.openpty(MacosIoctls.INSTANCE);
}
}

View file

@ -0,0 +1,215 @@
/* ###
* 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 ghidra.pty.unix;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.pty.AbstractPtyTest;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
public abstract class AbstractUnixPtyTest extends AbstractPtyTest {
protected abstract UnixPty openpty() throws IOException;
@Test
public void testOpenClosePty() throws IOException {
UnixPty pty = openpty();
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testForkIntoNonExistent()
throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashEchoTest()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashInterruptCat()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
}
}

View file

@ -70,13 +70,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows); System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); session.waitExited();
@ -101,13 +102,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env); pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env);
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows); System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); session.waitExited();