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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Run integration tests
|
# Run integration tests
|
||||||
(cd tests/gdb-tests && python3 tests.py $@)
|
(cd tests && python3 tests.py $@)
|
||||||
exit_code=$?
|
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
|
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