Integrate Debugger-agnostic tests into the test pipelines (#3215)

* Add dbg tests to test suite

* Fix 0

* Check for OSError in OpportunisticTerminalControl

* Split tests tasks

* Fix go tests in LLDB

* Update TLS tests to handle LLDB failing to resolve %GS on LLDB

* Disable go-based x86 test for windbg commands on LLDB

* Fix listening to new modules being loaded in LLDB

* Force LLDB tests to run in series

Parallel execution is broken, anyway

* Fix mallocng tests in LLDB

* ptmalloc2: Always cast tcache counts to pointer during try-free

* Catch LLDB_INVALID_ADDRESS in LLDB symbol lookup

* Handle the binary formatting from LLDB in `test_vis_heap_chunks`

* Split GDB and DBG GDB Nix tests, remove DBG LLDB Nix tests

* Replace ParamSpec in type param list with explicit use

* Add mising dependencies in Ubuntu test targets

* Revert "Add mising dependencies in Ubuntu test targets"

This reverts commit bd56a6b9dc.

* Disable -fcf-protection in test binaries

* Disable LLDB tests on Ubuntu 22.04

We don't seem to even officially support it for pwndbg-lldb
pull/3236/head
Matt. 4 months ago committed by GitHub
parent 43ce818c4c
commit 822a32a254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -29,5 +29,15 @@ jobs:
- name: Docker Build ${{ matrix.images }} - name: Docker Build ${{ matrix.images }}
run: docker compose build ${{ matrix.images }} run: docker compose build ${{ matrix.images }}
- name: Test on ${{ matrix.images }} - name: Run GDB Tests on ${{ matrix.images }}
run: docker compose run ${{ matrix.images }} ./tests.sh -d gdb -g gdb run: |
docker compose run ${{ matrix.images }} ./tests.sh -d gdb -g gdb
- name: Run DBG Tests on GDB on ${{ matrix.images }}
run: |
docker compose run ${{ matrix.images }} ./tests.sh -d gdb -g dbg
- name: Run DBG Tests on LLDB on ${{ matrix.images }}
run: |
docker compose run ${{ matrix.images }} ./tests.sh -d lldb -g dbg

@ -52,11 +52,16 @@ jobs:
run: | run: |
./unit-tests.sh ./unit-tests.sh
- name: Run tests - name: Run GDB Tests
if: matrix.type == 'tests' if: matrix.type == 'tests'
run: | run: |
./tests.sh --nix -d gdb -g gdb ./tests.sh --nix -d gdb -g gdb
- name: Run DBG Tests on GDB
if: matrix.type == 'tests'
run: |
./tests.sh --nix -d gdb -g dbg
- name: Run cross-arch tests - name: Run cross-arch tests
if: matrix.type == 'qemu-user-tests' if: matrix.type == 'qemu-user-tests'
run: | run: |
@ -112,11 +117,22 @@ jobs:
./.venv/bin/pip freeze ./.venv/bin/pip freeze
# We set `kernel.yama.ptrace_scope=0` for `attachp` command tests # We set `kernel.yama.ptrace_scope=0` for `attachp` command tests
- name: Run tests - name: Run GDB Tests
run: | run: |
sudo sysctl -w kernel.yama.ptrace_scope=0 sudo sysctl -w kernel.yama.ptrace_scope=0
./tests.sh -d gdb -g gdb ./tests.sh -d gdb -g gdb
- name: Run DBG Tests on GDB
run: |
sudo sysctl -w kernel.yama.ptrace_scope=0
./tests.sh -d gdb -g dbg
- name: Run DBG Tests on LLDB
if: matrix.os != 'ubuntu-22.04'
run: |
sudo sysctl -w kernel.yama.ptrace_scope=0
./tests.sh -d lldb -g dbg
qemu-user-tests: qemu-user-tests:
runs-on: [ubuntu-24.04] runs-on: [ubuntu-24.04]
timeout-minutes: 20 timeout-minutes: 20

@ -1352,7 +1352,12 @@ def try_free(addr: str | int) -> None:
) )
errors_found += 1 errors_found += 1
if int(allocator.get_tcache()["counts"][tc_idx]) < int(allocator.mp["tcache_count"]): # May be an array, and tc_idx may be negative, so always cast to a
# pointer before we index into it.
counts = allocator.get_tcache()["counts"]
if int(counts.address.cast(counts.type.target().pointer())[tc_idx]) < int(
allocator.mp["tcache_count"]
):
print(message.success("Using tcache_put")) print(message.success("Using tcache_put"))
if errors_found == 0: if errors_found == 0:
returned_before_error = True returned_before_error = True

@ -1249,6 +1249,12 @@ class LLDBProcess(pwndbg.dbg_mod.Process):
return None return None
sym_addr = ctx.symbol.addr.GetLoadAddress(self.target) sym_addr = ctx.symbol.addr.GetLoadAddress(self.target)
if sym_addr == lldb.LLDB_INVALID_ADDRESS:
# Unlikely. LLDB can return some truly insane matches sometimes. In
# this case, we could not find the load address of the symbol we
# matched. Act as if we found nothing.
return None
assert ( assert (
sym_addr <= address sym_addr <= address
), f"LLDB returned an out-of-range address {sym_addr:#x} for a requested symbol with address {address:#x}" ), f"LLDB returned an out-of-range address {sym_addr:#x} for a requested symbol with address {address:#x}"

@ -66,7 +66,7 @@ class OpportunisticTerminalControl:
if fd == -1: if fd == -1:
try: try:
fd = os.open("/dev/tty", os.O_RDWR) fd = os.open("/dev/tty", os.O_RDWR)
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError, OSError):
# Flop and die. # Flop and die.
self.supported = False self.supported = False
return return
@ -403,7 +403,7 @@ class IODriverPseudoTerminal(IODriver):
termios.tcsetwinsize(self.manager, size) # novm termios.tcsetwinsize(self.manager, size) # novm
signal.signal(signal.SIGWINCH, handle_sigwinch) signal.signal(signal.SIGWINCH, handle_sigwinch)
except FileNotFoundError: except (FileNotFoundError, PermissionError, OSError):
print( print(
"warning: no terminal device in /dev/tty, expect no support for terminal sizes" "warning: no terminal device in /dev/tty, expect no support for terminal sizes"
) )

@ -537,9 +537,8 @@ class ProcessDriver:
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()
self.listener.StartListeningForEventClass( self.listener.StartListeningForEvents(
target.GetDebugger(), target.broadcaster,
lldb.SBTarget.GetBroadcasterClassName(),
lldb.SBTarget.eBroadcastBitModulesLoaded, lldb.SBTarget.eBroadcastBitModulesLoaded,
) )

@ -31,6 +31,9 @@ else
CFLAGS += -O1 CFLAGS += -O1
endif endif
# LLDB does not support PLT symbolication with -fcf-protection :(
CFLAGS += -fcf-protection=none
PWD=$(shell pwd) PWD=$(shell pwd)
# Apparently we don't have this version? :( # Apparently we don't have this version? :(
#GLIBC=/glibc_versions/2.29/tcache_x64 #GLIBC=/glibc_versions/2.29/tcache_x64

@ -7,14 +7,17 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Concatenate from typing import Concatenate
from typing import Coroutine from typing import Coroutine
from typing import ParamSpec
from .... import host from .... import host
from ....host import Controller from ....host import Controller
BINARIES_PATH = os.environ.get("TEST_BINARIES_ROOT") BINARIES_PATH = os.environ.get("TEST_BINARIES_ROOT")
T = ParamSpec("T")
def pwndbg_test[**T](
def pwndbg_test(
test: Callable[Concatenate[Controller, T], Coroutine[Any, Any, None]], test: Callable[Concatenate[Controller, T], Coroutine[Any, Any, None]],
) -> Callable[T, None]: ) -> Callable[T, None]:
@functools.wraps(test) @functools.wraps(test)

@ -42,8 +42,8 @@ async def test_vis_heap_chunk_command(ctrl: Controller) -> None:
from pwndbg.commands.ptmalloc2 import bin_ascii from pwndbg.commands.ptmalloc2 import bin_ascii
first, second = (await ctrl.execute_and_capture(f"x/16xb {gdb_symbol}")).splitlines() first, second = (await ctrl.execute_and_capture(f"x/16xb {gdb_symbol}")).splitlines()
first = [int(v, 16) for v in first.split(":")[1].split("\t")[1:]] first = [int(v, 16) for v in first.split(":")[1].split()]
second = [int(v, 16) for v in second.split(":")[1].split("\t")[1:]] second = [int(v, 16) for v in second.split(":")[1].split()]
return bin_ascii(first + second) return bin_ascii(first + second)

@ -17,6 +17,11 @@ TLS_I386_BINARY = get_binary("tls.i386.out")
async def test_tls_address_and_command(ctrl: Controller, binary: str): async def test_tls_address_and_command(ctrl: Controller, binary: str):
import pwndbg.aglib.tls import pwndbg.aglib.tls
import pwndbg.aglib.vmmap import pwndbg.aglib.vmmap
from pwndbg.dbg import DebuggerType
if pwndbg.dbg.name() == DebuggerType.LLDB and binary == TLS_I386_BINARY:
pytest.skip("TLS commands are flaky in LLDB on i386")
return
await launch_to(ctrl, binary, "break_here") await launch_to(ctrl, binary, "break_here")

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import pytest
from ....host import Controller from ....host import Controller
from . import get_binary from . import get_binary
from . import pwndbg_test from . import pwndbg_test

@ -2,6 +2,8 @@ from __future__ import annotations
import re import re
import pytest
from ....host import Controller from ....host import Controller
from . import get_binary from . import get_binary
from . import pwndbg_test from . import pwndbg_test
@ -323,6 +325,13 @@ async def test_windbg_commands_x86(ctrl: Controller) -> None:
like dq, dw, db, ds etc. like dq, dw, db, ds etc.
""" """
import pwndbg import pwndbg
from pwndbg.dbg import DebuggerType
if pwndbg.dbg.name() == DebuggerType.LLDB:
pytest.skip(
"LLDB does not properly support Go, and fails to resolve expressions such as `$esp`"
)
return
await ctrl.launch(X86_BINARY) await ctrl.launch(X86_BINARY)

@ -48,12 +48,20 @@ def main():
) )
sys.exit(1) sys.exit(1)
force_serial = False
match args.driver: match args.driver:
case Driver.GDB: case Driver.GDB:
host = get_gdb_host(args, local_pwndbg_root) host = get_gdb_host(args, local_pwndbg_root)
case Driver.LLDB: case Driver.LLDB:
host = get_lldb_host(args, local_pwndbg_root) host = get_lldb_host(args, local_pwndbg_root)
# LLDB does not properly support having its tests run in parallel,
# so we forcibly disable it, for now.
print(
"WARNING: LLDB tests always run in series, even when parallel execution is requested."
)
force_serial = True
# Handle the case in which the user only wants the collection to run. # Handle the case in which the user only wants the collection to run.
if args.collect_only: if args.collect_only:
for test in host.collect(): for test in host.collect():
@ -62,7 +70,12 @@ def main():
# Actually run the tests. # Actually run the tests.
run_tests_and_print_stats( run_tests_and_print_stats(
host, args.test_name_filter, args.pdb, args.serial, args.verbose, coverage_out host,
args.test_name_filter,
args.pdb,
force_serial or args.serial,
args.verbose,
coverage_out,
) )

Loading…
Cancel
Save