Fix remote connection lifetime handling in LLDB (#2763)

* Add iOS support

* Rewrite it to match modern Pwndbg

* Forgot to break on healthy targets

* Lint
pull/2798/head
Matt. 9 months ago committed by GitHub
parent 96d85fbc83
commit a229b298da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -936,8 +936,8 @@ def process_connect(driver: ProcessDriver, relay: EventRelay, args: List[str], d
) )
return return
if driver.has_process(): if driver.has_connection():
print(message.error("error: a process is already being debugged")) print(message.error("error: debugger is already connected"))
return return
target = dbg.debugger.GetSelectedTarget() target = dbg.debugger.GetSelectedTarget()
@ -991,7 +991,8 @@ def process_connect(driver: ProcessDriver, relay: EventRelay, args: List[str], d
), "Could not delete the target we've just created. What?" ), "Could not delete the target we've just created. What?"
return return
# Tell the debugger that the process was suspended. # Tell the debugger that the process was suspended, if there is a process.
if driver.has_process():
dbg._trigger_event(EventType.STOP) dbg._trigger_event(EventType.STOP)

@ -7,6 +7,7 @@ from typing import Any
from typing import BinaryIO from typing import BinaryIO
from typing import Coroutine from typing import Coroutine
from typing import List from typing import List
from typing import Tuple
import lldb import lldb
@ -14,6 +15,7 @@ import pwndbg
from pwndbg.dbg.lldb import YieldContinue from pwndbg.dbg.lldb import YieldContinue
from pwndbg.dbg.lldb import YieldSingleStep from pwndbg.dbg.lldb import YieldSingleStep
from pwndbg.dbg.lldb.repl.io import IODriver from pwndbg.dbg.lldb.repl.io import IODriver
from pwndbg.dbg.lldb.repl.io import IODriverPlainText
class EventHandler: class EventHandler:
@ -81,6 +83,13 @@ class ProcessDriver:
""" """
Whether there's an active process in this driver. Whether there's an active process in this driver.
""" """
return self.process is not None and self.process.GetState() != lldb.eStateConnected
def has_connection(self) -> bool:
"""
Whether this driver's connected to a target. All drivers that have an
active process also must necessarily be connected.
"""
return self.process is not None return self.process is not None
def interrupt(self) -> None: def interrupt(self) -> None:
@ -94,7 +103,7 @@ class ProcessDriver:
first_timeout: int = 1, first_timeout: int = 1,
only_if_started: bool = False, only_if_started: bool = False,
fire_events: bool = True, fire_events: bool = True,
) -> lldb.SBEvent | None: ) -> Tuple[bool, lldb.SBEvent | None]:
""" """
Runs the event loop of the process until the next stop event is hit, with Runs the event loop of the process until the next stop event is hit, with
a configurable timeouts for the first and subsequent timeouts. a configurable timeouts for the first and subsequent timeouts.
@ -121,7 +130,8 @@ class ProcessDriver:
# started by a previous action and is running. # started by a previous action and is running.
running = not only_if_started running = not only_if_started
reason: lldb.SBEvent | None = None last: lldb.SBEvent | None = None
expected: bool = False
while True: while True:
event = lldb.SBEvent() event = lldb.SBEvent()
if not self.listener.WaitForEvent(timeout_time, event): if not self.listener.WaitForEvent(timeout_time, event):
@ -138,6 +148,7 @@ class ProcessDriver:
break break
continue continue
last = event
if self.debug: if self.debug:
descr = lldb.SBStream() descr = lldb.SBStream()
@ -170,7 +181,7 @@ class ProcessDriver:
# for the time being. Trigger the stopped event and return. # for the time being. Trigger the stopped event and return.
if fire_events: if fire_events:
self.eh.suspended() self.eh.suspended()
reason = event expected = True
break break
if new_state == lldb.eStateRunning or new_state == lldb.eStateStepping: if new_state == lldb.eStateRunning or new_state == lldb.eStateStepping:
@ -195,13 +206,12 @@ class ProcessDriver:
if fire_events: if fire_events:
self.eh.exited() self.eh.exited()
reason = event
break break
if io_started: if io_started:
self.io.stop() self.io.stop()
return reason return expected, last
def cont(self) -> None: def cont(self) -> None:
""" """
@ -275,6 +285,7 @@ class ProcessDriver:
process in this driver. Returns `True` if the coroutine ran to completion, process in this driver. Returns `True` if the coroutine ran to completion,
and `False` if it was cancelled. and `False` if it was cancelled.
""" """
assert self.has_process(), "called run_coroutine() on a driver with no process"
exception: Exception | None = None exception: Exception | None = None
while True: while True:
try: try:
@ -314,14 +325,20 @@ class ProcessDriver:
) )
continue continue
self._run_until_next_stop() healthy, event = self._run_until_next_stop()
if not healthy:
# The process exited. Cancel the execution controller.
exception = CancelledError()
continue
elif isinstance(step, YieldContinue): elif isinstance(step, YieldContinue):
# Continue the process and wait for the next stop-like event. # Continue the process and wait for the next stop-like event.
self.process.Continue() self.process.Continue()
event = self._run_until_next_stop() healthy, event = self._run_until_next_stop()
assert ( if not healthy:
event is not None # The process exited, Cancel the execution controller.
), "None should only be returned by _run_until_next_stop unless start timeouts are enabled" exception = CancelledError()
continue
# Check whether this stop event is the one we expect. # Check whether this stop event is the one we expect.
stop: lldb.SBBreakpoint | lldb.SBWatchpoint = step.target.inner stop: lldb.SBBreakpoint | lldb.SBWatchpoint = step.target.inner
@ -380,8 +397,33 @@ class ProcessDriver:
Fires the created() event. Fires the created() event.
""" """
stdin, stdout, stderr = io.stdio() assert not self.has_process(), "called launch() on a driver with a live process"
error = lldb.SBError() error = lldb.SBError()
# Do the launch, proper. We always stop the target, and let the upper
# layers deal with the user wanting the program to not stop at entry by
# calling `cont()`.
if self.has_connection():
# This is a remote launch.
#
# We ignore the IODriver we were given, and use the plain text
# driver, as we can't guarantee that anything else would work.
io = IODriverPlainText()
stdin, stdout, stderr = io.stdio()
self.process.RemoteLaunch(
args,
env,
stdin,
stdout,
stderr,
None,
lldb.eLaunchFlagStopAtEntry,
True,
error,
)
else:
# This is a local launch.
self.listener = lldb.SBListener("pwndbg.dbg.lldb.repl.proc.ProcessDriver") self.listener = lldb.SBListener("pwndbg.dbg.lldb.repl.proc.ProcessDriver")
assert self.listener.IsValid() assert self.listener.IsValid()
@ -393,10 +435,7 @@ class ProcessDriver:
lldb.SBTarget.GetBroadcasterClassName(), lldb.SBTarget.GetBroadcasterClassName(),
lldb.SBTarget.eBroadcastBitModulesLoaded, lldb.SBTarget.eBroadcastBitModulesLoaded,
) )
stdin, stdout, stderr = io.stdio()
# Do the launch, proper. We always stop the target, and let the upper
# layers deal with the user wanting the program to not stop at entry by
# calling `cont()`.
self.process = target.Launch( self.process = target.Launch(
self.listener, self.listener,
args, args,
@ -414,6 +453,8 @@ class ProcessDriver:
# Undo any initialization Launch() might've done. # Undo any initialization Launch() might've done.
self.process = None self.process = None
self.listener = None self.listener = None
if not error.success:
return error return error
assert self.listener.IsValid() assert self.listener.IsValid()
@ -473,13 +514,14 @@ class ProcessDriver:
def connect(self, target: lldb.SBTarget, io: IODriver, url: str, plugin: str) -> lldb.SBError: def connect(self, target: lldb.SBTarget, io: IODriver, url: str, plugin: str) -> lldb.SBError:
""" """
Connects to a remote proces with the given URL using the plugin with the Connects to a remote proces with the given URL using the plugin with the
given name, and attaches to the process until LLDB issues a start event given name. This might cause the process to launch in some implementations,
to us. or it might require a call to `launch()`, in implementations that
require a further call to `SBProcess::RemoteLaunch()`.
Potentially fires all types of events, as it is not known when LLDB will Fires the created() event if a process is automatically attached to
return control of the process to us. or launched when a connection succeeds.
""" """
assert not self.has_connection(), "called connect() on a driver with an active connection"
stdin, stdout, stderr = io.stdio() stdin, stdout, stderr = io.stdio()
error = lldb.SBError() error = lldb.SBError()
self.listener = lldb.SBListener("pwndbg.dbg.lldb.repl.proc.ProcessDriver") self.listener = lldb.SBListener("pwndbg.dbg.lldb.repl.proc.ProcessDriver")
@ -506,10 +548,27 @@ class ProcessDriver:
self.io = io self.io = io
# Unlike in `launch()`, it's not guaranteed that the process will not be # Unlike in `launch()`, it's not guaranteed that the process is actually
# running at this point, so we have to attach the I/O and wait until we # alive, as it might be in the Connected state, which indicates that
# get a stop event. # the connection was successful, but that we still need to launch it
self._run_until_next_stop(fire_events=False) # manually in the remote target.
while True:
healthy, event = self._run_until_next_stop(
with_io=False, fire_events=False, only_if_started=True
)
if healthy:
# The process has startarted. We can fire off the created event
# just fine.
self.eh.created() self.eh.created()
break
if (
event is not None
and lldb.SBProcess.GetStateFromEvent(event) == lldb.eStateConnected
):
# This indicates that on this implementation, connecting isn't
# enough to have the process launch or attach, and that we have
# to do that manually.
break
return error return error

Loading…
Cancel
Save