diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java index 21591c5c7c..16f5195f7d 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/java/agent/gdb/manager/GdbManager.java @@ -25,7 +25,7 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInfo; import agent.gdb.manager.breakpoint.GdbBreakpointInsertions; import agent.gdb.manager.impl.GdbManagerImpl; 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 @@ -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 * 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 - * 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. * * @param listener the listener to add diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py index c958acb6db..0ba32f22df 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/commands.py @@ -551,7 +551,7 @@ def putmem_state(address, length, state, pages=True): inf = gdb.selected_inferior() base, addr = STATE.trace.memory_mapper.map(inf, start) 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) diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py index 3cdca5b4f9..9ccf4b03a2 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/methods.py @@ -17,11 +17,10 @@ from concurrent.futures import Future, Executor from contextlib import contextmanager import re +import gdb from ghidratrace import sch from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange -import gdb - from . import commands, hooks, util @@ -690,8 +689,8 @@ def read_mem(inferior: sch.Schema('Inferior'), range: AddressRange): gdb.execute( f'ghidra trace putmem 0x{offset_start:x} {range.length()}') except: - commands.putmem_state( - offset_start, offset_start+range.length() - 1, 'error') + gdb.execute( + f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error') @REGISTRY.method diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java index 0a3370a972..d05850d29a 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java +++ b/Ghidra/Debug/Debugger-agent-gdb/src/test/java/agent/gdb/manager/impl/JoinedGdbManagerTest.java @@ -23,7 +23,8 @@ import org.junit.Ignore; import agent.gdb.manager.GdbManager; import ghidra.pty.PtySession; -import ghidra.pty.linux.LinuxPty; +import ghidra.pty.linux.LinuxIoctls; +import ghidra.pty.unix.UnixPty; import ghidra.util.Msg; @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; @Override protected CompletableFuture startManager(GdbManager manager) { try { - ptyUserGdb = LinuxPty.openpty(); + ptyUserGdb = UnixPty.openpty(LinuxIoctls.INSTANCE); manager.start(null); Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName()); diff --git a/Ghidra/Debug/Debugger-agent-lldb/data/debugger-launchers/local-lldb.sh b/Ghidra/Debug/Debugger-agent-lldb/data/debugger-launchers/local-lldb.sh new file mode 100755 index 0000000000..8f8a0e123c --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-lldb/data/debugger-launchers/local-lldb.sh @@ -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 +#@desc

Launch with lldb

+#@desc

This will launch the target on the local machine using lldb. LLDB must already +#@desc be installed on your system, and it must embed the Python 3 interpreter. You will also +#@desc need protobuf and psutil installed for Python 3.

+#@desc +#@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" diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/arch.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/arch.py index d4055719b3..e1eeb193ce 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/arch.py +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/arch.py @@ -14,20 +14,20 @@ # limitations under the License. ## from ghidratrace.client import Address, RegVal - import lldb from . import util + # NOTE: This map is derived from the ldefs using a script language_map = { 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'], 'armv7': ['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'], - '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'], - 'arm64e': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], + 'arm64e': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:v8A'], 'i386': ['x86:LE:32:default'], 'thumbv7': ['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', } -x86_compiler_map = { +default_compiler_map = { 'freebsd': 'gcc', 'linux': 'gcc', 'netbsd': 'gcc', @@ -55,10 +55,12 @@ x86_compiler_map = { } compiler_map = { - 'DATA:BE:64:default': data64_compiler_map, - 'DATA:LE:64:default': data64_compiler_map, - 'x86:LE:32:default': x86_compiler_map, - 'x86:LE:64:default': x86_compiler_map, + 'DATA:BE:64:': data64_compiler_map, + 'DATA:LE:64:': data64_compiler_map, + 'x86:LE:32:': default_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 # 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' - comp_map = compiler_map[lang] + comp_map = compiler_map[matched_lang[0]] osabi = get_osabi() - if osabi in comp_map: - return comp_map[osabi] + matched_osabi = sorted( + (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: return comp_map[None] return 'default' @@ -161,7 +171,8 @@ class DefaultMemoryMapper(object): def map_back(self, proc: lldb.SBProcess, address: Address) -> int: if address.space == self.defaultSpace: 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') @@ -203,11 +214,11 @@ class DefaultRegisterMapper(object): def map_value(self, proc, name, value): try: - ### TODO: this seems half-baked + # TODO: this seems half-baked av = value.to_bytes(8, "big") except e: raise ValueError("Cannot convert {}'s value: '{}', type: '{}'" - .format(name, value, value.type)) + .format(name, value, value.type)) return RegVal(self.map_name(proc, name), av) def map_name_back(self, proc, name): @@ -258,4 +269,3 @@ def compute_register_mapper(lang): if ':LE:' in lang: return DEFAULT_LE_REGISTER_MAPPER return register_mappers[lang] - diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/commands.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/commands.py index 0396ad97cb..ade0786407 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/commands.py +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/commands.py @@ -14,20 +14,23 @@ # limitations under the License. ## from contextlib import contextmanager +import functools import inspect import os.path +import shlex import socket -import time import sys +import time + +import psutil from ghidratrace import sch from ghidratrace.client import Client, Address, AddressRange, TraceObject -import psutil - import lldb from . import arch, hooks, methods, util + PAGE_SIZE = 4096 DEFAULT_REGISTER_BANK = "General Purpose Registers" @@ -119,59 +122,124 @@ class State(object): STATE = State() if __name__ == '__main__': - lldb.SBDebugger.InitializeWithErrorHandling(); + lldb.SBDebugger.InitializeWithErrorHandling() lldb.debugger = lldb.SBDebugger.Create() elif lldb.debugger: - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_connect "ghidra_trace_connect"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_listen "ghidra_trace_listen"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_disconnect "ghidra_trace_disconnect"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_start "ghidra_trace_start"') - 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('command script add -f ghidralldb.commands.ghidra_trace_info "ghidra_trace_info"') - lldb.debugger.HandleCommand('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_txstart "ghidra_trace_txstart"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txcommit "ghidra_trace_txcommit"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txabort "ghidra_trace_txabort"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_txopen "ghidra_trace_txopen"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_save "ghidra_trace_save"') - lldb.debugger.HandleCommand('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_set_snap "ghidra_trace_set_snap"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putmem "ghidra_trace_putmem"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putval "ghidra_trace_putval"') - lldb.debugger.HandleCommand('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_delmem "ghidra_trace_delmem"') - lldb.debugger.HandleCommand('command script add -f ghidralldb.commands.ghidra_trace_putreg "ghidra_trace_putreg"') - 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( + 'command container add -h "Commands for connecting to Ghidra" ghidra') + lldb.debugger.HandleCommand( + '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_connect ghidra trace connect') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_listen ghidra trace listen') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_disconnect ghidra trace disconnect') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_start ghidra trace start') + 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( + 'command script add -f ghidralldb.commands.ghidra_trace_info ghidra trace info') + lldb.debugger.HandleCommand( + '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_txstart ghidra trace tx-start') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_txcommit ghidra trace tx-commit') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_txabort ghidra trace tx-abort') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_txopen ghidra trace tx-open') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_save ghidra trace save') + lldb.debugger.HandleCommand( + '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_set_snap ghidra trace set-snap') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_putmem ghidra trace putmem') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_putval ghidra trace putval') + lldb.debugger.HandleCommand( + '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_delmem ghidra trace delmem') + lldb.debugger.HandleCommand( + 'command script add -f ghidralldb.commands.ghidra_trace_putreg ghidra trace putreg') + 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.SetAsync(True) 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): """ Connect LLDB to Ghidra for tracing @@ -179,11 +247,14 @@ def ghidra_trace_connect(debugger, command, result, internal_dict): Address must be of the form 'host:port' """ + args = shlex.split(command) + STATE.require_no_client() - address = command if len(command) > 0 else None - if address is None: - raise RuntimeError("'ghidra_trace_connect': missing required argument 'address'") - + if len(args) != 1: + raise RuntimeError( + "ghidra trace connect: missing required argument 'address'") + address = args[0] + parts = address.split(':') if len(parts) != 2: raise RuntimeError("address must be in the form 'host:port'") @@ -191,11 +262,13 @@ def ghidra_trace_connect(debugger, command, result, internal_dict): try: c = socket.socket() 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: raise RuntimeError("port must be numeric") - + +@convert_errors def ghidra_trace_listen(debugger, command, result, internal_dict): """ 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))) host, port = s.getsockname() s.listen(1) - print("Listening at {}:{}...\n".format(host, port)) + print(f"Listening at {host}:{port}...") c, (chost, cport) = s.accept() s.close() - print("Connection from {}:{}\n".format(chost, cport)) - STATE.client = Client(c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY) + print(f"Connection from {chost}:{cport}") + STATE.client = Client( + c, "lldb-" + util.LLDB_VERSION.full, methods.REGISTRY) except ValueError: raise RuntimeError("port must be numeric") +@convert_errors def ghidra_trace_disconnect(debugger, command, result, internal_dict): """Disconnect LLDB from Ghidra for tracing""" @@ -266,17 +341,19 @@ def start_trace(name): util.set_convenience_variable('_ghidra_tracing', "true") +@convert_errors def ghidra_trace_start(debugger, command, result, internal_dict): """Start a Trace in Ghidra""" STATE.require_client() name = command if len(command) > 0 else compute_name() - #if name is None: + # if name is None: # name = compute_name() STATE.require_no_trace() start_trace(name) +@convert_errors def ghidra_trace_stop(debugger, command, result, internal_dict): """Stop the Trace in Ghidra""" @@ -284,6 +361,7 @@ def ghidra_trace_stop(debugger, command, result, internal_dict): STATE.reset_trace() +@convert_errors def ghidra_trace_restart(debugger, command, result, internal_dict): """Restart or start the Trace in Ghidra""" @@ -292,27 +370,29 @@ def ghidra_trace_restart(debugger, command, result, internal_dict): STATE.trace.close() STATE.reset_trace() name = command if len(command) > 0 else compute_name() - #if name is None: + # if name is None: # name = compute_name() start_trace(name) +@convert_errors def ghidra_trace_info(debugger, command, result, internal_dict): """Get info about the Ghidra connection""" result = {} if STATE.client is None: - print("Not connected to Ghidra\n") + print("Not connected to Ghidra") return 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: - print("No trace\n") + print("No trace") return - print("Trace active\n") + print("Trace active") return result +@convert_errors def ghidra_trace_info_lcsp(debugger, command, result, internal_dict): """ 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() - print("Selected Ghidra language: {}\n".format(language)) - print("Selected Ghidra compiler: {}\n".format(compiler)) + print(f"Selected Ghidra language: {language}") + print(f"Selected Ghidra compiler: {compiler}") +@convert_errors def ghidra_trace_txstart(debugger, command, result, internal_dict): """ 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) +@convert_errors def ghidra_trace_txcommit(debugger, command, result, internal_dict): """ Commit the current transaction @@ -344,6 +426,7 @@ def ghidra_trace_txcommit(debugger, command, result, internal_dict): STATE.reset_tx() +@convert_errors def ghidra_trace_txabort(debugger, command, result, internal_dict): """ Abort the current transaction @@ -352,7 +435,7 @@ def ghidra_trace_txabort(debugger, command, result, internal_dict): """ tx = STATE.require_tx() - print("Aborting trace transaction!\n") + print("Aborting trace transaction!") tx.abort() STATE.reset_tx() @@ -365,37 +448,23 @@ def open_tracked_tx(description): STATE.reset_tx() +@convert_errors def ghidra_trace_txopen(debugger, command, result, internal_dict): """ Run a command with an open transaction - If possible, use this in the following idiom to ensure your transactions - are closed: - - 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. + This is generally useful only when executing a single 'put' command, or + when executing a custom command that performs several puts. """ - items = command.split(" "); + items = command.split(" ") description = items[0] command = items[1] with open_tracked_tx(description): - lldb.debugger.HandleCommand(command); + lldb.debugger.HandleCommand(command) +@convert_errors def ghidra_trace_save(debugger, command, result, internal_dict): """ Save the current trace @@ -404,6 +473,7 @@ def ghidra_trace_save(debugger, command, result, internal_dict): STATE.require_trace().save() +@convert_errors def ghidra_trace_new_snap(debugger, command, result, internal_dict): """ Create a new snapshot @@ -413,15 +483,15 @@ def ghidra_trace_new_snap(debugger, command, result, internal_dict): description = str(command) STATE.require_tx() - return {'snap': STATE.require_trace().snapshot(description)} # TODO: A convenience var for the current snapshot # 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.) -# ghidra_trace_trace start/stop/restart +# ghidra trace trace start/stop/restart +@convert_errors def ghidra_trace_set_snap(debugger, command, result, internal_dict): """ Go to a snapshot @@ -433,53 +503,58 @@ def ghidra_trace_set_snap(debugger, command, result, internal_dict): eval = util.get_eval(snap) if eval.IsValid(): snap = eval.GetValueAsUnsigned() - + 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() if pages: - start = start // PAGE_SIZE * PAGE_SIZE - end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE + start, end = quantize_pages(start, end) proc = util.get_process() error = lldb.SBError() if end - start <= 0: - return {'count': 0} + return buf = proc.ReadMemory(start, end - start, error) - + count = 0 if error.Success() and buf is not None: base, addr = trace.memory_mapper.map(proc, start) if base != addr.space: trace.create_overlay_space(base, addr.space) count = trace.put_bytes(addr, buf) - if from_tty: - print("Wrote {} bytes\n".format(count)) - return {'count': count} + if result is not None: + result.PutCString(f"Wrote {count} bytes") + else: + raise RuntimeError(f"Cannot read memory at {start:x}") def eval_address(address): try: return util.parse_and_eval(address) - except e: - raise RuntimeError("Cannot convert '{}' to address".format(address)) + except BaseException as e: + raise RuntimeError(f"Cannot convert '{address}' to address: {e}") def eval_range(address, length): start = eval_address(address) try: end = start + util.parse_and_eval(length) - except e: - raise RuntimeError("Cannot convert '{}' to length".format(length)) + except BaseException as e: + raise RuntimeError(f"Cannot convert '{length}' to length: {e}") 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) - 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): """ Record the given block of memory into the Ghidra trace. @@ -489,11 +564,12 @@ def ghidra_trace_putmem(debugger, command, result, internal_dict): address = items[0] length = items[1] pages = items[2] if len(items) > 2 else True - + 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): """ Record the given value into the Ghidra trace, if it's in memory. @@ -502,16 +578,29 @@ def ghidra_trace_putval(debugger, command, result, internal_dict): items = command.split(" ") value = items[0] pages = items[1] if len(items) > 1 else True - + STATE.require_tx() try: start = util.parse_and_eval(value) - except e: - raise RuntimeError("Value '{}' has no address".format(value)) + except BaseExcepion as e: + raise RuntimeError(f"Value '{value}' has no address: {e}") end = start + int(start.GetType().GetByteSize()) 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): """ 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.require_tx() - STATE.trace.validate_state(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) + putmem_state(address, length, state) +@convert_errors def ghidra_trace_delmem(debugger, command, result, internal_dict): """ Delete the given range of memory from the Ghidra trace. @@ -558,7 +642,7 @@ def putreg(frame, bank): space = REGS_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(), level=frame.GetFrameID()) subspace = BANK_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(), - level=frame.GetFrameID(), bank=bank.name) + level=frame.GetFrameID(), bank=bank.name) STATE.trace.create_overlay_space('register', space) robj = STATE.trace.create_object(space) robj.insert() @@ -568,12 +652,14 @@ def putreg(frame, bank): values = [] for i in range(0, bank.GetNumChildren()): 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())) # 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): """ Record the given register group for the current frame into the Ghidra trace. @@ -586,15 +672,16 @@ def ghidra_trace_putreg(debugger, command, result, internal_dict): STATE.require_tx() frame = util.selected_frame() regs = frame.GetRegisters() - if group is not 'all': + if group != 'all': bank = regs.GetFirstValueByName(group) return putreg(frame, bank) - + for i in range(0, regs.GetSize()): bank = regs.GetValueAtIndex(i) putreg(frame, bank) +@convert_errors def ghidra_trace_delreg(debugger, command, result, internal_dict): """ 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() proc = util.get_process() frame = util.selected_frame() - space = 'Processes[{}].Threads[{}].Stack[{}].Registers'.format( - proc.GetProcessID(), util.selected_thread().GetThreadID(), frame.GetFrameID() - ) + space = REGS_PATTERN.format(procnum=proc.GetProcessID(), tnum=util.selected_thread().GetThreadID(), + level=frame.GetFrameID()) mapper = STATE.trace.register_mapper names = [] 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) +@convert_errors def ghidra_trace_create_obj(debugger, command, result, internal_dict): """ Create an object in the Ghidra trace. @@ -627,29 +714,29 @@ def ghidra_trace_create_obj(debugger, command, result, internal_dict): """ path = command - + STATE.require_tx() obj = STATE.trace.create_object(path) obj.insert() - print("Created object: id={}, path='{}'\n".format(obj.id, obj.path)) - return {'id': obj.id, 'path': obj.path} + print(f"Created object: id={obj.id}, path='{obj.path}'") +@convert_errors def ghidra_trace_insert_obj(debugger, command, result, internal_dict): """ Insert an object into the Ghidra trace. """ path = command - + # NOTE: id parameter is probably not necessary, since this command is for # humans. STATE.require_tx() span = STATE.trace.proxy_object_path(path).insert() - print("Inserted object: lifespan={}\n".format(span)) - return {'lifespan': span} + print(f"Inserted object: lifespan={span}") +@convert_errors def ghidra_trace_remove_obj(debugger, command, result, internal_dict): """ 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): 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): 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) def to_bool_list(value, type): 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): 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): 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): @@ -697,7 +785,7 @@ def eval_value(value, schema=None): type = val.GetType() while type.IsTypedefType(): type = type.GetTypedefedType() - + code = type.GetBasicType() if code == lldb.eBasicTypeVoid: return None, sch.VOID @@ -715,14 +803,14 @@ def eval_value(value, schema=None): return int(val.GetValue()), sch.LONG if code == lldb.eBasicTypeBool: return bool(val.GetValue()), sch.BOOL - + # TODO: This seems like a bit of a hack type_name = type.GetName() if type_name.startswith("const char["): return val.GetSummary(), sch.STRING if type_name.startswith("const wchar_t["): return val.GetSummary(), sch.STRING - + if type.IsArrayType(): etype = type.GetArrayElementType() while etype.IsTypedefType(): @@ -763,14 +851,14 @@ def eval_value(value, schema=None): else: return to_int_list(val, type), sch.LONG_ARR elif type.IsPointerType(): - offset = int(val.GetValue(),16) + offset = int(val.GetValue(), 16) proc = util.get_process() base, addr = STATE.trace.memory_mapper.map(proc, offset) return (base, addr), sch.ADDRESS - raise ValueError( - "Cannot convert ({}): '{}', value='{}'".format(schema, value, val)) + raise ValueError(f"Cannot convert ({schema}): '{value}', value='{val}'") +@convert_errors def ghidra_trace_set_value(debugger, command, result, internal_dict): """ Set a value (attribute or element) in the Ghidra trace's object tree. @@ -787,12 +875,12 @@ def ghidra_trace_set_value(debugger, command, result, internal_dict): # spare me from porting path parsing to Python, but it may also be useful # if we ever allow ids here, since the id would be for the object, not the # complete value path. - + items = command.split(" ") path = items[0] key = items[1] value = items[2] - if len(items) > 3 and items[3] is not "": + if len(items) > 3 and items[3] != "": schema = items[3] # This is a horrible hack if (value.startswith("\"") or value.startswith("L\"")) and schema.endswith("\""): @@ -800,7 +888,7 @@ def ghidra_trace_set_value(debugger, command, result, internal_dict): schema = None else: schema = None - + schema = None if schema is None else sch.Schema(schema) STATE.require_tx() if schema == sch.OBJECT: @@ -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) +@convert_errors def ghidra_trace_retain_values(debugger, command, result, internal_dict): """ 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) +@convert_errors def ghidra_trace_get_obj(debugger, command, result, internal_dict): """ Get an object descriptor by its canonical path. @@ -860,10 +950,9 @@ def ghidra_trace_get_obj(debugger, command, result, internal_dict): """ path = command - + trace = STATE.require_trace() object = trace.get_object(path) - print("{}\t{}\n".format(object.id, object.path)) return object @@ -881,7 +970,7 @@ class TableColumn(object): def print_cell(self, i): 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): @@ -901,14 +990,14 @@ class Tabular(object): for rn in range(self.num_rows): for c in self.columns: c.print_cell(rn) - print('\n') + print('') def val_repr(value): if isinstance(value, TraceObject): return value.path elif isinstance(value, Address): - return '{}:{:08x}'.format(value.space, value.offset) + return f'{value.space}:{value.offset:08x}' return repr(value) @@ -920,19 +1009,21 @@ def print_values(values): table.print_table() +@convert_errors def ghidra_trace_get_values(debugger, command, result, internal_dict): """ List all values matching a given path pattern. """ pattern = command - + trace = STATE.require_trace() values = trace.get_values(pattern) print_values(values) return values +@convert_errors def ghidra_trace_get_values_rng(debugger, command, result, internal_dict): """ List all values intersecting a given address range. @@ -962,13 +1053,15 @@ def activate(path=None): else: frame = util.selected_frame() 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: path = FRAME_PATTERN.format( procnum=proc.GetProcessID(), tnum=t.GetThreadID(), level=frame.GetFrameID()) trace.proxy_object_path(path).activate() +@convert_errors def ghidra_trace_activate(debugger, command, result, internal_dict): """ Activate an object in Ghidra's GUI. @@ -976,12 +1069,13 @@ def ghidra_trace_activate(debugger, command, result, internal_dict): This has no effect if the current trace is not current in Ghidra. If path is omitted, this will activate the current frame. """ - + path = command if len(command) > 0 else None activate(path) +@convert_errors def ghidra_trace_disassemble(debugger, command, result, internal_dict): """ Disassemble starting at the given seed. @@ -1000,12 +1094,11 @@ def ghidra_trace_disassemble(debugger, command, result, internal_dict): trace.create_overlay_space(base, addr.space) length = STATE.trace.disassemble(addr) - print("Disassembled {} bytes\n".format(length)) - return {'length': length} + print(f"Disassembled {length} bytes") -def compute_proc_state(proc = None): - if proc.is_running: +def compute_proc_state(proc=None): + if proc.is_running: return 'RUNNING' return 'STOPPED' @@ -1021,6 +1114,7 @@ def put_processes(): procobj.insert() STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys) + def put_state(event_process): STATE.require_no_tx() STATE.tx = STATE.require_trace().start_tx("state", undoable=False) @@ -1033,6 +1127,7 @@ def put_state(event_process): STATE.reset_tx() +@convert_errors def ghidra_trace_put_processes(debugger, command, result, internal_dict): """ Put the list of processes into the trace's Processes list. @@ -1050,11 +1145,12 @@ def put_available(): procobj = STATE.trace.create_object(ppath) keys.append(AVAILABLE_KEY_PATTERN.format(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() STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys) +@convert_errors def ghidra_trace_put_available(debugger, command, result, internal_dict): """ 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() if b.GetCommandLineCommands(cmdList): list = [] - for i in range(0,cmdList.GetSize()): + for i in range(0, cmdList.GetSize()): list.append(cmdList.GetStringAtIndex(i)) brkobj.set_value('Commands', list) if b.GetCondition(): @@ -1108,17 +1204,19 @@ def put_single_breakpoint(b, ibobj, proc, ikeys): brkobj.retain_values(keys) brkobj.insert() + def put_single_watchpoint(b, ibobj, proc, ikeys): 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) desc = util.get_description(b, level=0) brkobj.set_value('_expression', desc) brkobj.set_value('_kinds', 'WRITE') if "type = r" in desc: - brkobj.set_value('_kinds', 'READ') + brkobj.set_value('_kinds', 'READ') if "type = rw" in desc: - brkobj.set_value('_kinds', 'READ,WRITE') + brkobj.set_value('_kinds', 'READ,WRITE') base, addr = mapper.map(proc, b.GetWatchAddress()) if base != addr.space: STATE.trace.create_overlay_space(base, addr.space) @@ -1148,6 +1246,7 @@ def put_breakpoints(): STATE.trace.proxy_object_path(BREAKPOINTS_PATH).retain_values(keys) ibobj.retain_values(ikeys) + def put_watchpoints(): target = util.get_target() proc = util.get_process() @@ -1163,6 +1262,7 @@ def put_watchpoints(): STATE.trace.proxy_object_path(WATCHPOINTS_PATH).retain_values(keys) +@convert_errors def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict): """ Put the current process's breakpoints into the trace. @@ -1171,7 +1271,9 @@ def ghidra_trace_put_breakpoints(debugger, command, result, internal_dict): STATE.require_tx() with STATE.client.batch() as b: put_breakpoints() - + + +@convert_errors def ghidra_trace_put_watchpoints(debugger, command, result, internal_dict): """ Put the current process's watchpoints into the trace. @@ -1180,7 +1282,7 @@ def ghidra_trace_put_watchpoints(debugger, command, result, internal_dict): STATE.require_tx() with STATE.client.batch() as b: put_watchpoints() - + def put_environment(): proc = util.get_process() @@ -1193,6 +1295,7 @@ def put_environment(): envobj.insert() +@convert_errors def ghidra_trace_put_environment(debugger, command, result, internal_dict): """ Put some environment indicators into the Ghidra trace @@ -1214,7 +1317,8 @@ def put_regions(): mapper = STATE.trace.memory_mapper keys = [] 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)) regobj = STATE.trace.create_object(rpath) 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) +@convert_errors def ghidra_trace_put_regions(debugger, command, result, internal_dict): """ 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) +@convert_errors def ghidra_trace_put_modules(debugger, command, result, internal_dict): """ 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): + # TODO: This does not seem to work - currently supplanted by proc.is_running if t.IsSuspended(): return 'SUSPENDED' if t.IsStopped(): @@ -1320,17 +1427,18 @@ def put_threads(): proc = util.get_process() keys = [] 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) 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()) tid = t.GetThreadID() tobj.set_value('_tid', tid) - tidstr = ('0x{:x}' if radix == - 16 else '0{:o}' if radix == 8 else '{}').format(tid) - tobj.set_value('_short_display', '[{}.{}:{}]'.format( - proc.GetProcessID(), t.GetThreadID(), tidstr)) + tidstr = f'0x{tid:x}' if radix == 16 else f'0{tid:o}' if radix == 8 else f'{tid}' + tobj.set_value('_short_display', + f'[{proc.GetProcessID()}.{t.GetThreadID()}:{tidstr}]') tobj.set_value('_display', compute_thread_display(t)) tobj.insert() STATE.trace.proxy_object_path( @@ -1342,13 +1450,15 @@ def put_event_thread(): # Assumption: Event thread is selected by lldb upon stopping t = util.selected_thread() 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) else: tobj = None STATE.trace.proxy_object_path('').set_value('_event_thread', tobj) +@convert_errors def ghidra_trace_put_threads(debugger, command, result, internal_dict): """ Put the current process's threads into the Ghidra trace @@ -1366,7 +1476,7 @@ def put_frames(): if t is None: return keys = [] - for i in range(0,t.GetNumFrames()): + for i in range(0, t.GetNumFrames()): f = t.GetFrameAtIndex(i) fpath = FRAME_PATTERN.format( 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) +@convert_errors def ghidra_trace_put_frames(debugger, command, result, internal_dict): """ 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() +@convert_errors def ghidra_trace_put_all(debugger, command, result, internal_dict): """ 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() 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, "$sp 1", result, internal_dict) put_processes() @@ -1412,8 +1525,9 @@ def ghidra_trace_put_all(debugger, command, result, internal_dict): put_breakpoints() put_watchpoints() put_available() - + +@convert_errors def ghidra_trace_install_hooks(debugger, command, result, internal_dict): """ Install hooks to trace in Ghidra @@ -1422,6 +1536,7 @@ def ghidra_trace_install_hooks(debugger, command, result, internal_dict): hooks.install_hooks() +@convert_errors def ghidra_trace_remove_hooks(debugger, command, result, internal_dict): """ Remove hooks to trace in Ghidra @@ -1434,6 +1549,7 @@ def ghidra_trace_remove_hooks(debugger, command, result, internal_dict): hooks.remove_hooks() +@convert_errors def ghidra_trace_sync_enable(debugger, command, result, internal_dict): """ 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() +@convert_errors def ghidra_trace_sync_disable(debugger, command, result, internal_dict): """ Cease synchronizing the current process with the Ghidra trace @@ -1464,13 +1581,14 @@ def ghidra_trace_sync_disable(debugger, command, result, internal_dict): hooks.disable_current_process() +@convert_errors def ghidra_util_wait_stopped(debugger, command, result, internal_dict): """ Spin wait until the selected thread is stopped. """ timeout = commmand if len(command) > 0 else '1' - + timeout = int(timeout) start = time.time() t = util.selected_thread() @@ -1481,7 +1599,8 @@ def ghidra_util_wait_stopped(debugger, command, result, internal_dict): time.sleep(0.1) if time.time() - start > timeout: raise RuntimeError('Timed out waiting for thread to stop') - - + + +@convert_errors def ghidra_util_mark(debugger, command, result, internal_dict): print(command) diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/hooks.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/hooks.py index 2996d41ab1..786a6a7fda 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/hooks.py +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/hooks.py @@ -13,15 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. ## -import time import threading +import time import lldb from . import commands, util + ALL_EVENTS = 0xFFFF + class HookState(object): __slots__ = ('installed', 'mem_catchpoint') @@ -31,7 +33,8 @@ class HookState(object): class ProcessState(object): - __slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'watches', 'visited') + __slots__ = ('first', 'regions', 'modules', 'threads', + 'breaks', 'watches', 'visited') def __init__(self): self.first = True @@ -64,9 +67,10 @@ class ProcessState(object): hashable_frame = (thread.GetThreadID(), frame.GetFrameID()) if first or hashable_frame not in self.visited: banks = frame.GetRegisters() - commands.putreg(frame, banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK)) - commands.putmem("$pc", "1", from_tty=False) - commands.putmem("$sp", "1", from_tty=False) + commands.putreg(frame, banks.GetFirstValueByName( + commands.DEFAULT_REGISTER_BANK)) + commands.putmem("$pc", "1", result=None) + commands.putmem("$sp", "1", result=None) self.visited.add(hashable_frame) if first or self.regions or self.threads or self.modules: # Sections, memory syscalls, or stack allocations @@ -117,10 +121,11 @@ HOOK_STATE = HookState() BRK_STATE = BrkState() PROC_STATE = {} + def process_event(self, listener, event): try: desc = util.get_description(event) - #event_process = lldb.SBProcess_GetProcessFromEvent(event) + # print('Event:', desc) event_process = util.get_process() if event_process not in PROC_STATE: PROC_STATE[event_process.GetProcessID()] = ProcessState() @@ -128,35 +133,29 @@ def process_event(self, listener, event): if rc is False: print("add listener for process failed") - commands.put_state(event_process) + # NB: Calling put_state on running leaves an open transaction + if event_process.is_running is False: + commands.put_state(event_process) type = event.GetType() if lldb.SBTarget.EventIsTargetEvent(event): - print('Event:', desc) if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0: - print("eBroadcastBitBreakpointChanged") return on_breakpoint_modified(event) if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0: - print("eBroadcastBitWatchpointChanged") return on_watchpoint_modified(event) if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0: - print("eBroadcastBitModulesLoaded") return on_new_objfile(event) if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0: - print("eBroadcastBitModulesUnloaded") return on_free_objfile(event) if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0: - print("eBroadcastBitSymbolsLoaded") return True if lldb.SBProcess.EventIsProcessEvent(event): if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0: - print("eBroadcastBitStateChanged") if not event_process.is_alive: return on_exited(event) if event_process.is_stopped: return on_stop(event) return True if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0: - print("eBroadcastBitInterrupt") if event_process.is_stopped: return on_stop(event) if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0: @@ -164,138 +163,100 @@ def process_event(self, listener, event): if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0: return True if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0: - print("eBroadcastBitProfileData") return True if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0: - print("eBroadcastBitStructuredData") return True # NB: Thread events not currently processes if lldb.SBThread.EventIsThreadEvent(event): - print('Event:', desc) if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0: - print("eBroadcastBitStackChanged") return on_frame_selected() if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0: - print("eBroadcastBitThreadSuspended") if event_process.is_stopped: return on_stop(event) if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0: - print("eBroadcastBitThreadResumed") return on_cont(event) if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0: - print("eBroadcastBitSelectedFrameChanged") return on_frame_selected() if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0: - print("eBroadcastBitThreadSelected") return on_thread_selected() if lldb.SBBreakpoint.EventIsBreakpointEvent(event): - print('Event:', desc) - btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event); - bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event); + btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event) + bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event) if btype is lldb.eBreakpointEventTypeAdded: - print("eBreakpointEventTypeAdded") return on_breakpoint_created(bpt) if btype is lldb.eBreakpointEventTypeAutoContinueChanged: - print("elldb.BreakpointEventTypeAutoContinueChanged") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeCommandChanged: - print("eBreakpointEventTypeCommandChanged") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeConditionChanged: - print("eBreakpointEventTypeConditionChanged") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeDisabled: - print("eBreakpointEventTypeDisabled") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeEnabled: - print("eBreakpointEventTypeEnabled") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeIgnoreChanged: - print("eBreakpointEventTypeIgnoreChanged") return True if btype is lldb.eBreakpointEventTypeInvalidType: - print("eBreakpointEventTypeInvalidType") return True if btype is lldb.eBreakpointEventTypeLocationsAdded: - print("eBreakpointEventTypeLocationsAdded") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeLocationsRemoved: - print("eBreakpointEventTypeLocationsRemoved") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeLocationsResolved: - print("eBreakpointEventTypeLocationsResolved") return on_breakpoint_modified(bpt) if btype is lldb.eBreakpointEventTypeRemoved: - print("eBreakpointEventTypeRemoved") return on_breakpoint_deleted(bpt) if btype is lldb.eBreakpointEventTypeThreadChanged: - print("eBreakpointEventTypeThreadChanged") return on_breakpoint_modified(bpt) print("UNKNOWN BREAKPOINT EVENT") return True if lldb.SBWatchpoint.EventIsWatchpointEvent(event): - print('Event:', desc) - btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event); - bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt); + btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event) + bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt) if btype is lldb.eWatchpointEventTypeAdded: - print("eWatchpointEventTypeAdded") return on_watchpoint_added(bpt) if btype is lldb.eWatchpointEventTypeCommandChanged: - print("eWatchpointEventTypeCommandChanged") return on_watchpoint_modified(bpt) if btype is lldb.eWatchpointEventTypeConditionChanged: - print("eWatchpointEventTypeConditionChanged") return on_watchpoint_modified(bpt) if btype is lldb.eWatchpointEventTypeDisabled: - print("eWatchpointEventTypeDisabled") return on_watchpoint_modified(bpt) if btype is lldb.eWatchpointEventTypeEnabled: - print("eWatchpointEventTypeEnabled") return on_watchpoint_modified(bpt) if btype is lldb.eWatchpointEventTypeIgnoreChanged: - print("eWatchpointEventTypeIgnoreChanged") return True if btype is lldb.eWatchpointEventTypeInvalidType: - print("eWatchpointEventTypeInvalidType") return True if btype is lldb.eWatchpointEventTypeRemoved: - print("eWatchpointEventTypeRemoved") return on_watchpoint_deleted(bpt) if btype is lldb.eWatchpointEventTypeThreadChanged: - print("eWatchpointEventTypeThreadChanged") return on_watchpoint_modified(bpt) if btype is lldb.eWatchpointEventTypeTypeChanged: - print("eWatchpointEventTypeTypeChanged") return on_watchpoint_modified(bpt) print("UNKNOWN WATCHPOINT EVENT") return True if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event): - print('Event:', desc) if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0: - print("eBroadcastBitAsynchronousErrorData") return True if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0: - print("eBroadcastBitAsynchronousOutputData") return True if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0: - print("eBroadcastBitQuitCommandReceived") return True if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0: - print("eBroadcastBitResetPrompt") return True if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0: - print("eBroadcastBitThreadShouldExit") return True print("UNKNOWN EVENT") return True except RuntimeError as e: print(e) - + + class EventThread(threading.Thread): func = process_event event = lldb.SBEvent() - - def run(self): + + def run(self): # Let's only try at most 4 times to retrieve any kind of event. # After that, the thread exits. listener = lldb.SBListener('eventlistener') @@ -314,7 +275,7 @@ class EventThread(threading.Thread): if rc is False: print("add listener for process failed") return - + # Not sure what effect this logic has rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS) if rc is False: @@ -329,12 +290,13 @@ class EventThread(threading.Thread): print("add listener for process failed") 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: print("add listener for threads failed") return - # THIS WILL NOT WORK: listener = util.get_debugger().GetListener() - + # THIS WILL NOT WORK: listener = util.get_debugger().GetListener() + while True: event_recvd = False while event_recvd is False: @@ -344,13 +306,14 @@ class EventThread(threading.Thread): while listener.GetNextEvent(self.event): self.func(listener, self.event) event_recvd = True - except Exception as e: + except BaseException as e: print(e) proc = util.get_process() if proc is not None and not proc.is_alive: break return + """ # Not sure if this is possible in LLDB... @@ -475,7 +438,7 @@ def on_memory_changed(event): with commands.STATE.client.batch(): with trace.open_tx("Memory *0x{:08x} changed".format(event.address)): 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): @@ -547,11 +510,13 @@ def on_exited(event): commands.put_event_thread() commands.activate() + def notify_others_breaks(proc): for num, state in PROC_STATE.items(): if num != proc.GetProcessID(): state.breaks = True + def notify_others_watches(proc): for num, state in PROC_STATE.items(): if num != proc.GetProcessID(): @@ -697,6 +662,7 @@ def remove_hooks(): return HOOK_STATE.installed = False + def enable_current_process(): proc = util.get_process() PROC_STATE[proc.GetProcessID()] = ProcessState() diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py index 359ff94568..a59babcf0c 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/methods.py @@ -18,7 +18,6 @@ import re from ghidratrace import sch from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange - import lldb from . import commands, util @@ -66,9 +65,7 @@ def find_proc_by_num(procnum): def find_proc_by_pattern(object, pattern, err_msg): - print(object.path) mat = pattern.fullmatch(object.path) - print(mat) if mat is None: raise TypeError(f"{object} is not {err_msg}") procnum = int(mat['procnum']) @@ -81,11 +78,12 @@ def find_proc_by_obj(object): def find_proc_by_procbreak_obj(object): return find_proc_by_pattern(object, PROC_BREAKS_PATTERN, - "a BreakpointLocationContainer") + "a BreakpointLocationContainer") + def find_proc_by_procwatch_obj(object): return find_proc_by_pattern(object, PROC_WATCHES_PATTERN, - "a WatchpointContainer") + "a WatchpointContainer") def find_proc_by_env_obj(object): @@ -108,7 +106,8 @@ def find_thread_by_num(proc, tnum): for t in proc.threads: if t.GetThreadID() == tnum: 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): @@ -166,7 +165,7 @@ def find_reg_by_name(f, name): # I could keep my own cache in a dict, but why? def find_bpt_by_number(breaknum): # 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) if b.GetID() == breaknum: return b @@ -189,7 +188,7 @@ def find_bpt_by_obj(object): # I could keep my own cache in a dict, but why? def find_wpt_by_number(watchnum): # 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) if w.GetID() == watchnum: return w @@ -203,6 +202,7 @@ def find_wpt_by_pattern(pattern, object, err_msg): watchnum = int(mat['watchnum']) return find_wpt_by_number(watchnum) + def find_wpt_by_obj(object): 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')): """List processes on lldb's host system.""" 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') @@ -254,14 +254,14 @@ def refresh_breakpoints(node: sch.Schema('BreakpointContainer')): process). """ 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') def refresh_processes(node: sch.Schema('ProcessContainer')): """Refresh the list of 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') @@ -273,7 +273,7 @@ def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')): refreshed. """ 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') @@ -285,20 +285,21 @@ def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')): refreshed. """ 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') def refresh_environment(node: sch.Schema('Environment')): """Refresh the environment descriptors (arch, os, endian).""" 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') def refresh_threads(node: sch.Schema('ThreadContainer')): """Refresh the list of threads in the process.""" 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') @@ -307,7 +308,7 @@ def refresh_stack(node: sch.Schema('Stack')): t = find_thread_by_stack_obj(node) t.process.SetSelectedThread(t) 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') @@ -317,14 +318,14 @@ def refresh_registers(node: sch.Schema('RegisterValueContainer')): f.thread.SetSelectedFrame(f.GetFrameID()) # TODO: Groups? 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') def refresh_mappings(node: sch.Schema('Memory')): """Refresh the list of memory regions for the process.""" 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') @@ -335,7 +336,7 @@ def refresh_modules(node: sch.Schema('ModuleContainer')): This will refresh the sections for all modules, not just the selected one. """ 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') @@ -343,6 +344,7 @@ def activate_process(process: sch.Schema('Process')): """Switch to the process.""" return + @REGISTRY.method(action='activate') def activate_thread(thread: sch.Schema('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) util.get_debugger().HandleCommand(f'process attach -p {pid}') + @REGISTRY.method(action='attach') def attach_pid(process: sch.Schema('Process'), pid: int): """Attach the process to the given target.""" util.get_debugger().HandleCommand(f'process attach -p {pid}') + @REGISTRY.method(action='attach') def attach_name(process: sch.Schema('Process'), name: str): """Attach the process to the given target.""" @@ -395,23 +399,24 @@ def detach(process: sch.Schema('Process')): @REGISTRY.method(action='launch') def launch_loader(process: sch.Schema('Process'), - file: ParamDesc(str, display='File'), - args: ParamDesc(str, display='Arguments')=''): + file: ParamDesc(str, display='File'), + args: ParamDesc(str, display='Arguments')=''): """ Start a native process with the given command line, stopping at 'main'. If 'main' is not defined in the file, this behaves like 'run'. """ util.get_debugger().HandleCommand(f'file {file}') - if args is not '': - util.get_debugger().HandleCommand(f'settings set target.run-args {args}') + if args != '': + util.get_debugger().HandleCommand( + f'settings set target.run-args {args}') util.get_debugger().HandleCommand(f'process launch --stop-at-entry') @REGISTRY.method(action='launch') def launch(process: sch.Schema('Process'), - file: ParamDesc(str, display='File'), - args: ParamDesc(str, display='Arguments')=''): + file: ParamDesc(str, display='File'), + args: ParamDesc(str, display='Arguments')=''): """ Run a native process with the given command line. @@ -419,8 +424,9 @@ def launch(process: sch.Schema('Process'), signaled. """ util.get_debugger().HandleCommand(f'file {file}') - if args is not '': - util.get_debugger().HandleCommand(f'settings set target.run-args {args}') + if args != '': + util.get_debugger().HandleCommand( + f'settings set target.run-args {args}') util.get_debugger().HandleCommand(f'run') @@ -440,9 +446,9 @@ def _continue(process: sch.Schema('Process')): def interrupt(): """Interrupt the execution of the debugged program.""" util.get_debugger().HandleCommand('process interrupt') - #util.get_process().SendAsyncInterrupt() - #util.get_debugger().HandleCommand('^c') - #util.get_process().Signal(2) + # util.get_process().SendAsyncInterrupt() + # util.get_debugger().HandleCommand('^c') + # util.get_process().Signal(2) @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( proc, Address(range.space, range.min)) 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') def break_read_expression(expression: str): """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') @@ -543,13 +551,15 @@ def break_write_range(process: sch.Schema('Process'), range: AddressRange): offset_start = process.trace.memory_mapper.map_back( proc, Address(range.space, range.min)) 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') def break_write_expression(expression: str): """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') @@ -559,13 +569,15 @@ def break_access_range(process: sch.Schema('Process'), range: AddressRange): offset_start = process.trace.memory_mapper.map_back( proc, Address(range.space, range.min)) 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') def break_access_expression(expression: str): """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') @@ -580,12 +592,14 @@ def toggle_watchpoint(breakpoint: sch.Schema('WatchpointSpec'), enabled: bool): wpt = find_wpt_by_obj(watchpoint) wpt.enabled = enabled + @REGISTRY.method(action='toggle') def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool): """Toggle a breakpoint.""" bpt = find_bpt_by_obj(breakpoint) bpt.enabled = enabled + @REGISTRY.method(action='toggle') def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool): """Toggle a breakpoint location.""" @@ -601,6 +615,7 @@ def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')): wptnum = wpt.GetID() util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}') + @REGISTRY.method(action='delete') def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')): """Delete a breakpoint.""" @@ -615,8 +630,16 @@ def read_mem(process: sch.Schema('Process'), range: AddressRange): proc = find_proc_by_obj(process) offset_start = process.trace.memory_mapper.map_back( proc, Address(range.space, range.min)) + ci = util.get_debugger().GetCommandInterpreter() 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 @@ -628,7 +651,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes): @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.""" f = find_frame_by_obj(frame) f.select() @@ -637,4 +660,5 @@ def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes): reg = find_reg_by_name(f, mname) size = int(lldb.parse_and_eval(f'sizeof(${mname})')) 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};') diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/parameters.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/parameters.py deleted file mode 100644 index 2c7a43a676..0000000000 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/parameters.py +++ /dev/null @@ -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() - diff --git a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/util.py b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/util.py index 2eb1d50442..a5478caf0b 100644 --- a/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/util.py +++ b/Ghidra/Debug/Debugger-agent-lldb/src/main/py/src/ghidralldb/util.py @@ -27,7 +27,10 @@ LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor']) def _compute_lldb_ver(): blurb = lldb.debugger.GetVersionString() 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] return LldbVersion(full, int(major), int(minor)) @@ -36,6 +39,7 @@ LLDB_VERSION = _compute_lldb_ver() GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for " + class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])): pass @@ -70,7 +74,7 @@ class ModuleInfoReader(object): name = s.GetName() attrs = s.GetPermissions() return Section(name, start, end, offset, attrs) - + def finish_module(self, name, sections): alloc = {k: s for k, s in sections.items()} if len(alloc) == 0: @@ -96,7 +100,7 @@ class ModuleInfoReader(object): fspec = module.GetFileSpec() name = debracket(fspec.GetFilename()) sections = {} - for i in range(0, module.GetNumSections()): + for i in range(0, module.GetNumSections()): s = self.section_from_sbsection(module.GetSectionAtIndex(i)) sname = debracket(s.name) sections[sname] = s @@ -107,8 +111,8 @@ class ModuleInfoReader(object): def _choose_module_info_reader(): 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'])): @@ -137,8 +141,8 @@ class RegionInfoReader(object): reglist = get_process().GetMemoryRegions() for i in range(0, reglist.GetSize()): module = get_target().GetModuleAtIndex(i) - info = lldb.SBMemoryRegionInfo(); - success = reglist.GetMemoryRegionAtIndex(i, info); + info = lldb.SBMemoryRegionInfo() + success = reglist.GetMemoryRegionAtIndex(i, info) if success: r = self.region_from_sbmemreg(info) regions.append(r) @@ -177,28 +181,39 @@ def _choose_breakpoint_location_info_reader(): BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader() + def get_debugger(): return lldb.SBDebugger.FindDebuggerWithID(1) + def get_target(): return get_debugger().GetTargetAtIndex(0) + def get_process(): return get_target().GetProcess() - + + def selected_thread(): return get_process().GetSelectedThread() + def selected_frame(): return selected_thread().GetSelectedFrame() - + + def parse_and_eval(expr, signed=False): if signed is True: - return get_target().EvaluateExpression(expr).GetValueAsSigned() - return get_target().EvaluateExpression(expr).GetValueAsUnsigned() + return get_eval(expr).GetValueAsSigned() + return get_eval(expr).GetValueAsUnsigned() + 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): stream = lldb.SBStream() @@ -208,8 +223,10 @@ def get_description(object, level=None): object.GetDescription(stream, level) return escape_ansi(stream.GetData()) + conv_map = {} + def get_convenience_variable(id): #val = get_target().GetEnvironment().Get(id) if id not in conv_map: @@ -219,18 +236,20 @@ def get_convenience_variable(id): return "auto" return val + def set_convenience_variable(id, value): #env = get_target().GetEnvironment() - #return env.Set(id, value, True) + # return env.Set(id, value, True) conv_map[id] = value - + 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) - + + def debracket(init): val = init - val = val.replace("[","(") - val = val.replace("]",")") + val = val.replace("[", "(") + val = val.replace("]", ")") return val diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java index 3dcd7bb8ca..ece13a58cf 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiAcceptor.java @@ -37,6 +37,13 @@ public interface TraceRmiAcceptor { */ 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 * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java index 9ef8efb73a..1f30063ab8 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracermi/TraceRmiLaunchOffer.java @@ -57,16 +57,30 @@ public interface TraceRmiLaunchOffer { * @param exception optional error, if failed */ public record LaunchResult(Program program, Map sessions, - TraceRmiConnection connection, Trace trace, Throwable exception) - implements AutoCloseable { + TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace, + Throwable exception) implements AutoCloseable { + public LaunchResult(Program program, Map 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 public void close() throws Exception { - for (TerminalSession s : sessions.values()) { - s.close(); - } if (connection != null) { connection.close(); } + if (acceptor != null) { + acceptor.cancel(); + } + for (TerminalSession s : sessions.values()) { + s.close(); + } } } diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java index 41c1d37cc2..dda7a3e10c 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java +++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/RunBashInTerminalScript.java @@ -27,7 +27,7 @@ public class RunBashInTerminalScript extends TerminalGhidraScript { Map env = new HashMap<>(System.getenv()); env.put("TERM", "xterm-256color"); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); - displayInTerminal(pty.getParent(), () -> { + displayInTerminal(pty, () -> { try { session.waitExited(); } diff --git a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java index 49c661f054..68b351aa79 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java +++ b/Ghidra/Debug/Debugger-rmi-trace/ghidra_scripts/TerminalGhidraScript.java @@ -37,14 +37,16 @@ public class TerminalGhidraScript extends GhidraScript { 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(); + PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), parent.getInputStream(), parent.getOutputStream())) { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { - parent.setWindowSize(cols, rows); + child.setWindowSize(cols, rows); } }); waiter.run(); @@ -55,7 +57,7 @@ public class TerminalGhidraScript extends GhidraScript { Map env = new HashMap<>(System.getenv()); env.put("TERM", "xterm-256color"); pty.getChild().nullSession(); - displayInTerminal(pty.getParent(), () -> { + displayInTerminal(pty, () -> { while (true) { try { Thread.sleep(100000); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java index 1aab3f8287..da4491b350 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/gui/tracermi/launcher/AbstractTraceRmiLaunchOffer.java @@ -441,6 +441,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer Pty pty = factory.openpty(); PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), parent.getInputStream(), parent.getOutputStream()); terminal.setSubTitle(ShellUtils.generateLine(commandLine)); @@ -448,7 +449,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer @Override public void resized(short cols, short rows) { try { - parent.setWindowSize(cols, rows); + child.setWindowSize(cols, rows); } catch (Exception e) { Msg.error(this, "Could not resize pty: " + e); @@ -490,12 +491,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer Pty pty = factory.openpty(); PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), parent.getInputStream(), parent.getOutputStream()); TerminalListener resizeListener = new TerminalListener() { @Override public void resized(short cols, short rows) { - parent.setWindowSize(cols, rows); + child.setWindowSize(cols, rows); } }; terminal.addTerminalListener(resizeListener); @@ -549,7 +551,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer if (lastExc == null) { lastExc = new CancelledException(); } - return new LaunchResult(program, sessions, connection, trace, lastExc); + return new LaunchResult(program, sessions, acceptor, connection, trace, lastExc); } acceptor = null; sessions.clear(); @@ -598,10 +600,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer } } 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; prompt = mode != PromptMode.NEVER; LaunchResult result = - new LaunchResult(program, sessions, connection, trace, lastExc); + new LaunchResult(program, sessions, acceptor, connection, trace, lastExc); if (prompt) { switch (promptError(result)) { case KEEP: @@ -621,13 +629,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer catch (Exception 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; } 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(); for (Entry ent : result.sessions().entrySet()) { TerminalSession session = ent.getValue(); - sb.append("
  • Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " → " + - HTMLUtilities.escapeHTML(session.description()) + ""); + sb.append("
  • Terminal: %s → %s".formatted( + HTMLUtilities.escapeHTML(ent.getKey()), + HTMLUtilities.escapeHTML(session.description()))); if (session.isTerminated()) { sb.append(" (Terminated)"); } sb.append("
  • \n"); } + if (result.acceptor() != null) { + sb.append("
  • Acceptor: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString()))); + } if (result.connection() != null) { - sb.append("
  • Connection: " + - HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) + - "
  • \n"); + sb.append("
  • Connection: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()))); } if (result.trace() != null) { - sb.append( - "
  • Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "
  • \n"); + sb.append("
  • Trace: %s
  • \n".formatted( + HTMLUtilities.escapeHTML(result.trace().getName()))); } return sb.toString(); } diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java index ab52c32911..2f211820cc 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/DefaultTraceRmiAcceptor.java @@ -62,6 +62,11 @@ public class DefaultTraceRmiAcceptor extends AbstractTraceRmiListener implements } } + @Override + public boolean isClosed() { + return socket.isClosed(); + } + @Override public void close() { plugin.removeAcceptor(this); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java index 4bd69ecb31..4d3f85ffa9 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java @@ -310,7 +310,11 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider PropertyEditor editor = getEditor(param); Object val = computeMemorizedValue(param); - editor.setValue(val); + if (val == null) { + editor.setValue(""); + } else { + editor.setValue(val); + } editor.addPropertyChangeListener(this); pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); paramEditors.put(param, editor); diff --git a/Ghidra/Debug/Framework-Debugging/build.gradle b/Ghidra/Debug/Framework-Debugging/build.gradle index 39efb34953..327154b076 100644 --- a/Ghidra/Debug/Framework-Debugging/build.gradle +++ b/Ghidra/Debug/Framework-Debugging/build.gradle @@ -28,8 +28,8 @@ dependencies { api project(':SoftwareModeling') api project(':ProposedUtils') - api "net.java.dev.jna:jna:5.4.0" - api "net.java.dev.jna:jna-platform:5.4.0" + api "net.java.dev.jna:jna:5.14.0" + api "net.java.dev.jna:jna-platform:5.14.0" testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java index 184f541fef..3cfe46edd3 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyChild.java @@ -101,4 +101,12 @@ public interface PtyChild extends PtyEndpoint { default String nullSession(TermMode... mode) throws IOException { 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); } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java index 2ab7e3a240..2f1146975b 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtyParent.java @@ -19,11 +19,4 @@ package ghidra.pty; * The parent (UNIX "master") end of a pseudo-terminal */ 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); } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java index a096c2f79b..f0281ce74d 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/PtySession.java @@ -15,6 +15,9 @@ */ package ghidra.pty; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + /** * A session led by the child pty * @@ -31,6 +34,8 @@ public interface PtySession { */ int waitExited() throws InterruptedException; + int waitExited(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException; + /** * Take the greatest efforts to terminate the session (leader and descendants) * diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxIoctls.java similarity index 60% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxIoctls.java index 11f3ffdbaa..66aecc1180 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxIoctls.java @@ -15,20 +15,24 @@ */ package ghidra.pty.linux; -import ghidra.pty.PtyParent; -import ghidra.pty.linux.PosixC.Winsize; +import ghidra.pty.unix.PosixC.Ioctls; +import ghidra.pty.unix.UnixPtySessionLeader; -public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent { - LinuxPtyParent(int fd) { - super(fd); +public enum LinuxIoctls implements Ioctls { + INSTANCE; + + @Override + public Class leaderClass() { + return LinuxPtySessionLeader.class; } @Override - public void setWindowSize(short cols, short rows) { - Winsize.ByReference ws = new Winsize.ByReference(); - ws.ws_col = cols; - ws.ws_row = rows; - ws.write(); - PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer()); + public long TIOCSCTTY() { + return 0x540eL; + } + + @Override + public long TIOCSWINSZ() { + return 0x5414L; } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java index 4d4982b132..4ff4118cfb 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyFactory.java @@ -19,15 +19,16 @@ import java.io.IOException; import ghidra.pty.Pty; import ghidra.pty.PtyFactory; +import ghidra.pty.unix.UnixPty; public enum LinuxPtyFactory implements PtyFactory { INSTANCE; @Override public Pty openpty(short cols, short rows) throws IOException { - LinuxPty pty = LinuxPty.openpty(); + UnixPty pty = UnixPty.openpty(LinuxIoctls.INSTANCE); if (cols != 0 && rows != 0) { - pty.getParent().setWindowSize(cols, rows); + pty.getChild().setWindowSize(cols, rows); } return pty; } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java index e5615cec93..1a4088e6c3 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtySessionLeader.java @@ -15,11 +15,10 @@ */ package ghidra.pty.linux; -import java.util.List; +import ghidra.pty.unix.PosixC.Ioctls; +import ghidra.pty.unix.UnixPtySessionLeader; -public class LinuxPtySessionLeader { - private static final PosixC LIB_POSIX = PosixC.INSTANCE; - private static final int O_RDWR = 2; // TODO: Find this in libs +public class LinuxPtySessionLeader extends UnixPtySessionLeader { public static void main(String[] args) throws Exception { LinuxPtySessionLeader leader = new LinuxPtySessionLeader(); @@ -27,61 +26,8 @@ public class LinuxPtySessionLeader { leader.run(); } - protected String ptyPath; - protected List subArgs; - - 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); - } + @Override + protected Ioctls ioctls() { + return LinuxIoctls.INSTANCE; } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java index 1ae6675088..20bf77a767 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalProcessPtySession.java @@ -15,6 +15,9 @@ */ package ghidra.pty.local; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import ghidra.pty.PtySession; import ghidra.util.Msg; @@ -36,6 +39,15 @@ public class LocalProcessPtySession implements PtySession { 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 public void destroyForcibly() { process.destroyForcibly(); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java index 658b3f3923..5b6a71208d 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/local/LocalWindowsNativeProcessPtySession.java @@ -15,6 +15,9 @@ */ package ghidra.pty.local; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import com.sun.jna.LastErrorException; import com.sun.jna.platform.win32.Kernel32; 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); } - @Override - public int waitExited() throws InterruptedException { + protected int doWaitExited(int millis) throws TimeoutException { while (true) { - switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) { + switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), millis)) { case Kernel32.WAIT_OBJECT_0: case Kernel32.WAIT_ABANDONED: IntByReference lpExitCode = new IntByReference(); @@ -54,13 +56,32 @@ public class LocalWindowsNativeProcessPtySession implements PtySession { return lpExitCode.getValue(); } case Kernel32.WAIT_TIMEOUT: - throw new AssertionError(); + throw new TimeoutException(); case Kernel32.WAIT_FAILED: 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 public void destroyForcibly() { if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) { diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosIoctls.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosIoctls.java new file mode 100644 index 0000000000..3a4a6fafcc --- /dev/null +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosIoctls.java @@ -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 leaderClass() { + return MacosPtySessionLeader.class; + } + + @Override + public long TIOCSCTTY() { + return 0x20007461L; + } + + @Override + public long TIOCSWINSZ() { + return 0x80087467L; + } +} diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java index 29bf767a80..9141a85937 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtyFactory.java @@ -19,22 +19,22 @@ import java.io.IOException; import ghidra.pty.Pty; import ghidra.pty.PtyFactory; -import ghidra.pty.linux.LinuxPty; +import ghidra.pty.unix.UnixPty; public enum MacosPtyFactory implements PtyFactory { INSTANCE; @Override public Pty openpty(short cols, short rows) throws IOException { - LinuxPty pty = LinuxPty.openpty(); + UnixPty pty = UnixPty.openpty(MacosIoctls.INSTANCE); if (cols != 0 && rows != 0) { - pty.getParent().setWindowSize(cols, rows); + pty.getChild().setWindowSize(cols, rows); } return pty; } @Override public String getDescription() { - return "local (MacOS)"; + return "local (macOS)"; } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtySessionLeader.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtySessionLeader.java new file mode 100644 index 0000000000..87b0e84b66 --- /dev/null +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/macos/MacosPtySessionLeader.java @@ -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; + } +} diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java index 7e74067fed..1bf3bb12a7 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/GhidraSshPtyFactory.java @@ -234,7 +234,7 @@ public class GhidraSshPtyFactory implements PtyFactory { try { SshPty pty = new SshPty((ChannelExec) session.openChannel("exec")); if (cols != 0 && rows != 0) { - pty.getParent().setWindowSize(cols, rows); + pty.getChild().setWindowSize(cols, rows); } return pty; } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java index 9d792a097b..488ec1bca4 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyChild.java @@ -118,4 +118,8 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild { public OutputStream getOutputStream() { 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); + } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java index c752ace3f6..19a281aa5b 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtyParent.java @@ -26,9 +26,4 @@ public class SshPtyParent extends SshPtyEndpoint implements PtyParent { public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { super(channel, outputStream, inputStream); } - - @Override - public void setWindowSize(short cols, short rows) { - channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0); - } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java index 35650307ef..078ad4ef69 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/ssh/SshPtySession.java @@ -15,6 +15,9 @@ */ package ghidra.pty.ssh; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import com.jcraft.jsch.*; import ghidra.pty.PtySession; @@ -27,16 +30,37 @@ public class SshPtySession implements PtySession { this.channel = channel; } - @Override - public int waitExited() throws InterruptedException { + protected int doWaitExited(Long millis) throws InterruptedException, TimeoutException { + long startMs = System.currentTimeMillis(); // Doesn't look like there's a clever way to wait. So do the spin sleep :( 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 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 public void destroyForcibly() { channel.disconnect(); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Err.java similarity index 97% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Err.java index 46dc1595e2..918e7f0ad1 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Err.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Err.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import com.sun.jna.LastErrorException; import com.sun.jna.Native; diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdInputStream.java similarity index 98% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdInputStream.java index 47075327cc..75ad7a813c 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdInputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdInputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import java.io.IOException; import java.io.InputStream; diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdOutputStream.java similarity index 98% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdOutputStream.java index bedcc42aee..0762c80eff 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/FdOutputStream.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/FdOutputStream.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import java.io.IOException; import java.io.OutputStream; diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/PosixC.java similarity index 84% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/PosixC.java index 4cc789e0aa..03b6041f06 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/PosixC.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/PosixC.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import com.sun.jna.*; import com.sun.jna.Structure.FieldOrder; @@ -26,10 +26,16 @@ import com.sun.jna.Structure.FieldOrder; */ public interface PosixC extends Library { + interface Ioctls { + Class leaderClass(); + + long TIOCSCTTY(); + + long TIOCSWINSZ(); + } + @FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" }) class Winsize extends Structure { - public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific - public short ws_row; public short ws_col; 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", - "c_ospeed" }) - class Termios extends Structure { - public static final int TCSANOW = 0; + @FieldOrder({ "steal" }) + class ControllingTty extends Structure { + public int steal; + 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 int c_iflag; @@ -110,7 +126,7 @@ public interface PosixC extends Library { } @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)); } @@ -141,7 +157,7 @@ public interface PosixC extends Library { 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); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java similarity index 70% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java index 3c635ca8f2..37f7019cac 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPty.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPty.java @@ -13,53 +13,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import java.io.IOException; -import com.sun.jna.*; +import com.sun.jna.Memory; import com.sun.jna.ptr.IntByReference; import ghidra.pty.Pty; +import ghidra.pty.unix.PosixC.Ioctls; import ghidra.util.Msg; -public class LinuxPty implements Pty { +public class UnixPty implements Pty { static final PosixC LIB_POSIX = PosixC.INSTANCE; private final int aparent; private final int achild; - //private final String name; private boolean closed = false; - private final LinuxPtyParent parent; - private final LinuxPtyChild child; + private final UnixPtyParent parent; + private final UnixPtyChild child; - public static LinuxPty openpty() throws IOException { + public static UnixPty openpty(Ioctls ioctls) throws IOException { // TODO: Support termp and winp? IntByReference p = new IntByReference(); IntByReference c = new IntByReference(); Memory n = new Memory(1024); 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 + ")"); this.aparent = aparent; this.achild = achild; - this.parent = new LinuxPtyParent(aparent); - this.child = new LinuxPtyChild(achild, name); + this.parent = new UnixPtyParent(ioctls, aparent); + this.child = new UnixPtyChild(ioctls, achild, name); } @Override - public LinuxPtyParent getParent() { + public UnixPtyParent getParent() { return parent; } @Override - public LinuxPtyChild getChild() { + public UnixPtyChild getChild() { return child; } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyChild.java similarity index 85% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyChild.java index 8a89555d15..cc91555897 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyChild.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import java.io.File; import java.io.IOException; @@ -21,17 +21,17 @@ import java.util.*; import ghidra.pty.PtyChild; import ghidra.pty.PtySession; -import ghidra.pty.linux.PosixC.Termios; import ghidra.pty.local.LocalProcessPtySession; +import ghidra.pty.unix.PosixC.*; 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; private final String name; - LinuxPtyChild(int fd, String name) { - super(fd); + UnixPtyChild(Ioctls ioctls, int fd, String name) { + super(ioctls, fd); this.name = name; } @@ -70,7 +70,7 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { argsList.add(javaCommand); argsList.add("-cp"); argsList.add(System.getProperty("java.class.path")); - argsList.add(LinuxPtySessionLeader.class.getCanonicalName()); + argsList.add(ioctls.leaderClass().getCanonicalName()); argsList.add(name); argsList.addAll(Arrays.asList(args)); @@ -106,4 +106,18 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { tmios.c_lflag &= ~Termios.ECHO; 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); + } + } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java similarity index 83% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java index 12b1b989ca..dc2ba76cde 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/LinuxPtyEndpoint.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyEndpoint.java @@ -13,19 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import java.io.InputStream; import java.io.OutputStream; 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; private final FdOutputStream outputStream; private final FdInputStream inputStream; - LinuxPtyEndpoint(int fd) { + UnixPtyEndpoint(Ioctls ioctls, int fd) { + this.ioctls = ioctls; this.fd = fd; this.outputStream = new FdOutputStream(fd); this.inputStream = new FdInputStream(fd); diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyParent.java new file mode 100644 index 0000000000..c9184de7f1 --- /dev/null +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtyParent.java @@ -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); + } +} diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtySessionLeader.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtySessionLeader.java new file mode 100644 index 0000000000..164fb4394a --- /dev/null +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/UnixPtySessionLeader.java @@ -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 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); + } + } +} diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Util.java similarity index 98% rename from Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java rename to Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Util.java index 6578ea77ef..7cfc113210 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/linux/Util.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/unix/Util.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.pty.linux; +package ghidra.pty.unix; import com.sun.jna.*; import com.sun.jna.ptr.IntByReference; diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java index 76ff4a3f39..952ec903ce 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyChild.java @@ -111,4 +111,9 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild { public String nullSession(Collection mode) throws IOException { throw new UnsupportedOperationException("ConPTY does not have a name"); } + + @Override + public void setWindowSize(short cols, short rows) { + pseudoConsoleHandle.resize(rows, cols); + } } diff --git a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java index 2317be5655..140a22ed52 100644 --- a/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java +++ b/Ghidra/Framework/Pty/src/main/java/ghidra/pty/windows/ConPtyParent.java @@ -22,9 +22,4 @@ public class ConPtyParent extends ConPtyEndpoint implements PtyParent { PseudoConsoleHandle pseudoConsoleHandle) { super(writeHandle, readHandle, pseudoConsoleHandle); } - - @Override - public void setWindowSize(short cols, short rows) { - pseudoConsoleHandle.resize(rows, cols); - } } diff --git a/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java index f438164809..6c6e012bc7 100644 --- a/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/linux/LinuxPtyTest.java @@ -15,201 +15,24 @@ */ package ghidra.pty.linux; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -import java.io.*; -import java.util.*; +import java.io.IOException; import org.junit.Before; -import org.junit.Test; -import ghidra.dbg.testutil.DummyProc; import ghidra.framework.OperatingSystem; -import ghidra.pty.AbstractPtyTest; -import ghidra.pty.PtyChild.Echo; -import ghidra.pty.PtySession; +import ghidra.pty.unix.AbstractUnixPtyTest; +import ghidra.pty.unix.UnixPty; -public class LinuxPtyTest extends AbstractPtyTest { +public class LinuxPtyTest extends AbstractUnixPtyTest { @Before public void checkLinux() { assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM); } - @Test - public void testOpenClosePty() throws IOException { - LinuxPty pty = LinuxPty.openpty(); - 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 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 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()); - } + @Override + protected UnixPty openpty() throws IOException { + return UnixPty.openpty(LinuxIoctls.INSTANCE); } } diff --git a/Ghidra/Framework/Pty/src/test/java/ghidra/pty/macos/MacosPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/macos/MacosPtyTest.java new file mode 100644 index 0000000000..fc5563ba8a --- /dev/null +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/macos/MacosPtyTest.java @@ -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); + } +} diff --git a/Ghidra/Framework/Pty/src/test/java/ghidra/pty/unix/AbstractUnixPtyTest.java b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/unix/AbstractUnixPtyTest.java new file mode 100644 index 0000000000..6a5795aaec --- /dev/null +++ b/Ghidra/Framework/Pty/src/test/java/ghidra/pty/unix/AbstractUnixPtyTest.java @@ -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 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 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()); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java index 14f6a15ab0..2206663601 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/app/plugin/core/terminal/TerminalProviderTest.java @@ -70,13 +70,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), parent.getInputStream(), parent.getOutputStream())) { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { System.err.println("resized: " + cols + "x" + rows); - parent.setWindowSize(cols, rows); + child.setWindowSize(cols, rows); } }); session.waitExited(); @@ -101,13 +102,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest { pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env); PtyParent parent = pty.getParent(); + PtyChild child = pty.getChild(); try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), parent.getInputStream(), parent.getOutputStream())) { term.addTerminalListener(new TerminalListener() { @Override public void resized(short cols, short rows) { System.err.println("resized: " + cols + "x" + rows); - parent.setWindowSize(cols, rows); + child.setWindowSize(cols, rows); } }); session.waitExited();