From 94b7a8f999c8a41e3189e392aa6bd18a0ffe3435 Mon Sep 17 00:00:00 2001 From: gfelber <@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:34:58 +0200 Subject: [PATCH 1/5] added two arguments to gdb.debug: + port: specifies the port that should be used by the gdbserver (this is useful for alpine) + gdb_args: allows forwarding arguments to the gdb binary spawned by gdb.attach() (just a passthrough) aslo downgraded "GDB Python API is supported only for local processes" to a warning, because it seems to work on some systems. --- pwnlib/gdb.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 33aac5810..2d8097854 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -283,7 +283,7 @@ def _execve_script(argv, executable, env, ssh): return tmp.name -def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python_wrapper_script=None): +def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None, python_wrapper_script=None): """_gdbserver_args(pid=None, path=None, args=None, which=None, env=None) -> list Sets up a listening gdbserver, to either connect to the specified @@ -292,6 +292,7 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python Arguments: pid(int): Process ID to attach to path(str): Process to launch + port(int): Port to use for gdbserver args(list): List of arguments to provide on the debugger command line which(callaable): Function to find the path of a binary. env(dict): Environment variables to pass to the program @@ -347,7 +348,7 @@ def _gdbserver_args(pid=None, path=None, args=None, which=None, env=None, python elif env is not None: gdbserver_args += ['--wrapper', which('env'), '-i'] + env_args + ['--'] - gdbserver_args += ['localhost:0'] + gdbserver_args += [f'localhost:{port}'] gdbserver_args += args return gdbserver_args @@ -412,7 +413,7 @@ def _get_runner(ssh=None): else: return tubes.process.process @LocalContext -def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api=False, **kwargs): +def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, sysroot=None, api=False, **kwargs): r""" Launch a GDB server with the specified command line, and launches GDB to attach to it. @@ -420,9 +421,11 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api= Arguments: args(list): Arguments to the process, similar to :class:`.process`. gdbscript(str): GDB script to run. + gdb_args(list): List of additional arguments to pass to GDB. exe(str): Path to the executable on disk env(dict): Environment to start the binary in ssh(:class:`.ssh`): Remote ssh session to use to launch the process. + port(int): Gdb port to use sysroot(str): Set an alternate system root. The system root is used to load absolute shared library symbol files. This is useful to instruct gdb to load a local version of binaries/libraries instead of downloading @@ -612,7 +615,7 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api= gdbscript = gdbscript or '' if api and runner is not tubes.process.process: - raise ValueError('GDB Python API is supported only for local processes') + log.warn('GDB Python API is supported only for local processes') args, env = misc.normalize_argv_env(args, env, log) if env: @@ -628,7 +631,7 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api= if ssh or context.native or (context.os == 'android'): if len(args) > 0 and which(packing._decode(args[0])) == packing._decode(exe): - args = _gdbserver_args(args=args, which=which, env=env) + args = _gdbserver_args(args=args, port=port, which=which, env=env) else: # GDBServer is limited in it's ability to manipulate argv[0] @@ -636,7 +639,7 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api= # ``execve`` calls. # Therefore, we use a wrapper script to execute the target binary script = _execve_script(args, executable=exe, env=env, ssh=ssh) - args = _gdbserver_args(args=args, which=which, env=env, python_wrapper_script=script) + args = _gdbserver_args(args=args, port=port, which=which, env=env, python_wrapper_script=script) else: qemu_port = random.randint(1024, 65535) qemu_user = qemu.user_path() @@ -667,17 +670,19 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, api= # Set the .executable on the process object. gdbserver.executable = exe - # Find what port we need to connect to - if ssh or context.native or (context.os == 'android'): - port = _gdbserver_port(gdbserver, ssh) - else: - port = qemu_port + # if the port was set manually we won't need to find it + if not port: + # Find what port we need to connect to + if ssh or context.native or (context.os == 'android'): + port = _gdbserver_port(gdbserver, ssh) + else: + port = qemu_port host = '127.0.0.1' if not ssh and context.os == 'android': host = context.adb_host - tmp = attach((host, port), exe=exe, gdbscript=gdbscript, ssh=ssh, sysroot=sysroot, api=api) + tmp = attach((host, port), exe=exe, gdbscript=gdbscript, gdb_args=gdb_args, ssh=ssh, sysroot=sysroot, api=api) if api: _, gdb = tmp gdbserver.gdb = gdb From 2b260559ff3015267f84f1f1db1ec932544a3e7a Mon Sep 17 00:00:00 2001 From: gfelber <@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:57:52 +0200 Subject: [PATCH 2/5] added changelog entry and fixed python2.7 compatibility --- CHANGELOG.md | 1 + pwnlib/gdb.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb9e31d1..6af7f8135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ The table below shows which release corresponds to each branch, and what date th ## 4.14.0 (`dev`) +- [#2382][2382] added optional port and gdb_args parameters to gdb.debug() - [#2360][2360] Add offline parameter for `search_by_hash` series function - [#2356][2356] Add local libc database provider for libcdb - [#2374][2374] libcdb.unstrip_libc: debug symbols are fetched only if not present diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 2d8097854..421a12718 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -348,7 +348,7 @@ def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None elif env is not None: gdbserver_args += ['--wrapper', which('env'), '-i'] + env_args + ['--'] - gdbserver_args += [f'localhost:{port}'] + gdbserver_args += ['localhost:%d' % port] gdbserver_args += args return gdbserver_args From 78df1901d9a0ffbb4787b1c6254cd40c18561222 Mon Sep 17 00:00:00 2001 From: gfelber <@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:10:48 +0200 Subject: [PATCH 3/5] also added optional gdbserver_args parameter to gdb.debug() --- CHANGELOG.md | 2 +- pwnlib/gdb.py | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af7f8135..ea037c065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,7 +71,7 @@ The table below shows which release corresponds to each branch, and what date th ## 4.14.0 (`dev`) -- [#2382][2382] added optional port and gdb_args parameters to gdb.debug() +- [#2382][2382] added optional port, gdb_args and gdbserver_args parameters to gdb.debug() - [#2360][2360] Add offline parameter for `search_by_hash` series function - [#2356][2356] Add local libc database provider for libcdb - [#2374][2374] libcdb.unstrip_libc: debug symbols are fetched only if not present diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 421a12718..8c02aa46d 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -283,7 +283,7 @@ def _execve_script(argv, executable, env, ssh): return tmp.name -def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None, python_wrapper_script=None): +def _gdbserver_args(pid=None, path=None, port=0, gdbserver_args=None, args=None, which=None, env=None, python_wrapper_script=None): """_gdbserver_args(pid=None, path=None, args=None, which=None, env=None) -> list Sets up a listening gdbserver, to either connect to the specified @@ -293,6 +293,7 @@ def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None pid(int): Process ID to attach to path(str): Process to launch port(int): Port to use for gdbserver + gdbserver_args(list): List of additional arguments to pass to gdbserver args(list): List of arguments to provide on the debugger command line which(callaable): Function to find the path of a binary. env(dict): Environment variables to pass to the program @@ -301,6 +302,11 @@ def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None Returns: A list of arguments to invoke gdbserver. """ + if gdbserver_args is None: + gdbserver_args = list() + elif not isinstance(gdbserver_args, (list, tuple)): + gdbserver_args = [gdbserver_args] + if [pid, path, args].count(None) != 2: log.error("Must specify exactly one of pid, path, or args") @@ -324,7 +330,7 @@ def _gdbserver_args(pid=None, path=None, port=0, args=None, which=None, env=None orig_args = args - gdbserver_args = [gdbserver, '--multi'] + gdbserver_args = [gdbserver, '--multi'] + gdbserver_args if context.aslr: gdbserver_args += ['--no-disable-randomization'] else: @@ -413,7 +419,7 @@ def _get_runner(ssh=None): else: return tubes.process.process @LocalContext -def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, sysroot=None, api=False, **kwargs): +def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, port=0, gdbserver_args=None, sysroot=None, api=False, **kwargs): r""" Launch a GDB server with the specified command line, and launches GDB to attach to it. @@ -426,6 +432,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por env(dict): Environment to start the binary in ssh(:class:`.ssh`): Remote ssh session to use to launch the process. port(int): Gdb port to use + gdbserver_args(list): List of additional arguments to pass to gdbserver sysroot(str): Set an alternate system root. The system root is used to load absolute shared library symbol files. This is useful to instruct gdb to load a local version of binaries/libraries instead of downloading @@ -631,7 +638,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por if ssh or context.native or (context.os == 'android'): if len(args) > 0 and which(packing._decode(args[0])) == packing._decode(exe): - args = _gdbserver_args(args=args, port=port, which=which, env=env) + args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env) else: # GDBServer is limited in it's ability to manipulate argv[0] @@ -639,7 +646,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por # ``execve`` calls. # Therefore, we use a wrapper script to execute the target binary script = _execve_script(args, executable=exe, env=env, ssh=ssh) - args = _gdbserver_args(args=args, port=port, which=which, env=env, python_wrapper_script=script) + args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env, python_wrapper_script=script) else: qemu_port = random.randint(1024, 65535) qemu_user = qemu.user_path() From 730550242eb696b53f7a8fcf0d1bb38004303652 Mon Sep 17 00:00:00 2001 From: gfelber <34159565+gfelber@users.noreply.github.com> Date: Mon, 13 May 2024 15:56:46 +0200 Subject: [PATCH 4/5] implemented requested fixes for #2382: + gdb.debug port is now also used for qemu + GDB Python API is now tested for tubes.process.process a warning for ssh.process and an error for everything else + updated docs to use mention that gdbserver ports are randomized by default + now using gdbserver_port to check if the correct port was set + fixed CHANGELOG.md structure --- CHANGELOG.md | 3 ++- pwnlib/gdb.py | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bb4ef6f..b0ff3469b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,6 @@ The table below shows which release corresponds to each branch, and what date th ## 4.14.0 (`dev`) -- [#2382][2382] added optional port, gdb_args and gdbserver_args parameters to gdb.debug() - [#2360][2360] Add offline parameter for `search_by_hash` series function - [#2356][2356] Add local libc database provider for libcdb - [#2374][2374] libcdb.unstrip_libc: debug symbols are fetched only if not present @@ -83,6 +82,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2391][2391] Fix error message when passing invalid kwargs to `xor` - [#2376][2376] Return buffered data on first EOF in tube.readline() - [#2387][2387] Convert apport_corefile() output from bytes-like object to string +- [#2382][2382] added optional port, gdb_args and gdbserver_args parameters to gdb.debug() [2360]: https://github.com/Gallopsled/pwntools/pull/2360 [2356]: https://github.com/Gallopsled/pwntools/pull/2356 @@ -94,6 +94,7 @@ The table below shows which release corresponds to each branch, and what date th [2391]: https://github.com/Gallopsled/pwntools/pull/2391 [2376]: https://github.com/Gallopsled/pwntools/pull/2376 [2387]: https://github.com/Gallopsled/pwntools/pull/2387 +[2382]: https://github.com/Gallopsled/pwntools/pull/2382 ## 4.13.0 (`beta`) diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 501aa1408..815266eeb 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -292,7 +292,7 @@ def _gdbserver_args(pid=None, path=None, port=0, gdbserver_args=None, args=None, Arguments: pid(int): Process ID to attach to path(str): Process to launch - port(int): Port to use for gdbserver + port(int): Port to use for gdbserver, default: random gdbserver_args(list): List of additional arguments to pass to gdbserver args(list): List of arguments to provide on the debugger command line which(callaable): Function to find the path of a binary. @@ -435,7 +435,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por exe(str): Path to the executable on disk env(dict): Environment to start the binary in ssh(:class:`.ssh`): Remote ssh session to use to launch the process. - port(int): Gdb port to use + port(int): Gdb port to use, default: random gdbserver_args(list): List of additional arguments to pass to gdbserver sysroot(str): Set an alternate system root. The system root is used to load absolute shared library symbol files. This is useful to instruct @@ -626,7 +626,9 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por gdbscript = gdbscript or '' if api and runner is not tubes.process.process: - log.warn('GDB Python API is supported only for local processes') + if runner is not ssh.process: + raise ValueError('GDB Python API is supported only for local processes') + log.warn('GDB Python API for ssh processes is not officially tested') args, env = misc.normalize_argv_env(args, env, log) if env: @@ -652,7 +654,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por script = _execve_script(args, executable=exe, env=env, ssh=ssh) args = _gdbserver_args(gdbserver_args=gdbserver_args, args=args, port=port, which=which, env=env, python_wrapper_script=script) else: - qemu_port = random.randint(1024, 65535) + qemu_port = port if port != 0 else random.randint(1024, 65535) qemu_user = qemu.user_path() sysroot = sysroot or qemu.ld_prefix(env=env) if not qemu_user: @@ -681,13 +683,13 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por # Set the .executable on the process object. gdbserver.executable = exe - # if the port was set manually we won't need to find it - if not port: - # Find what port we need to connect to - if ssh or context.native or (context.os == 'android'): - port = _gdbserver_port(gdbserver, ssh) - else: - port = qemu_port + if ssh or context.native or (context.os == 'android'): + gdb_port = _gdbserver_port(gdbserver, ssh) + if port != 0 and port != gdb_port: + log.error("gdbserver port (%d) doesn't equals set port (%d)" % (gdb_port, port)) + port = gdb_port + else: + port = qemu_port host = '127.0.0.1' if not ssh and context.os == 'android': From 0ee8d36697d63df01e865ae9f914c39305e83ac3 Mon Sep 17 00:00:00 2001 From: gfelber <34159565+gfelber@users.noreply.github.com> Date: Mon, 13 May 2024 15:56:46 +0200 Subject: [PATCH 5/5] implemented requested fixes for #2382: + gdb.debug port is now also used for qemu + GDB Python API is now tested for tubes.process.process a warning for ssh.process and an error for everything else + updated docs to use mention that gdbserver ports are randomized by default + now using gdbserver_port to check if the correct port was set + fixed CHANGELOG.md structure + added timeouts for recvline() gdb tests + also run gdb api tests for ssh runner --- pwnlib/gdb.py | 109 ++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py index 815266eeb..1ee753c6f 100644 --- a/pwnlib/gdb.py +++ b/pwnlib/gdb.py @@ -195,7 +195,7 @@ def debug_assembly(asm, gdbscript=None, vma=None, api=False): >>> assembly = shellcraft.echo("Hello world!\n") >>> io = gdb.debug_assembly(assembly) - >>> io.recvline() + >>> io.recvline(timeout=1) b'Hello world!\n' """ tmp_elf = make_elf_from_assembly(asm, vma=vma, extract=False) @@ -230,7 +230,7 @@ def debug_shellcode(data, gdbscript=None, vma=None, api=False): >>> assembly = shellcraft.echo("Hello world!\n") >>> shellcode = asm(assembly) >>> io = gdb.debug_shellcode(shellcode) - >>> io.recvline() + >>> io.recvline(timeout=1) b'Hello world!\n' """ if isinstance(data, six.text_type): @@ -490,12 +490,12 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por Send a command to Bash >>> io.sendline(b"echo hello") - >>> io.recvline() + >>> io.recvline(timeout=30) b'hello\n' Interact with the process - >>> io.interactive() # doctest: +SKIP + >>> io.interactive(timeout=1) # doctest: +SKIP >>> io.close() Create a new process, and stop it at '_start' @@ -514,7 +514,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por Send a command to Bash >>> io.sendline(b"echo hello") - >>> io.recvline() + >>> io.recvline(timeout=10) b'hello\n' Interact with the process @@ -526,53 +526,24 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por >>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh") >>> io.sendline(b"echo $0") - >>> io.recvline() + >>> io.recvline(timeout=10) b'\xde\xad\xbe\xef\n' >>> io.close() Demonstrate that LD_PRELOAD is respected >>> io = process(["grep", "libc.so.6", "/proc/self/maps"]) - >>> real_libc_path = io.recvline().split()[-1] + >>> real_libc_path = io.recvline(timeout=1).split()[-1] >>> io.close() >>> import shutil >>> local_path = shutil.copy(real_libc_path, "./local-libc.so") # make a copy of libc to demonstrate that it is loaded >>> io = gdb.debug(["grep", "local-libc.so", "/proc/self/maps"], gdbscript="continue", env={"LD_PRELOAD": "./local-libc.so"}) - >>> io.recvline().split()[-1] # doctest: +ELLIPSIS + >>> io.recvline(timeout=1).split()[-1] # doctest: +ELLIPSIS b'.../local-libc.so' >>> io.close() >>> os.remove("./local-libc.so") # cleanup - Using GDB Python API: - - .. doctest:: - :skipif: is_python2 - - Debug a new process - - >>> io = gdb.debug(['echo', 'foo'], api=True) - - Stop at 'write' - - >>> bp = io.gdb.Breakpoint('write', temporary=True) - >>> io.gdb.continue_and_wait() - - Dump 'count' - - >>> count = io.gdb.parse_and_eval('$rdx') - >>> long = io.gdb.lookup_type('long') - >>> int(count.cast(long)) - 4 - - Resume the program - - >>> io.gdb.continue_nowait() - >>> io.recvline() - b'foo\n' - >>> io.close() - - Using SSH: You can use :func:`debug` to spawn new processes on remote machines as well, @@ -601,7 +572,7 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por >>> io = gdb.debug(args=[b'\xde\xad\xbe\xef'], gdbscript='continue', exe="/bin/sh", ssh=shell) >>> io.sendline(b"echo $0") - >>> io.recvline() + >>> io.recvline(timeout=10) b'$ \xde\xad\xbe\xef\n' >>> io.close() @@ -609,9 +580,55 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por >>> io = gdb.debug(args=[], gdbscript='continue', exe="/bin/sh", ssh=shell) >>> io.sendline(b"echo $0") - >>> io.recvline() + >>> io.recvline(timeout=10) b'$ \n' >>> io.close() + + + Using GDB Python API: + + .. doctest:: + :skipif: is_python2 + + Debug a new process + + >>> io = gdb.debug(['echo', 'foo'], api=True) + + or using ssh + + >>> shell = ssh('travis', 'example.pwnme', password='demopass') + >>> ssh_io = gdb.debug(['/bin/echo', 'foo'], ssh=shell, api=True) + + Stop at 'write' + + >>> bp = io.gdb.Breakpoint('write', temporary=True) + >>> io.gdb.continue_and_wait() + >>> ssh_bp = ssh_io.gdb.Breakpoint('write', temporary=True) + >>> ssh_io.gdb.continue_and_wait() + + Dump 'count' + + >>> count = io.gdb.parse_and_eval('$rdx') + >>> long = io.gdb.lookup_type('long') + >>> int(count.cast(long)) + 4 + >>> count = ssh_io.gdb.parse_and_eval('$rdx') + >>> long = ssh_io.gdb.lookup_type('long') + >>> int(count.cast(long)) + 4 + + Resume the program + + >>> io.gdb.continue_nowait() + >>> io.recvline(timeout=1) + b'foo\n' + >>> io.close() + + >>> ssh_io.gdb.continue_nowait() + >>> ssh_io.recvline(timeout=1) + b'foo\n' + >>> ssh_io.close() + >>> shell.close() """ if isinstance(args, six.integer_types + (tubes.process.process, tubes.ssh.ssh_channel)): log.error("Use gdb.attach() to debug a running process") @@ -625,10 +642,8 @@ def debug(args, gdbscript=None, gdb_args=None, exe=None, ssh=None, env=None, por which = _get_which(ssh) gdbscript = gdbscript or '' - if api and runner is not tubes.process.process: - if runner is not ssh.process: - raise ValueError('GDB Python API is supported only for local processes') - log.warn('GDB Python API for ssh processes is not officially tested') + if api and runner is not tubes.process.process and not ssh: + raise ValueError('GDB Python API is supported only for local and ssh processes') args, env = misc.normalize_argv_env(args, env, log) if env: @@ -962,7 +977,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr ... detach ... quit ... ''') - >>> io.recvline() + >>> io.recvline(timeout=10) b'Hello from process debugger!\n' >>> io.sendline(b'echo Hello from bash && exit') >>> io.recvall() @@ -989,7 +1004,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr Observe the forced line - >>> io.recvline() + >>> io.recvline(timeout=1) b'Hello from process debugger!\n' Interact with the program in a regular way @@ -1013,7 +1028,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr ... detach ... quit ... ''') - >>> io.recvline() + >>> io.recvline(timeout=10) b'Hello from remote debugger!\n' >>> io.sendline(b'echo Hello from bash && exit') >>> io.recvall() @@ -1032,7 +1047,7 @@ def attach(target, gdbscript = '', exe = None, gdb_args = None, ssh = None, sysr >>> io.recvline(timeout=5) # doctest: +SKIP b'Hello from ssh debugger!\n' >>> io.sendline(b'This will be echoed back') - >>> io.recvline() + >>> io.recvline(timeout=1) b'This will be echoed back\n' >>> io.close() """