mirror of https://github.com/pwndbg/pwndbg.git
Qemu user test structure (#2275)
* Initial version of qemu-user tests * Refactor testing files to reduce file duplication, introduce qemu-user-tests * lint and edit github actions workflow file. Move old qemu-user tests to seperate directory * Add iproute2 so ss command is available * test ubuntu 24 * funkiness with current working directory... * Further remote old test_qemu.sh and integrate into a Pytest fixture * lint * Disable ASLR, add test for aarch64 jumps * Use Popen.kill() function to make sure it closes. Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com> * qemu.kill() on the other fixture as well * comment * comment * lint * system test path stuff * remove old try-catch block * revert * revert path change * Use os._exit to pass return code, and move qemu-user tests above system tests because they run significantly faster * lint * Flush stdout before os._exit * Comment out flaky check for the address of main in old qemu tests * rename qemu-user to cross-arch * rename qemu-user to cross-arch and hotfix to not run pytest when cross-arch is used * remove todo comment * another comment * Test pwndbg.gdblib.symbol.address is not None and revert setarch -R * Revert os.exit change * Revert os.exit change * Revert os.exit change * readd os.exit in new exit places * lint * rebase * delete file introduced in rebase * break up tests into 3 files to invoke separately. Update GitHub workflow, remove code duplication in existing test * code coverage * fix code coverage * lint * test difference between Ubuntu 22 and 24 in Kernel tests * lint --------- Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>pull/2373/head
parent
5954563a5d
commit
1438fc0616
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
(cd tests && python3 tests.py -t cross-arch $@)
|
||||
exit_code=$?
|
||||
exit $exit_code
|
||||
@ -1,24 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run integration tests
|
||||
(cd tests/gdb-tests && python3 tests.py $@)
|
||||
(cd tests && python3 tests.py $@)
|
||||
exit_code=$?
|
||||
|
||||
COV=0
|
||||
# Run unit tests
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" == "--cov" ]; then
|
||||
COV=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $COV -eq 1 ]; then
|
||||
coverage run -m pytest tests/unit-tests
|
||||
else
|
||||
pytest tests/unit-tests
|
||||
fi
|
||||
|
||||
exit_code=$((exit_code + $?))
|
||||
|
||||
exit $exit_code
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
.PHONY: all
|
||||
all: reference-binary.aarch64.out reference-binary.riscv64.out
|
||||
|
||||
reference-binary.aarch64.out : reference-binary.aarch64.c
|
||||
@echo "[+] Building '$@'"
|
||||
@aarch64-linux-gnu-gcc $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $? $(LDFLAGS)
|
||||
|
||||
# apt install crossbuild-essential-riscv64
|
||||
reference-binary.riscv64.out : reference-binary.riscv64.c
|
||||
@echo "[+] Building '$@'"
|
||||
@riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -g $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $? $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm reference-binary.aarch64.out
|
||||
rm reference-binary.riscv64.out
|
||||
@ -0,0 +1,105 @@
|
||||
"""
|
||||
This file should consist of global test fixtures.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import gdb
|
||||
import pytest
|
||||
from pwn import context
|
||||
from pwn import make_elf_from_assembly
|
||||
|
||||
_start_binary_called = False
|
||||
|
||||
QEMU_PORT = os.environ.get("QEMU_PORT")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qemu_assembly_run():
|
||||
"""
|
||||
Returns function that launches given binary with 'starti' command
|
||||
|
||||
The `path` is returned from `make_elf_from_assembly` (provided by pwntools)
|
||||
"""
|
||||
|
||||
qemu: subprocess.Popen = None
|
||||
|
||||
if QEMU_PORT is None:
|
||||
print("'QEMU_PORT' environment variable not set")
|
||||
sys.stdout.flush()
|
||||
os._exit(1)
|
||||
|
||||
def _start_binary(asm: str, arch: str, *args):
|
||||
nonlocal qemu
|
||||
|
||||
context.arch = arch
|
||||
binary_tmp_path = make_elf_from_assembly(asm)
|
||||
|
||||
qemu = subprocess.Popen(
|
||||
[
|
||||
f"qemu-{arch}",
|
||||
"-g",
|
||||
f"{QEMU_PORT}",
|
||||
f"{binary_tmp_path}",
|
||||
]
|
||||
)
|
||||
|
||||
gdb.execute(f"target remote :{QEMU_PORT}")
|
||||
gdb.execute("set exception-verbose on")
|
||||
|
||||
global _start_binary_called
|
||||
# if _start_binary_called:
|
||||
# raise Exception('Starting more than one binary is not supported in pwndbg tests.')
|
||||
|
||||
_start_binary_called = True
|
||||
|
||||
yield _start_binary
|
||||
|
||||
qemu.kill()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qemu_start_binary():
|
||||
"""
|
||||
Returns function that launches given binary with 'starti' command
|
||||
|
||||
Argument `path` is the path to the binary
|
||||
"""
|
||||
|
||||
qemu: subprocess.Popen = None
|
||||
|
||||
if QEMU_PORT is None:
|
||||
print("'QEMU_PORT' environment variable not set")
|
||||
sys.stdout.flush()
|
||||
os._exit(1)
|
||||
|
||||
def _start_binary(path: str, arch: str, *args):
|
||||
nonlocal qemu
|
||||
|
||||
qemu = subprocess.Popen(
|
||||
[
|
||||
f"qemu-{arch}",
|
||||
"-L",
|
||||
f"/usr/{arch}-linux-gnu/",
|
||||
"-g",
|
||||
f"{QEMU_PORT}",
|
||||
f"{path}",
|
||||
]
|
||||
)
|
||||
|
||||
gdb.execute(f"target remote :{QEMU_PORT}")
|
||||
gdb.execute("set exception-verbose on")
|
||||
|
||||
global _start_binary_called
|
||||
# if _start_binary_called:
|
||||
# raise Exception('Starting more than one binary is not supported in pwndbg tests.')
|
||||
|
||||
_start_binary_called = True
|
||||
|
||||
yield _start_binary
|
||||
|
||||
qemu.kill()
|
||||
@ -1,36 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
TESTS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "tests/system")
|
||||
|
||||
|
||||
class CollectTestFunctionNames:
|
||||
"""See https://github.com/pytest-dev/pytest/issues/2039#issuecomment-257753269"""
|
||||
|
||||
def __init__(self):
|
||||
self.collected = []
|
||||
|
||||
def pytest_collection_modifyitems(self, items):
|
||||
for item in items:
|
||||
self.collected.append(item.nodeid)
|
||||
|
||||
|
||||
collector = CollectTestFunctionNames()
|
||||
rv = pytest.main(["--collect-only", TESTS_PATH], plugins=[collector])
|
||||
|
||||
if rv == pytest.ExitCode.INTERRUPTED:
|
||||
print("Failed to collect all tests, perhaps there is a syntax error in one of test files?")
|
||||
sys.stdout.flush()
|
||||
os._exit(1)
|
||||
|
||||
|
||||
print("Listing collected tests:")
|
||||
for nodeid in collector.collected:
|
||||
print("Test:", nodeid)
|
||||
|
||||
# easy way to exit GDB session
|
||||
sys.exit(0)
|
||||
@ -1,32 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
use_pdb = os.environ.get("USE_PDB") == "1"
|
||||
|
||||
sys._pwndbg_unittest_run = True
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
test = os.environ["PWNDBG_LAUNCH_TEST"]
|
||||
|
||||
test = os.path.join(CURRENT_DIR, test)
|
||||
|
||||
args = [test, "-vvv", "-s", "--showlocals", "--color=yes"]
|
||||
|
||||
if use_pdb:
|
||||
args.append("--pdb")
|
||||
|
||||
print(f"Launching pytest with args: {args}")
|
||||
|
||||
return_code = pytest.main(args)
|
||||
|
||||
if return_code != 0:
|
||||
print("-" * 80)
|
||||
print("If you want to debug tests locally, run ./tests.sh with the --pdb flag")
|
||||
print("-" * 80)
|
||||
|
||||
sys.exit(return_code)
|
||||
@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
make -C binaries
|
||||
|
||||
ROOT_DIR="$(readlink -f ../../)"
|
||||
GDB_INIT_PATH="$ROOT_DIR/gdbinit.py"
|
||||
COVERAGERC_PATH="$ROOT_DIR/pyproject.toml"
|
||||
|
||||
handle_sigint() {
|
||||
echo "Exiting..." >&2
|
||||
pkill qemu-aarch64
|
||||
pkill qemu-riscv64
|
||||
exit 1
|
||||
}
|
||||
trap handle_sigint SIGINT
|
||||
|
||||
gdb_load_pwndbg=(--command "$GDB_INIT_PATH" -ex "set exception-verbose on")
|
||||
run_gdb() {
|
||||
COVERAGE_FILE=$ROOT_DIR/.cov/coverage \
|
||||
COVERAGE_PROCESS_START=$COVERAGERC_PATH \
|
||||
PWNDBG_DISABLE_COLORS=1 \
|
||||
gdb-multiarch --silent --nx --nh "${gdb_load_pwndbg[@]}" "$@" -ex "quit" 2> /dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
test_arch() {
|
||||
local arch="$1"
|
||||
|
||||
qemu-${arch} \
|
||||
-g 1234 \
|
||||
-L /usr/${arch}-linux-gnu/ \
|
||||
./binaries/reference-binary.${arch}.out &
|
||||
|
||||
run_gdb \
|
||||
-ex "set sysroot /usr/${arch}-linux-gnu/" \
|
||||
-ex "file ./binaries/reference-binary.${arch}.out" \
|
||||
-ex 'py import coverage;coverage.process_startup()' \
|
||||
-ex "target remote :1234" \
|
||||
-ex "source ./tests/user/test_${arch}.py"
|
||||
local result=$?
|
||||
pkill qemu-${arch}
|
||||
return $result
|
||||
}
|
||||
|
||||
ARCHS=("aarch64" "riscv64")
|
||||
|
||||
FAILED_TESTS=()
|
||||
for arch in "${ARCHS[@]}"; do
|
||||
test_arch "$arch"
|
||||
if [ $? -ne 0 ]; then
|
||||
FAILED_TESTS+=("$arch")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#FAILED_TESTS[@]}" -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Failing tests: ${FAILED_TESTS[@]}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from . import binaries
|
||||
@ -0,0 +1,19 @@
|
||||
.PHONY: all
|
||||
all: reference-binary.aarch64.out reference-binary.riscv64.out
|
||||
|
||||
%.aarch64.out : %.aarch64.c
|
||||
@echo "[+] Building '$@'"
|
||||
@aarch64-linux-gnu-gcc $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $< $(LDFLAGS)
|
||||
|
||||
%.riscv64.out : %.riscv64.c
|
||||
@echo "[+] Building '$@'"
|
||||
@riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -g $(CFLAGS) $(EXTRA_FLAGS) -w -o $@ $? $(LDFLAGS)
|
||||
|
||||
AARCH64_SOURCES := $(wildcard *.aarch64.c)
|
||||
AARCH64_TARGETS := $(AARCH64_SOURCES:.aarch64.c=.aarch64.out)
|
||||
|
||||
RISCV64_SOURCES := $(wildcard *.riscv64.c)
|
||||
RISCV64_TARGETS := $(RISCV64_SOURCES:.riscv64.c=.riscv64.out)
|
||||
|
||||
clean:
|
||||
rm -f *.aarch64.out *.x86_64.out *.arm.out
|
||||
@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
path = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def get(x):
|
||||
return os.path.join(path, x)
|
||||
@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import gdb
|
||||
from capstone.arm64_const import ARM64_INS_BL
|
||||
|
||||
import pwndbg.gdblib.disasm
|
||||
import pwndbg.gdblib.nearpc
|
||||
from pwndbg.gdblib.disasm.instruction import InstructionCondition
|
||||
|
||||
AARCH64_GRACEFUL_EXIT = """
|
||||
mov x0, 0
|
||||
mov x8, 93
|
||||
svc 0
|
||||
"""
|
||||
|
||||
SIMPLE_FUNCTION = f"""
|
||||
|
||||
bl my_function
|
||||
b end
|
||||
|
||||
my_function:
|
||||
ret
|
||||
|
||||
end:
|
||||
{AARCH64_GRACEFUL_EXIT}
|
||||
"""
|
||||
|
||||
|
||||
def test_syscall_annotation(qemu_assembly_run):
|
||||
""" """
|
||||
qemu_assembly_run(AARCH64_GRACEFUL_EXIT, "aarch64")
|
||||
|
||||
instructions = pwndbg.gdblib.disasm.near(
|
||||
address=pwndbg.gdblib.regs.pc, instructions=3, emulate=True
|
||||
)[0]
|
||||
future_syscall_ins = instructions[2]
|
||||
|
||||
assert future_syscall_ins.syscall == 93
|
||||
assert future_syscall_ins.syscall_name == "exit"
|
||||
|
||||
gdb.execute("stepuntilasm svc")
|
||||
|
||||
# Both for emulation and non-emulation, ensure a syscall at current PC gets enriched
|
||||
instructions = pwndbg.gdblib.disasm.emulate_one(), pwndbg.gdblib.disasm.no_emulate_one()
|
||||
|
||||
for i in instructions:
|
||||
assert i.syscall == 93
|
||||
assert i.syscall_name == "exit"
|
||||
|
||||
|
||||
def test_branch_enhancement(qemu_assembly_run):
|
||||
qemu_assembly_run(SIMPLE_FUNCTION, "aarch64")
|
||||
|
||||
instruction = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert instruction.id == ARM64_INS_BL
|
||||
assert instruction.call_like
|
||||
assert not instruction.is_conditional_jump
|
||||
assert instruction.is_unconditional_jump
|
||||
assert instruction.target_string == "my_function"
|
||||
|
||||
|
||||
CONDITIONAL_JUMPS = f"""
|
||||
mov x2, 0b1010
|
||||
mov x3, 0
|
||||
|
||||
cbz x3, A
|
||||
nop
|
||||
|
||||
A:
|
||||
cbnz x2, B
|
||||
nop
|
||||
|
||||
B:
|
||||
tbz x2, #0, C
|
||||
nop
|
||||
|
||||
C:
|
||||
tbnz x2, #3, D
|
||||
nop
|
||||
|
||||
D:
|
||||
cmp x2, x3
|
||||
b.eq E
|
||||
nop
|
||||
|
||||
E:
|
||||
b.ne F
|
||||
nop
|
||||
|
||||
F:
|
||||
{AARCH64_GRACEFUL_EXIT}
|
||||
"""
|
||||
|
||||
|
||||
def test_conditional_jumps(qemu_assembly_run):
|
||||
qemu_assembly_run(CONDITIONAL_JUMPS, "aarch64")
|
||||
|
||||
gdb.execute("stepuntilasm cbz")
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.TRUE
|
||||
|
||||
gdb.execute("si")
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.TRUE
|
||||
|
||||
gdb.execute("si")
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.TRUE
|
||||
|
||||
gdb.execute("si")
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.TRUE
|
||||
|
||||
gdb.execute("si")
|
||||
gdb.execute("si")
|
||||
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.FALSE
|
||||
|
||||
gdb.execute("si")
|
||||
gdb.execute("si")
|
||||
|
||||
ins = pwndbg.gdblib.disasm.one_with_config()
|
||||
|
||||
assert ins.condition == InstructionCondition.TRUE
|
||||
|
||||
|
||||
def test_conditional_jumps_no_emulate(qemu_assembly_run):
|
||||
gdb.execute("set emulate off")
|
||||
test_conditional_jumps(qemu_assembly_run)
|
||||
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
COV=0
|
||||
# Run unit tests
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" == "--cov" ]; then
|
||||
COV=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $COV -eq 1 ]; then
|
||||
coverage run -m pytest tests/unit-tests
|
||||
else
|
||||
pytest tests/unit-tests
|
||||
fi
|
||||
|
||||
exit_code=$((exit_code + $?))
|
||||
|
||||
exit $exit_code
|
||||
Loading…
Reference in new issue