Add fedora tests & docker (#3476)

* add fedora41

* fix

* fix

* fix

* fix launched_sleep_binary

* fix lint

* dnf fix for py3.14

* rm env PIP_NO_CACHE_DIR

* remove duplicate tests: test_context_commands.py

* cleanup mallocng tests

* lint

* fix test

* fix lint

* enable test test_mallocng_find

* fix rip

* fix ptmalloc crash?

* fix ptmalloc crash?

---------

Co-authored-by: k4lizen <124312252+k4lizen@users.noreply.github.com>
dev
patryk4815 21 hours ago committed by GitHub
parent 662b0010d6
commit 4fc51a71c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
images: [ubuntu22.04, ubuntu24.04, debian12, archlinux]
images: [ubuntu22.04, ubuntu24.04, debian12, archlinux, fedora41, fedora42, fedora43]
runs-on: ubuntu-latest
timeout-minutes: 30
@ -45,7 +45,7 @@ jobs:
strategy:
fail-fast: false
matrix:
images: [ubuntu24.04]
images: [ubuntu24.04, fedora41, fedora42, fedora43]
runs-on: ubuntu-24.04-arm
timeout-minutes: 30

@ -13,7 +13,6 @@ FROM $image AS base
WORKDIR /pwndbg
ENV PIP_NO_CACHE_DIR=true
ENV LANG=en_US.utf8
ENV TZ=America/New_York
ENV PWNDBG_VENV_PATH=/venv

@ -13,7 +13,6 @@ FROM $image
WORKDIR /pwndbg
ENV PIP_NO_CACHE_DIR=true
ENV LANG=en_US.utf8
ENV TZ=America/New_York
ENV PWNDBG_VENV_PATH=/venv

@ -0,0 +1,55 @@
# This dockerfile was created for development & testing purposes, for DNF-based distro.
#
# Build as: docker build -f Dockerfile.dnf -t pwndbg .
#
# For testing use: docker run --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwndbg bash
#
# For development, mount the directory so the host changes are reflected into container:
# docker run -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v `pwd`:/pwndbg pwndbg bash
#
ARG image=fedora:41
FROM $image
WORKDIR /pwndbg
ENV LANG=en_US.utf8
ENV TZ=America/New_York
ENV PWNDBG_VENV_PATH=/venv
ENV UV_PROJECT_ENVIRONMENT=/venv
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
dnf -y install \
glibc-langpack-en \
glibc-locale-source \
vim-minimal && \
localedef -i en_US -f UTF-8 en_US.UTF-8 && \
dnf clean all && \
rm -rf /var/cache/dnf
# setup.sh needs scripts/common.sh
COPY ./scripts/common.sh /pwndbg/scripts/
COPY ./setup.sh /pwndbg/
COPY ./uv.lock /pwndbg/
COPY ./pyproject.toml /pwndbg/
# pyproject.toml requires these files, pip install would fail
RUN touch README.md && mkdir pwndbg && touch pwndbg/empty.py
# fix for py3.14
# RUN dnf install -y uv git python3-devel patch ncurses-devel gcc
# RUN uv sync --all-groups --all-extras
RUN dnf install -y uv git python3-devel patch ncurses-devel gcc
RUN ./setup.sh
# Comment these lines if you won't run the tests.
COPY ./setup-dev.sh /pwndbg/
RUN ./setup-dev.sh
# Cleanup dummy files
RUN rm README.md && rm -rf pwndbg
COPY . /pwndbg/
ENV PATH="${PWNDBG_VENV_PATH}/bin:${PATH}"

@ -52,3 +52,27 @@ services:
dockerfile: Dockerfile.arch
args:
image: archlinux:latest
fedora41:
<<: *base-spec
build:
context: .
dockerfile: Dockerfile.dnf
args:
image: fedora:41
fedora42:
<<: *base-spec
build:
context: .
dockerfile: Dockerfile.dnf
args:
image: fedora:42
fedora43:
<<: *base-spec
build:
context: .
dockerfile: Dockerfile.dnf
args:
image: fedora:43

@ -513,10 +513,9 @@ class Heap:
raise ValueError(f"Cannot build heap object on an unmapped address ({hex(addr)})")
heap_info = allocator.get_heap(addr)
try:
ar_ptr = None
if heap_info is not None:
ar_ptr = int(heap_info["ar_ptr"])
except pwndbg.dbg_mod.Error:
ar_ptr = None
if ar_ptr is not None and ar_ptr in (ar.address for ar in allocator.arenas):
# Case 2; non-main arena.
@ -1713,7 +1712,10 @@ class DebugSymsHeap(GlibcMemoryAllocator[pwndbg.dbg_mod.Type, pwndbg.dbg_mod.Val
"""Find & read the heap_info struct belonging to the chunk at 'addr'."""
if self.heap_info is None:
return None
return pwndbg.aglib.memory.get_typed_pointer_value(self.heap_info, heap_for_ptr(addr))
haddr = heap_for_ptr(addr)
if pwndbg.aglib.memory.peek(haddr) is None:
return None
return pwndbg.aglib.memory.get_typed_pointer_value(self.heap_info, haddr)
def get_tcache(
self, tcache_addr: int | pwndbg.dbg_mod.Value | None = None

@ -158,12 +158,15 @@ EOF
install_dnf() {
sudo dnf upgrade || true
sudo dnf install -y \
make \
nasm \
gcc \
curl \
wget \
gdb \
musl-gcc \
g++ \
parallel \
qemu-system-x86 \
qemu-system-arm \
qemu-user
@ -176,7 +179,7 @@ install_dnf() {
command -v go &> /dev/null || sudo dnf install -y go
if [[ "$1" != "" ]]; then
sudo dnf install shfmt
sudo dnf install -y shfmt
fi
}

@ -38,18 +38,18 @@ async def test_mallocng_slot_user(ctrl: Controller, binary: str):
expected_output = [
"slab",
f" group: {re_addr} ",
f" meta: {re_addr} ",
rf" group: {re_addr}\s+",
rf" meta: {re_addr}\s+",
"general",
f" start: {re_addr} ",
f" user start: {re_addr} aka `p`",
rf" end: {re_addr} start \+ stride - 4",
rf" start: {re_addr}\s+",
rf" user start: {re_addr}\s+aka `p`",
rf" end: {re_addr}\s+start \+ stride - 4",
" stride: 0x60 distance between adjacent slots",
""" user size: 0x50 aka "nominal size", `n`""",
r" slack: 0x0 \(0x0\) slot's unused memory \/ 0x10",
" state: allocated ",
"in-band",
r" offset: 0x[0-9] \(0x[0-9]{0,1}0\) distance to first slot start \/ 0x10",
r" offset: 0x[0-9]\s+\(0x[0-9]{0,1}0\)\s+distance to first slot start \/ 0x10",
r" index: 0x0 index of slot in its group",
" hdr reserved: 0x5 describes: end - p - n",
" use ftr reserved",
@ -217,16 +217,16 @@ async def test_mallocng_group(ctrl: Controller, binary: str):
expected_out = [
"group",
f" @ {re_addr} - {re_addr}",
f" meta: {re_addr} ",
rf" meta: {re_addr}\s+",
" active_idx: 0x4 ",
f" storage: {re_addr} start of slots",
rf" storage: {re_addr}\s+start of slots",
"---",
" group size: 0x1f0 ",
"meta",
f" @ {re_addr}",
f" prev: {re_addr} ",
f" next: {re_addr} ",
f" mem: {re_addr} the group",
rf" prev: {re_addr}\s+",
rf" next: {re_addr}\s+",
rf" mem: {re_addr}\s+the group",
" avail_mask: 0x18 0b00000000000000000000000000011000",
" freed_mask: 0x0 0b00000000000000000000000000000000",
r" last_idx: 0x4 \(cnt: 0x5\) index of last slot",
@ -346,11 +346,8 @@ async def test_mallocng_find(ctrl: Controller, binary: str):
await launch_to(ctrl, binary, "break_here")
await ctrl.finish()
if pwndbg.aglib.arch.name != "x86-64":
pytest.skip("TODO multiarch")
# Check no slot found
find_out = color.strip(await ctrl.execute_and_capture("ng-find $rip"))
find_out = color.strip(await ctrl.execute_and_capture("ng-find $pc"))
assert "No slot found containing that address.\n" == find_out
buffer1_addr = int(pwndbg.dbg.selected_frame().evaluate_expression("buffer1"))

@ -1,585 +0,0 @@
from __future__ import annotations
import re
import gdb
import pytest
import pwndbg.aglib.memory
import pwndbg.aglib.regs
import pwndbg.commands
import pwndbg.commands.canary
import pwndbg.commands.context
from . import get_binary
REFERENCE_BINARY = get_binary("reference-binary.native.out")
USE_FDS_BINARY = get_binary("use-fds.native.out")
TABSTOP_BINARY = get_binary("tabstop.native.out")
SYSCALLS_BINARY = get_binary("syscalls.x86-64.out")
MANGLING_BINARY = get_binary("symbol_1600_and_752.native.out")
def test_context_disasm_show_fd_filepath(start_binary):
"""
Tests context disasm command and whether it shows properly opened fd filepath
"""
start_binary(USE_FDS_BINARY)
# Run until main
gdb.execute("break main")
gdb.execute("continue")
# Stop on read(0, ...) -> should show /dev/pts/X or pipe:X on CI
gdb.execute("nextcall")
out = pwndbg.commands.context.context_disasm()
assert "[ DISASM / x86-64 / set emulate on ]" in out[0] # Sanity check
call_read_line_idx = out.index(next(line for line in out if "<read@plt>" in line))
lines_after_call_read = out[call_read_line_idx:]
line_call_read, line_fd, line_buf, line_nbytes, *_rest = lines_after_call_read
assert "call read@plt" in line_call_read
# When running tests with GNU Parallel, sometimes the file name looks
# '/tmp/parZ4YC4.par', and occasionally '(deleted)' is present after the
# filename
line_fd = line_fd.strip()
assert re.match(
r"fd:\s+1 \((/dev/pts/\d+|/tmp/par.+\.par(?: \(deleted\))?|pipe:\[\d+\])\)", line_fd
)
line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0", line_nbytes)
# Stop on open(...)
gdb.execute("nextcall")
# Stop on read(...) -> should show use-fds.out
gdb.execute("nextcall")
out = pwndbg.commands.context.context_disasm()
assert "[ DISASM / x86-64 / set emulate on ]" in out[0] # Sanity check
call_read_line_idx = out.index(next(line for line in out if "<read@plt>" in line))
lines_after_call_read = out[call_read_line_idx:]
line_call_read, line_fd, line_buf, line_nbytes, *_rest = lines_after_call_read
line_fd = line_fd.strip()
assert re.match(r"fd:\s+3 \(.*?/tests/binaries/host/use-fds.native.out\)", line_fd)
line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0x10", line_nbytes)
@pytest.mark.parametrize("sections", ("''", '""', "none", "-", ""))
def test_empty_context_sections(start_binary, sections):
start_binary(USE_FDS_BINARY)
# Sanity check
default_ctx_sects = "regs disasm code ghidra stack backtrace expressions threads heap_tracker"
assert pwndbg.config.context_sections.value == default_ctx_sects
assert gdb.execute("context", to_string=True) != ""
# Actual test check
gdb.execute(f"set context-sections {sections}", to_string=True)
assert pwndbg.config.context_sections.value == ""
assert gdb.execute("context", to_string=True) == ""
# Bring back old values && sanity check
gdb.execute(f"set context-sections {default_ctx_sects}")
assert pwndbg.config.context_sections.value == default_ctx_sects
assert gdb.execute("context", to_string=True) != ""
def test_source_code_tabstop(start_binary):
start_binary(TABSTOP_BINARY)
# Run until line 6
gdb.execute("break tabstop.native.c:6")
gdb.execute("continue")
# Default context-code-tabstop = 8
src = gdb.execute("context code", to_string=True)
assert """ 1 #include <stdio.h>\n""" in src
assert """ 2 \n""" in src
assert """ 3 int main() {\n""" in src
assert """ 4 // test mix indent\n""" in src
assert """ 5 do {\n""" in src
assert """ 6 puts("tab line");\n""" in src
assert """ 7 } while (0);\n""" in src
assert """ 8 return 0;\n""" in src
assert """ 9 }\n""" in src
assert """10 \n""" in src
# Test context-code-tabstop = 2
gdb.execute("set context-code-tabstop 2")
src = gdb.execute("context code", to_string=True)
assert """ 1 #include <stdio.h>\n""" in src
assert """ 2 \n""" in src
assert """ 3 int main() {\n""" in src
assert """ 4 // test mix indent\n""" in src
assert """ 5 do {\n""" in src
assert """ 6 puts("tab line");\n""" in src
assert """ 7 } while (0);\n""" in src
assert """ 8 return 0;\n""" in src
assert """ 9 }\n""" in src
assert """10 \n""" in src
# Disable context-code-tabstop
gdb.execute("set context-code-tabstop 0")
src = gdb.execute("context code", to_string=True)
assert """ 1 #include <stdio.h>\n""" in src
assert """ 2 \n""" in src
assert """ 3 int main() {\n""" in src
assert """ 4 \t// test mix indent\n""" in src
assert """ 5 do {\n""" in src
assert """ 6 \t\tputs("tab line");\n""" in src
assert """ 7 } while (0);\n""" in src
assert """ 8 return 0;\n""" in src
assert """ 9 }\n""" in src
assert """10 \n""" in src
def test_context_disasm_syscalls_args_display(start_binary):
start_binary(SYSCALLS_BINARY)
gdb.execute("nextsyscall")
dis = gdb.execute("context disasm", to_string=True)
assert dis == (
"LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n"
"──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n"
" 0x400080 <_start> mov eax, 0 EAX => 0\n"
" 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n"
" 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n"
" 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n"
" ► 0x400094 <_start+20> syscall <SYS_read>\n"
" fd: 0x1337\n"
" buf: 0xdeadbeef\n"
" nbytes: 0\n"
" 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n"
" 0x40009b <_start+27> int 0x80 <SYS_unlink>\n"
" 0x40009d add byte ptr [rax], al\n"
" 0x40009f add byte ptr [rax], al\n"
" 0x4000a1 add byte ptr [rax], al\n"
" 0x4000a3 add byte ptr [rax], al\n"
"────────────────────────────────────────────────────────────────────────────────\n"
)
gdb.execute("nextsyscall")
dis = gdb.execute("context disasm", to_string=True)
assert dis == (
"LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n"
"──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n"
" 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n"
" 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n"
" 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n"
" 0x400094 <_start+20> syscall <SYS_read>\n"
" 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n"
" ► 0x40009b <_start+27> int 0x80 <SYS_unlink>\n"
" name: 0x1337\n"
" 0x40009d add byte ptr [rax], al\n"
" 0x40009f add byte ptr [rax], al\n"
" 0x4000a1 add byte ptr [rax], al\n"
" 0x4000a3 add byte ptr [rax], al\n"
" 0x4000a5 add byte ptr [rax], al\n"
"────────────────────────────────────────────────────────────────────────────────\n"
)
def test_context_disasm_syscalls_args_display_no_emulate(start_binary):
gdb.execute("set emulate off")
start_binary(SYSCALLS_BINARY)
gdb.execute("nextsyscall")
dis = gdb.execute("context disasm", to_string=True)
assert dis == (
"LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n"
"─────────────────────[ DISASM / x86-64 / set emulate off ]──────────────────────\n"
" 0x400080 <_start> mov eax, 0 EAX => 0\n"
" 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n"
" 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n"
" 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n"
" ► 0x400094 <_start+20> syscall <SYS_read>\n"
" fd: 0x1337\n"
" buf: 0xdeadbeef\n"
" nbytes: 0\n"
" 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n"
" 0x40009b <_start+27> int 0x80 <SYS_unlink>\n"
" 0x40009d add byte ptr [rax], al\n"
" 0x40009f add byte ptr [rax], al\n"
" 0x4000a1 add byte ptr [rax], al\n"
" 0x4000a3 add byte ptr [rax], al\n"
"────────────────────────────────────────────────────────────────────────────────\n"
)
gdb.execute("nextsyscall")
dis = gdb.execute("context disasm", to_string=True)
assert dis == (
"LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n"
"─────────────────────[ DISASM / x86-64 / set emulate off ]──────────────────────\n"
" 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n"
" 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n"
" 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n"
" 0x400094 <_start+20> syscall <SYS_read>\n"
" 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n"
" ► 0x40009b <_start+27> int 0x80 <SYS_unlink>\n"
" name: 0x1337\n"
" 0x40009d add byte ptr [rax], al\n"
" 0x40009f add byte ptr [rax], al\n"
" 0x4000a1 add byte ptr [rax], al\n"
" 0x4000a3 add byte ptr [rax], al\n"
" 0x4000a5 add byte ptr [rax], al\n"
"────────────────────────────────────────────────────────────────────────────────\n"
)
def test_context_backtrace_show_proper_symbol_names(start_binary):
start_binary(MANGLING_BINARY)
gdb.execute("break A::foo")
gdb.execute("continue")
backtrace = gdb.execute("context backtrace", to_string=True).split("\n")
assert backtrace[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA"
assert (
backtrace[1]
== "─────────────────────────────────[ BACKTRACE ]──────────────────────────────────"
)
assert re.match(r".*0 0x[0-9a-f]+ A::foo\(int, int\)", backtrace[2])
# Match A::call_foo()+38 or similar: the offset may change so we match \d+ at the end
assert re.match(r".*1 0x[0-9a-f]+ A::call_foo\(\)\+\d+", backtrace[3])
# Match main+87 or similar offset
assert re.match(r".*2 0x[0-9a-f]+ main\+\d+", backtrace[4])
# Match __libc_start_main+243 or similar offset
# Note: on Ubuntu 22.04 there will be __libc_start_call_main and then __libc_start_main
# but on older distros there will be only __libc_start_main
# Let's not bother too much about it and make it the last call assertion here
assert re.match(
r".*3 0x[0-9a-f]+ (__libc_start_main|__libc_start_call_main)\+\d+", backtrace[5]
)
assert (
backtrace[-2]
== "────────────────────────────────────────────────────────────────────────────────"
)
assert backtrace[-1] == ""
def test_context_disasm_works_properly_with_disasm_flavor_switch(start_binary):
start_binary(SYSCALLS_BINARY)
def assert_intel(out):
assert "mov eax, 0" in out[2]
assert "mov edi, 0x1337" in out[3]
assert "mov esi, 0xdeadbeef" in out[4]
assert "mov ecx, 0x10" in out[5]
assert "syscall" in out[6]
def assert_att(out):
assert "mov movl $0, %eax" not in out[2]
assert "mov movl $0x1337, %edi" not in out[3]
assert "mov movl $0xdeadbeef, %esi" not in out[4]
assert "mov movl $0x10, %ecx" not in out[5]
assert "syscall" in out[6]
out = gdb.execute("context disasm", to_string=True).split("\n")
assert out[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA"
assert (
out[1] == "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────"
)
assert_intel(out)
gdb.execute("set disassembly-flavor att")
assert out[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA"
assert (
out[1] == "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────"
)
assert_att(out)
@pytest.mark.parametrize("patch_or_api", (True, False))
def test_context_disasm_proper_render_on_mem_change_issue_1818(start_binary, patch_or_api):
start_binary(SYSCALLS_BINARY)
old = gdb.execute("context disasm", to_string=True).split("\n")
# Just a sanity check
assert old[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA"
assert "mov eax, 0" in old[2]
assert "mov edi, 0x1337" in old[3]
assert "mov esi, 0xdeadbeef" in old[4]
assert "mov ecx, 0x10" in old[5]
assert "syscall" in old[6]
# 5 bytes because 'mov eax, 0' is 5 bytes long
if patch_or_api:
gdb.execute("patch $rip nop;nop;nop;nop;nop", to_string=True)
else:
# Do the same, but through write API
pwndbg.aglib.memory.write(pwndbg.aglib.regs.pc, b"\x90" * 5)
# Actual test: we expect the read memory to be different now ;)
# (and not e.g. returned incorrectly from a not cleared cache)
new = gdb.execute("context disasm", to_string=True).split("\n")
assert new[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA"
assert "nop" in new[2]
assert "nop" in new[3]
assert "nop" in new[4]
assert "nop" in new[5]
assert "nop" in new[6]
assert "mov edi, 0x1337" in new[7]
assert "mov esi, 0xdeadbeef" in new[8]
assert "mov ecx, 0x10" in new[9]
assert "syscall" in new[10]
ONE_GADGET_BINARY = get_binary("onegadget.x86-64.out")
def test_context_disasm_fsbase_annotations(start_binary):
"""
This test checks that fsbase support in annotations is working properly.
If this breaks, either our x86 memory operand parser is broken, we cannot fetch fsbase, or we are not passing FSBASE to Unicorn.
See: https://github.com/pwndbg/pwndbg/pull/2317
For this test, we use a binary we know has a stack canary.
Between compilations and between x86 vs x86_64, the exact instruction changes, but matches a regex pattern.
"""
start_binary(ONE_GADGET_BINARY)
gdb.execute("b break_here")
gdb.execute("c")
# In view, there should now be the fs/gs memory reference
output = gdb.execute("context disasm", to_string=True).split("\n")
pattern = re.compile(r"\b(mov|sub)\s+\w+,\s+(qword|dword)\s+ptr\s+(gs|fs):\[0x[0-9a-f]+\]")
found = False
for line in output:
if pattern.search(line):
found = True
break
assert found
LONG_FUNCTION_X64_BINARY = get_binary("long_function.x86-64.out")
def test_context_disasm_call_instruction_split(start_binary):
"""
This checks for the following scenario:
We are on a `call` instruction, and `si` to enter the function. Then, we do `fin` to return to the caller.
There should be a split in the disassembly after the call instruction.
"""
start_binary(LONG_FUNCTION_X64_BINARY)
gdb.execute("start")
# Call ctx so instructions get disassembled and cached
gdb.execute("ctx")
gdb.execute("si")
gdb.execute("fin")
dis = gdb.execute("context disasm", to_string=True)
dis = pwndbg.color.strip(dis)
expected = (
"LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n"
"──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n"
" 0x400080 <_start> call function <function>\n"
" \n"
" ► 0x400085 <_start+5> mov eax, 2 EAX => 2\n"
" 0x40008a <_start+10> mov ebx, 3 EBX => 3\n"
" 0x40008f <_start+15> add rax, rbx RAX => 5 (2 + 3)\n"
" 0x400092 <_start+18> xor rax, rbx RAX => 6 (5 ^ 3)\n"
" 0x400095 <_start+21> nop \n"
" 0x400096 <_start+22> jmp exit <exit>\n"
"\n"
" 0x4000ab <exit> mov eax, 0x3c EAX => 0x3c\n"
" 0x4000b0 <exit+5> mov edi, 0 EDI => 0\n"
" 0x4000b5 <exit+10> syscall <SYS_exit>\n"
" 0x4000b7 add byte ptr [rax], al\n"
"────────────────────────────────────────────────────────────────────────────────\n"
)
assert dis == expected
def test_context_hide_sections(start_binary):
start_binary(SYSCALLS_BINARY)
# Disable one section
out = gdb.execute("context", to_string=True)
assert "REGISTERS" in out
assert "STACK" in out
gdb.execute("context regs --off")
out = gdb.execute("context", to_string=True)
assert "REGISTERS" not in out
assert "STACK" in out
gdb.execute("context regs --on")
out = gdb.execute("context", to_string=True)
assert "REGISTERS" in out
assert "STACK" in out
# Disable multiple sections
gdb.execute("context stack disasm --off")
out = gdb.execute("context", to_string=True)
assert "STACK" not in out
assert "DISASM" not in out
gdb.execute("context stack --on")
out = gdb.execute("context", to_string=True)
assert "STACK" in out
assert "DISASM" not in out
gdb.execute("context stack disasm --on")
out = gdb.execute("context", to_string=True)
assert "STACK" in out
assert "DISASM" in out
# Disable all sections at once
gdb.execute("context --off")
out = gdb.execute("context", to_string=True)
assert len(out) == 0
gdb.execute("context --on")
out = gdb.execute("context", to_string=True)
assert "REGISTERS" in out
assert "DISASM" in out
def test_context_history_prev_next(start_binary):
start_binary(LONG_FUNCTION_X64_BINARY)
# Add two context outputs to the history
first_ctx = gdb.execute("ctx", to_string=True)
gdb.execute("si")
second_ctx = gdb.execute("ctx", to_string=True)
assert first_ctx != second_ctx
# Go back to the first context
gdb.execute("contextprev")
history_ctx = gdb.execute("ctx", to_string=True)
assert first_ctx == history_ctx.replace(" (history 1/2)", "")
assert "(history 1/2)" in history_ctx
# Go to the second context again
gdb.execute("contextnext")
history_ctx = gdb.execute("ctx", to_string=True)
assert second_ctx == history_ctx.replace(" (history 2/2)", "")
assert "(history 2/2)" in history_ctx
# Make sure new events are displayed right away
# and disable the history scroll.
gdb.execute("si")
# Execute twice since the prompt hook isn't installed in tests
# which causes the legend to still have the (history 2/2) string at first.
gdb.execute("ctx", to_string=True)
third_ctx = gdb.execute("ctx", to_string=True)
assert history_ctx != third_ctx
assert "(history " not in third_ctx
# Check if cwatch expressions are also stored in the history
gdb.execute("cwatch $rip")
gdb.execute("cwatch execute 'p/z $rsp'")
fourth_ctx = gdb.execute("ctx", to_string=True)
assert "1: $rip = " in fourth_ctx
assert "2: p/z $rsp\n$1 = 0x" in fourth_ctx
# The next context shows a different output variable $2
gdb.execute("si")
fifth_ctx = gdb.execute("ctx", to_string=True)
assert "1: $rip = " in fifth_ctx
assert "2: p/z $rsp\n$2 = 0x" in fifth_ctx
# Check that the expression section shows the old gdb variable $1 again.
gdb.execute("contextprev")
history_ctx = gdb.execute("ctx", to_string=True)
assert "1: $rip = " in history_ctx
assert "2: p/z $rsp\n$1 = 0x" in history_ctx
gdb.execute("cunwatch 2")
gdb.execute("cunwatch 1")
def test_context_history_search(start_binary):
start_binary(REFERENCE_BINARY)
gdb.execute("break main")
gdb.execute("break break_here")
gdb.execute("starti")
gdb.execute("context")
gdb.execute("continue")
gdb.execute("context")
gdb.execute("continue")
gdb.execute("context")
for _ in range(5):
gdb.execute("ni")
gdb.execute("context")
# Search for something in the past
search_result = gdb.execute("contextsearch puts@plt", to_string=True)
assert "Found 1 match. Selected entry 2 for match in section 'disasm'." in search_result
# Search for something that happened later and have the search wrap around
search_result = gdb.execute("contextsearch 'Hello World'", to_string=True)
assert "No more matches before the current entry. Starting from the top." in search_result
assert "Found 7 matches. Selected entry 8 for match in section " in search_result
search_result = gdb.execute("contextsearch 'Hello World'", to_string=True)
assert "Found 7 matches. Selected entry 7 for match in section " in search_result
# Select a section to search in
search_result = gdb.execute("contextsearch 'Hello World' disasm", to_string=True)
assert "Found 1 match. Selected entry 2 for match in section 'disasm'." in search_result
# Search for something that doesn't exist
search_result = gdb.execute("contextsearch 'nonexistent'", to_string=True)
assert "String 'nonexistent' not found in context history." in search_result
# Search in non-existing section
search_result = gdb.execute("ctxsearch 'Hello World' nonexistent", to_string=True)
assert "Section 'nonexistent' not found in context history." in search_result
def test_context_output_redirection(start_binary):
start_binary(REFERENCE_BINARY)
# Test CallOutput redirection
def receive_output(output):
receive_output.context_output = output
receive_output.context_output = ""
pwndbg.commands.context.contextoutput(
"regs",
receive_output,
clearing=True,
banner="top",
width=80,
)
gdb.execute("start")
out = gdb.execute("ctx", to_string=True)
assert "REGISTERS" not in out
assert "STACK" in out
assert "REGISTERS" in receive_output.context_output
assert "STACK" not in receive_output.context_output
pwndbg.commands.context.resetcontextoutput("regs")
Loading…
Cancel
Save