GP-4389: Fixes for Trace RMI lldb on macOS

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

View File

@ -25,7 +25,7 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
import agent.gdb.manager.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

View File

@ -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)

View File

@ -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

View File

@ -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<Void> 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());

View File

@ -0,0 +1,75 @@
#!/usr/bin/env bash
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
#@title lldb
#@desc <html><body width="300px">
#@desc <h3>Launch with <tt>lldb</tt></h3>
#@desc <p>This will launch the target on the local machine using <tt>lldb</tt>. LLDB must already
#@desc be installed on your system, and it must embed the Python 3 interpreter. You will also
#@desc need <tt>protobuf</tt> and <tt>psutil</tt> installed for Python 3.</p>
#@desc </body></html>
#@menu-group local
#@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#lldb
#@enum StartCmd:str run "process launch" "process launch --stop-at-entry"
#@arg :str "Image" "The target binary executable image"
#@args "Arguments" "Command-line arguments to pass to the target"
#@env OPT_LLDB_PATH:str="lldb" "Path to lldb" "The path to lldb. Omit the full path to resolve using the system PATH."
#@env OPT_START_CMD:StartCmd="process launch" "Run command" "The lldb command to actually run the target."
#@env OPT_EXTRA_TTY:bool=false "Target TTY" "Provide a separate terminal emulator for the target."
#@tty TTY_TARGET if env:OPT_EXTRA_TTY
if [ -d ${GHIDRA_HOME}/ghidra/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-agent-lldb/build/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
elif [ -d ${GHIDRA_HOME}/.git ]
then
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-lldb/build/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH
else
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-lldb/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH
fi
target_image="$1"
shift
target_args="$@"
if [ -z "$target_args" ]
then
argspart=
else
argspart=-o "settings set target.run-args $target_args"
fi
if [ -z "$TARGET_TTY" ]
then
ttypart=
else
ttypart=-o "settings set target.output-path $TTY_TARGET" -o "settings set target.input-path $TTY_TARGET"
fi
"$OPT_LLDB_PATH" \
-o "version" \
-o "script import ghidralldb" \
-o "target create \"$target_image\"" \
$argspart \
$ttypart \
-o "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \
-o "ghidra trace start" \
-o "ghidra trace sync-enable" \
-o "$OPT_START_CMD"

View File

@ -14,20 +14,20 @@
# limitations under the License.
##
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]

View File

@ -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()

View File

@ -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};')

View File

@ -1,46 +0,0 @@
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
import lldb
# TODO: I don't know how to register a custom parameter prefix. I would rather
# these were 'ghidra language' and 'ghidra compiler'
class GhidraLanguageParameter(lldb.Parameter):
"""
The language id for Ghidra traces. Set this to 'auto' to try to derive it
from 'show arch' and 'show endian'. Otherwise, set it to a Ghidra
LanguageID.
"""
def __init__(self):
super().__init__('ghidra-language', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraLanguageParameter()
class GhidraCompilerParameter(lldb.Parameter):
"""
The compiler spec id for Ghidra traces. Set this to 'auto' to try to derive
it from 'show osabi'. Otherwise, set it to a Ghidra CompilerSpecID. Note
that valid compiler spec ids depend on the language id.
"""
def __init__(self):
super().__init__('ghidra-compiler', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraCompilerParameter()

View File

@ -27,7 +27,10 @@ LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor'])
def _compute_lldb_ver():
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

View File

@ -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
*

View File

@ -57,16 +57,30 @@ public interface TraceRmiLaunchOffer {
* @param exception optional error, if failed
*/
public record LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiConnection connection, Trace trace, Throwable exception)
implements AutoCloseable {
TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
Throwable exception) implements AutoCloseable {
public LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
Throwable exception) {
this.program = program;
this.sessions = sessions;
this.acceptor = acceptor == null || acceptor.isClosed() ? null : acceptor;
this.connection = connection;
this.trace = trace;
this.exception = exception;
}
@Override
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();
}
}
}

View File

@ -27,7 +27,7 @@ public class RunBashInTerminalScript extends TerminalGhidraScript {
Map<String, String> 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();
}

View File

@ -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<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color");
pty.getChild().nullSession();
displayInTerminal(pty.getParent(), () -> {
displayInTerminal(pty, () -> {
while (true) {
try {
Thread.sleep(100000);

View File

@ -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<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue();
sb.append("<li>Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " &rarr; <tt>" +
HTMLUtilities.escapeHTML(session.description()) + "</tt>");
sb.append("<li>Terminal: %s &rarr; <tt>%s</tt>".formatted(
HTMLUtilities.escapeHTML(ent.getKey()),
HTMLUtilities.escapeHTML(session.description())));
if (session.isTerminated()) {
sb.append(" (Terminated)");
}
sb.append("</li>\n");
}
if (result.acceptor() != null) {
sb.append("<li>Acceptor: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString())));
}
if (result.connection() != null) {
sb.append("<li>Connection: <tt>" +
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) +
"</tt></li>\n");
sb.append("<li>Connection: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString())));
}
if (result.trace() != null) {
sb.append(
"<li>Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "</li>\n");
sb.append("<li>Trace: %s</li>\n".formatted(
HTMLUtilities.escapeHTML(result.trace().getName())));
}
return sb.toString();
}

View File

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

View File

@ -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);

View File

@ -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')
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)
*

View File

@ -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<? extends UnixPtySessionLeader> 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;
}
}

View File

@ -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;
}

View File

@ -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<String> 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;
}
}

View File

@ -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();

View File

@ -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)) {

View File

@ -0,0 +1,38 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public enum MacosIoctls implements Ioctls {
INSTANCE;
@Override
public Class<? extends UnixPtySessionLeader> leaderClass() {
return MacosPtySessionLeader.class;
}
@Override
public long TIOCSCTTY() {
return 0x20007461L;
}
@Override
public long TIOCSWINSZ() {
return 0x80087467L;
}
}

View File

@ -19,22 +19,22 @@ import java.io.IOException;
import ghidra.pty.Pty;
import ghidra.pty.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)";
}
}

View File

@ -0,0 +1,33 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public class MacosPtySessionLeader extends UnixPtySessionLeader {
public static void main(String[] args) throws Exception {
MacosPtySessionLeader leader = new MacosPtySessionLeader();
leader.parseArgs(args);
leader.run();
}
@Override
protected Ioctls ioctls() {
return MacosIoctls.INSTANCE;
}
}

View File

@ -234,7 +234,7 @@ public class GhidraSshPtyFactory implements PtyFactory {
try {
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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<? extends UnixPtySessionLeader> 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);

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -0,0 +1,25 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.unix;
import ghidra.pty.PtyParent;
import ghidra.pty.unix.PosixC.Ioctls;
public class UnixPtyParent extends UnixPtyEndpoint implements PtyParent {
UnixPtyParent(Ioctls ioctls, int fd) {
super(ioctls, fd);
}
}

View File

@ -0,0 +1,93 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.unix;
import java.util.List;
import ghidra.pty.unix.PosixC.ControllingTty;
import ghidra.pty.unix.PosixC.Ioctls;
public abstract class UnixPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
protected String ptyPath;
protected List<String> subArgs;
protected abstract Ioctls ioctls();
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.close(0);
LIB_POSIX.close(1);
LIB_POSIX.close(2);
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
LIB_POSIX.close(fd);
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
ControllingTty.ByReference ctty = new ControllingTty.ByReference();
ctty.steal = 0;
LIB_POSIX.ioctl(0, ioctls().TIOCSCTTY(), ctty.getPointer());
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
// Print to both redirected and to inherited stderr
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
}
}

View File

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

View File

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

View File

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

View File

@ -15,201 +15,24 @@
*/
package ghidra.pty.linux;
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<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testSessionBashInterruptCat() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
@Override
protected UnixPty openpty() throws IOException {
return UnixPty.openpty(LinuxIoctls.INSTANCE);
}
}

View File

@ -0,0 +1,38 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.macos;
import static org.junit.Assume.assumeTrue;
import java.io.IOException;
import org.junit.Before;
import ghidra.framework.OperatingSystem;
import ghidra.pty.unix.AbstractUnixPtyTest;
import ghidra.pty.unix.UnixPty;
public class MacosPtyTest extends AbstractUnixPtyTest {
@Before
public void checkLinux() {
assumeTrue(OperatingSystem.MAC_OS_X == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Override
protected UnixPty openpty() throws IOException {
return UnixPty.openpty(MacosIoctls.INSTANCE);
}
}

View File

@ -0,0 +1,215 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.pty.unix;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.pty.AbstractPtyTest;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
public abstract class AbstractUnixPtyTest extends AbstractPtyTest {
protected abstract UnixPty openpty() throws IOException;
@Test
public void testOpenClosePty() throws IOException {
UnixPty pty = openpty();
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testForkIntoNonExistent()
throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashEchoTest()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashInterruptCat()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
}
}

View File

@ -70,13 +70,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
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();