Skip to content

Commit a669c30

Browse files
hugsyGrazfather
andauthored
Migrate tests to RPyC (#97)
## Description/Motivation/Screenshots This PR is the GEF-Extras counter-part of hugsy/gef#1040 Also since `kallsyms` was removed from GEF (in preparation for the kernel module), it was added here with its tests ## How Has This Been Tested ? "Tested" indicates that the PR works *and* the unit test (i.e. `make test`) run passes without issue. * [x] x86-32 * [x] x86-64 * [ ] ARM * [x] AARCH64 * [ ] MIPS * [ ] POWERPC * [ ] SPARC * [ ] RISC-V ## Checklist <!-- N.B.: Your patch won't be reviewed unless fulfilling the following base requirements: --> <!--- Put an `x` in all the boxes that are complete, or that don't apply --> * [x] My code follows the code style of this project. * [x] My change includes a change to the documentation, if required. * [x] If my change adds new code, [adequate tests](https://hugsy.github.io/gef/testing) have been added. * [x] I have read and agree to the [CONTRIBUTING](https://github.com/hugsy/gef/blob/main/.github/CONTRIBUTING.md) document. --------- Co-authored-by: Grazfather <grazfather@gmail.com>
1 parent 4b98e62 commit a669c30

File tree

18 files changed

+524
-363
lines changed

18 files changed

+524
-363
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: CI Test for GEF-EXTRAS
22

33
env:
44
BRANCH: main
5+
NB_CPU: 1
56

67
on:
78
push:
@@ -66,30 +67,18 @@ jobs:
6667
6768
- name: Checkout GEF
6869
run: |
69-
mkdir -p ${{ env.GEF_PATH_DIR }}
70-
wget -O ${{ env.GEF_PATH }} https://raw.githubusercontent.com/hugsy/gef/${{ env.BRANCH }}/gef.py
70+
git clone -b ${{ env.BRANCH }} https://github.com/hugsy/gef ${{ env.GEF_PATH_DIR }}
7171
echo "source ${{ env.GEF_PATH }}" > ~/.gdbinit
7272
gdb -q -ex 'gef missing' -ex 'gef help' -ex 'gef config' -ex start -ex continue -ex quit /bin/pwd
7373
74-
- name: Build config file
74+
- name: Setup Tests
7575
run: |
76-
gdb -q \
77-
-ex "gef config pcustom.struct_path '$(pwd)/structs'" \
78-
-ex "gef config syscall-args.path '$(pwd)/syscall-tables'" \
79-
-ex "gef config context.libc_args True" \
80-
-ex "gef config context.libc_args_path '$(pwd)/glibc-function-args'" \
81-
-ex 'gef save' \
82-
-ex quit
76+
make -C tests/binaries -j ${{ env.NB_CPU }}
8377
8478
- name: Run Tests
8579
run: |
86-
make -C tests/binaries -j ${{ env.NB_CPU }}
8780
python${{ env.PY_VER }} -m pytest --forked -n ${{ env.NB_CPU }} -v -k "not benchmark" tests/
8881
89-
- name: Run linter
90-
run: |
91-
python${{ env.PY_VER }} -m pylint --rcfile=$(pwd)/.pylintrc gef.py tests/*/*.py
92-
9382
standalone:
9483
runs-on: ubuntu-latest
9584
name: "Verify GEF-Extras install from gef/scripts"

.github/workflows/validate.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ name: Validation
22

33
on:
44
pull_request:
5-
5+
branches:
6+
- main
67

78
jobs:
89
pre_commit:
910
name: Check formatting
1011
runs-on: ubuntu-latest
1112
steps:
1213
- uses: actions/checkout@v3
13-
- uses: actions/setup-python@v3
14+
- uses: actions/setup-python@v5.0.0
15+
with:
16+
python-version: "3.8"
1417
- uses: pre-commit/action@v3.0.0
1518

1619
docs_link_check:
@@ -20,9 +23,9 @@ jobs:
2023
contents: read
2124
steps:
2225
- name: checkout
23-
uses: actions/checkout@v2
26+
uses: actions/checkout@v4
2427
- name: Check links
25-
uses: lycheeverse/lychee-action@v1.4.1
28+
uses: lycheeverse/lychee-action@v1.9.1
2629
env:
2730
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
2831
with:

scripts/__init__.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ def register_external_context_pane(pane_name: str, display_pane_function: Callab
814814
def pane_title() -> str: ...
815815

816816

817-
def register(cls: Type["GenericCommand"]) -> Type["GenericCommand"]: ...
817+
def register(cls: Union[Type["GenericCommand"], Type["GenericFunction"]]) -> Union[Type["GenericCommand"], Type["GenericFunction"]]: ...
818818

819819

820820
class GenericCommandBase:

scripts/assemble.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
__AUTHOR__ = "hugsy"
32
__VERSION__ = 0.2
43
__LICENSE__ = "MIT"

scripts/kernel/__init__.py

Whitespace-only changes.

scripts/kernel/symbols.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
Collection of functions and commands to manipulate kernel symbols
3+
"""
4+
5+
__AUTHOR__ = "hugsy"
6+
__VERSION__ = 0.1
7+
__LICENSE__ = "MIT"
8+
9+
import argparse
10+
from typing import TYPE_CHECKING, Any, List
11+
12+
if TYPE_CHECKING:
13+
from .. import * # this will allow linting for GEF and GDB
14+
15+
16+
@register
17+
class SolveKernelSymbolCommand(GenericCommand):
18+
"""Solve kernel symbols from kallsyms table."""
19+
20+
_cmdline_ = "ksymaddr"
21+
_syntax_ = f"{_cmdline_} SymbolToSearch"
22+
_example_ = f"{_cmdline_} prepare_creds"
23+
24+
@parse_arguments({"symbol": ""}, {})
25+
def do_invoke(self, _: List[str], **kwargs: Any) -> None:
26+
def hex_to_int(num):
27+
try:
28+
return int(num, 16)
29+
except ValueError:
30+
return 0
31+
32+
args: argparse.Namespace = kwargs["arguments"]
33+
if not args.symbol:
34+
self.usage()
35+
return
36+
sym = args.symbol
37+
with open("/proc/kallsyms", "r") as f:
38+
syms = [line.strip().split(" ", 2) for line in f]
39+
matches = [
40+
(hex_to_int(addr), sym_t, " ".join(name.split()))
41+
for addr, sym_t, name in syms
42+
if sym in name
43+
]
44+
for addr, sym_t, name in matches:
45+
if sym == name.split()[0]:
46+
ok(f"Found matching symbol for '{name}' at {addr:#x} (type={sym_t})")
47+
else:
48+
warn(
49+
f"Found partial match for '{sym}' at {addr:#x} (type={sym_t}): {name}"
50+
)
51+
if not matches:
52+
err(f"No match for '{sym}'")
53+
elif matches[0][0] == 0:
54+
err(
55+
"Check that you have the correct permissions to view kernel symbol addresses"
56+
)
57+
return

scripts/libc_function_args/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ class GlibcFunctionArguments:
3131
@staticmethod
3232
def load_libc_args() -> bool:
3333
"""Load the LIBC function arguments. Returns `True` on success, `False` or an Exception otherwise."""
34-
global gef
3534

3635
# load libc function arguments' definitions
3736
path = pathlib.Path(

tests/base.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import os
2+
import pathlib
3+
import random
4+
import subprocess
5+
import tempfile
6+
import time
7+
import unittest
8+
9+
import rpyc
10+
11+
from .utils import GEF_EXTRAS_SCRIPTS_PATH, debug_target
12+
13+
COVERAGE_DIR = os.getenv("COVERAGE_DIR", "")
14+
GEF_PATH = pathlib.Path(os.getenv("GEF_PATH", "../gef/gef.py")).absolute()
15+
RPYC_GEF_PATH = GEF_PATH.parent / "scripts/remote_debug.py"
16+
RPYC_HOST = "localhost"
17+
RPYC_PORT = 18812
18+
RPYC_SPAWN_TIME = 1.0
19+
20+
21+
class RemoteGefUnitTestGeneric(unittest.TestCase):
22+
"""
23+
The base class for GEF test cases. This will create the `rpyc` environment to programmatically interact with
24+
GDB and GEF in the test.
25+
"""
26+
27+
def setUp(self) -> None:
28+
self._coverage_file = None
29+
if not hasattr(self, "_target"):
30+
setattr(self, "_target", debug_target("default"))
31+
else:
32+
assert isinstance(self._target, pathlib.Path) # type: ignore pylint: disable=E1101
33+
assert self._target.exists() # type: ignore pylint: disable=E1101
34+
self._port = random.randint(1025, 65535)
35+
self._commands = ""
36+
37+
if COVERAGE_DIR:
38+
self._coverage_file = pathlib.Path(COVERAGE_DIR) / os.getenv(
39+
"PYTEST_XDIST_WORKER", "gw0"
40+
)
41+
self._commands += f"""
42+
pi import coverage
43+
pi cov = coverage.Coverage(data_file="{self._coverage_file}", auto_data=True, branch=True)
44+
pi cov.start()
45+
"""
46+
47+
self._commands += f"""
48+
source {GEF_PATH}
49+
gef config gef.debug True
50+
gef config gef.propagate_debug_exception True
51+
gef config gef.disable_color True
52+
53+
gef config gef.extra_plugins_dir {GEF_EXTRAS_SCRIPTS_PATH}
54+
55+
source {RPYC_GEF_PATH}
56+
pi start_rpyc_service({self._port})
57+
"""
58+
59+
self._initfile = tempfile.NamedTemporaryFile(mode="w", delete=False)
60+
self._initfile.write(self._commands)
61+
self._initfile.flush()
62+
self._command = [
63+
"gdb",
64+
"-q",
65+
"-nx",
66+
"-ex",
67+
f"source {self._initfile.name}",
68+
"--",
69+
str(self._target.absolute()), # type: ignore pylint: disable=E1101
70+
]
71+
self._process = subprocess.Popen(self._command)
72+
assert self._process.pid > 0
73+
time.sleep(RPYC_SPAWN_TIME)
74+
self._conn = rpyc.connect(
75+
RPYC_HOST,
76+
self._port,
77+
)
78+
self._gdb = self._conn.root.gdb
79+
self._gef = self._conn.root.gef
80+
return super().setUp()
81+
82+
def tearDown(self) -> None:
83+
if COVERAGE_DIR:
84+
self._gdb.execute("pi cov.stop()")
85+
self._gdb.execute("pi cov.save()")
86+
self._conn.close()
87+
self._process.terminate()
88+
return super().tearDown()

tests/commands/capstone_disassemble.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,55 @@
33
"""
44

55
import pytest
6+
from tests.base import RemoteGefUnitTestGeneric
67

7-
from tests.utils import (ARCH, GefUnitTestGeneric, gdb_run_cmd,
8-
gdb_start_silent_cmd, removeuntil)
8+
from tests.utils import (
9+
ARCH,
10+
ERROR_INACTIVE_SESSION_MESSAGE,
11+
removeuntil,
12+
)
913

1014

11-
@pytest.mark.skipif(ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}")
12-
class CapstoneDisassembleCommand(GefUnitTestGeneric):
15+
@pytest.mark.skipif(
16+
ARCH in ("mips64el", "ppc64le", "riscv64"), reason=f"Skipped for {ARCH}"
17+
)
18+
class CapstoneDisassembleCommand(RemoteGefUnitTestGeneric):
1319
"""`capstone-disassemble` command test module"""
1420

1521
def setUp(self) -> None:
1622
try:
1723
import capstone # pylint: disable=W0611
1824
except ImportError:
19-
pytest.skip("capstone-engine not available",
20-
allow_module_level=True)
25+
pytest.skip("capstone-engine not available", allow_module_level=True)
2126
return super().setUp()
2227

2328
def test_cmd_capstone_disassemble(self):
24-
self.assertFailIfInactiveSession(gdb_run_cmd("capstone-disassemble"))
25-
res = gdb_start_silent_cmd("capstone-disassemble")
26-
self.assertNoException(res)
27-
self.assertTrue(len(res.splitlines()) > 1)
28-
29-
self.assertFailIfInactiveSession(
30-
gdb_run_cmd("capstone-disassemble --show-opcodes"))
31-
res = gdb_start_silent_cmd(
32-
"capstone-disassemble --show-opcodes --length 5 $pc")
33-
self.assertNoException(res)
34-
self.assertTrue(len(res.splitlines()) >= 5)
29+
gdb = self._gdb
30+
cmd = "capstone-disassemble"
31+
32+
self.assertEqual(
33+
ERROR_INACTIVE_SESSION_MESSAGE, gdb.execute(cmd, to_string=True)
34+
)
35+
36+
gdb.execute("start")
37+
res = gdb.execute("capstone-disassemble", to_string=True) or ""
38+
assert res
39+
40+
cmd = "capstone-disassemble --show-opcodes"
41+
res = gdb.execute(cmd, to_string=True) or ""
42+
assert res
43+
44+
cmd = "capstone-disassemble --show-opcodes --length 5 $pc"
45+
res = gdb.execute(cmd, to_string=True) or ""
46+
assert res
47+
48+
lines = res.splitlines()
49+
self.assertGreaterEqual(len(lines), 5)
50+
3551
# jump to the output buffer
3652
res = removeuntil("→ ", res, included=True)
37-
addr, opcode, symbol, *_ = [x.strip()
38-
for x in res.splitlines()[2].strip().split()]
53+
addr, opcode, symbol, *_ = [x.strip() for x in lines[2].strip().split()]
54+
3955
# match the correct output format: <addr> <opcode> [<symbol>] mnemonic [operands,]
4056
# gef➤ cs --show-opcodes --length 5 $pc
4157
# → 0xaaaaaaaaa840 80000090 <main+20> adrp x0, #0xaaaaaaaba000
@@ -49,6 +65,7 @@ def test_cmd_capstone_disassemble(self):
4965
self.assertTrue(int(opcode, 16))
5066
self.assertTrue(symbol.startswith("<") and symbol.endswith(">"))
5167

52-
res = gdb_start_silent_cmd("cs --show-opcodes main")
53-
self.assertNoException(res)
54-
self.assertTrue(len(res.splitlines()) > 1)
68+
cmd = "cs --show-opcodes main"
69+
res = gdb.execute(cmd, to_string=True) or ""
70+
assert res
71+
self.assertGreater(len(res.splitlines()), 1)

tests/commands/kernel/ksymaddr.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
`ksymaddr` command test module
3+
"""
4+
5+
6+
from tests.base import RemoteGefUnitTestGeneric
7+
8+
9+
class KsymaddrCommand(RemoteGefUnitTestGeneric):
10+
"""`ksymaddr` command test module"""
11+
12+
cmd = "ksymaddr"
13+
14+
def test_cmd_ksymaddr(self):
15+
gdb = self._gdb
16+
res = gdb.execute(f"{self.cmd} prepare_kernel_cred", to_string=True)
17+
self.assertIn("Found matching symbol for 'prepare_kernel_cred'", res)

0 commit comments

Comments
 (0)