Some enhancements about `pwndbg.gdblib.config` (#1315)

* Add more features for `pwndbg.gdblib.config`

* Support all parameter-class

* Use `get_show_string` to render better output when using `show <param>`

* Show more information when using `help set <param>` and `help show <param>` if we create a config with `help_docstring` parameter.

Some examples of the updates included in this commit:

1. `gdb.PARAM_AUTO_BOOLEAN` with `help_docstring`

In Python script:

```
pwndbg.gdblib.config.add_param(
    "test",
    None,
    "test",
    "on == AAAA\noff == BBBB\nauto == CCCC",
    gdb.PARAM_AUTO_BOOLEAN,
    scope="test",
)
```

In GDB:

```
pwndbg> show test
The current value of 'test' is 'auto'
pwndbg> set test on
Set test to 'on'
pwndbg> set test off
Set test to 'off'
pwndbg> set test auto_with_typo
"on", "off" or "auto" expected.
pwndbg> show test
The current value of 'test' is 'off'
pwndbg> set test auto
Set test to 'auto'
pwndbg> show test
The current value of 'test' is 'auto'
pwndbg> help show test
Show test
on == AAAA
off == BBBB
auto == CCCC
pwndbg> help set test
Set test
on == AAAA
off == BBBB
auto == CCCC
```

2. `gdb.PARAM_AUTO_BOOLEAN` with `help_docstring`

In Python script:

```
pwndbg.gdblib.config.add_param(
    "test",
    "A",
    "test",
    "A == AAAA\nB == BBBB\nC == CCCC",
    gdb.PARAM_ENUM,
    ["A", "B", "C"],
    scope="test",
)
```

In GDB:

```
pwndbg> show test
The current value of 'test' is 'A'
pwndbg> set test B
Set test to 'B'
pwndbg> set test C
Set test to 'C'
pwndbg> set test D
Undefined item: "D".
pwndbg> show test
The current value of 'test' is 'C'
pwndbg> help show test
Show test
A == AAAA
B == BBBB
C == CCCC
pwndbg> help set test
Set test
A == AAAA
B == BBBB
C == CCCC
```

* Update the tests for gdblib parameter

* Use auto boolean for `safe-linking`

* Fix some comments

* Pass `help_docstring` directly

* Force callers of `add_param` to use keyword arguments

* Create `add_heap_param()` to avoid setting the scope of param everytime
pull/1317/head
Alan Li 3 years ago committed by GitHub
parent 7efaa33b0c
commit 5b56071746
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,7 +7,7 @@ class ColorParameter(pwndbg.lib.config.Parameter):
def add_param(name, default, docstring, color_param=False):
return config.add_param(name, default, docstring, "theme")
return config.add_param(name, default, docstring, scope="theme")
def add_color_param(name, default, docstring):

@ -21,47 +21,52 @@ import pwndbg.lib.config
config = pwndbg.lib.config.Config()
PARAM_CLASSES = {
# The Python boolean values, True and False are the only valid values.
bool: gdb.PARAM_BOOLEAN,
# This is like PARAM_INTEGER, except 0 is interpreted as itself.
int: gdb.PARAM_ZINTEGER,
# When the user modifies the string, any escape sequences,
# such as \t, \f, and octal escapes, are translated into
# corresponding characters and encoded into the current host charset.
str: gdb.PARAM_STRING,
}
# See this for details about the API of `gdb.Parameter`:
# https://sourceware.org/gdb/onlinedocs/gdb/Parameters-In-Python.html
class Parameter(gdb.Parameter):
def __init__(self, param: pwndbg.lib.config.Parameter):
# `set_doc` and `show_doc` must be set before `gdb.Parameter.__init__`.
# `set_doc`, `show_doc`, and `__doc__` must be set before `gdb.Parameter.__init__`.
# They will be used for `help set <param>` and `help show <param>`,
# respectively
self.set_doc = "Set " + param.docstring
self.show_doc = "Show " + param.docstring
param_class = PARAM_CLASSES[type(param.value)]
super().__init__(param.name, gdb.COMMAND_SUPPORT, param_class)
self.__doc__ = param.help_docstring
if param.param_class == gdb.PARAM_ENUM:
super().__init__(
param.name,
gdb.COMMAND_SUPPORT,
param.param_class,
param.enum_sequence,
)
else:
super().__init__(param.name, gdb.COMMAND_SUPPORT, param.param_class)
self.param = param
self.value = param.value
@property
def native_value(self):
return Parameter._value_to_gdb_native(self.param.value)
return Parameter._value_to_gdb_native(self.param.value, param_class=self.param.param_class)
@property
def native_default(self):
return Parameter._value_to_gdb_native(self.param.default)
return Parameter._value_to_gdb_native(
self.param.default, param_class=self.param.param_class
)
def get_set_string(self):
"""Handles the GDB `set <param>` command"""
# GDB will set `self.value` to the user's input
self.param.value = self.value
if self.value is None and self.param.param_class in (gdb.PARAM_UINTEGER, gdb.PARAM_INTEGER):
# Note: This is really weird, according to GDB docs, 0 should mean "unlimited" for gdb.PARAM_UINTEGER and gdb.PARAM_INTEGER, but somehow GDB sets the value to `None` actually :/
# And hilarious thing is that GDB won't let you set the default value to `None` when you construct the `gdb.Parameter` object with `gdb.PARAM_UINTEGER` or `gdb.PARAM_INTEGER` lol
# Maybe it's a bug of GDB?
# Anyway, to avoid some unexpected behaviors, we'll still set `self.param.value` to 0 here.
self.param.value = 0
else:
self.param.value = self.value
for trigger in config.triggers[self.param.name]:
trigger()
@ -71,15 +76,32 @@ class Parameter(gdb.Parameter):
if not pwndbg.decorators.first_prompt:
return ""
return "Set %s to %r" % (self.param.docstring, self.param.value)
return "Set %s to %r" % (self.param.docstring, self.native_value)
def get_show_string(self, svalue):
"""Handles the GDB `show <param>` command"""
return "The current value of %r is %r" % (
self.param.name,
Parameter._value_to_gdb_native(svalue, self.param.param_class),
)
@staticmethod
def _value_to_gdb_native(value):
def _value_to_gdb_native(value, param_class=None):
"""Translates Python value into native GDB syntax string."""
# Convert booleans to "on" or "off". Other types pass through normally
if isinstance(value, bool):
# Convert booleans to "on" or "off".
return "on" if value else "off"
elif value is None and param_class == gdb.PARAM_AUTO_BOOLEAN:
# None for gdb.PARAM_AUTO_BOOLEAN means "auto".
return "auto"
elif value == 0 and param_class in (gdb.PARAM_UINTEGER, gdb.PARAM_INTEGER):
# 0 for gdb.PARAM_UINTEGER and gdb.PARAM_INTEGER means "unlimited".
return "unlimited"
elif value == -1 and param_class == gdb.PARAM_ZUINTEGER_UNLIMITED:
# -1 for gdb.PARAM_ZUINTEGER_UNLIMITED means "unlimited".
return "unlimited"
# Other types pass through normally
return value

@ -18,7 +18,10 @@ import pwndbg.lib.memoize
import pwndbg.search
safe_lnk = pwndbg.gdblib.config.add_param(
"safe-linking", "auto", "whether glibc use safe-linking (on/off/auto)"
"safe-linking",
None,
"whether glibc use safe-linking (on/off/auto)",
param_class=gdb.PARAM_AUTO_BOOLEAN,
)
glibc_version = pwndbg.gdblib.config.add_param(
@ -92,4 +95,4 @@ def check_safe_linking():
- https://lanph3re.blogspot.com/2020/08/blog-post.html
- https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/
"""
return (get_version() >= (2, 32) or safe_lnk == "on") and safe_lnk != "off"
return (get_version() >= (2, 32) or safe_lnk) and safe_lnk is not False

@ -5,31 +5,37 @@ import pwndbg.heap.heap
current = None
main_arena = pwndbg.gdblib.config.add_param("main-arena", "0", "&main_arena for heuristics", "heap")
thread_arena = pwndbg.gdblib.config.add_param(
"thread-arena", "0", "*thread_arena for heuristics", "heap"
)
def add_heap_param(
name, default, docstring, *, help_docstring=None, param_class=None, enum_sequence=None
):
return pwndbg.gdblib.config.add_param(
name,
default,
docstring,
help_docstring=help_docstring,
param_class=param_class,
enum_sequence=enum_sequence,
scope="heap",
)
mp_ = pwndbg.gdblib.config.add_param("mp", "0", "&mp_ for heuristics", "heap")
tcache = pwndbg.gdblib.config.add_param("tcache", "0", "*tcache for heuristics", "heap")
main_arena = add_heap_param("main-arena", "0", "&main_arena for heuristics")
global_max_fast = pwndbg.gdblib.config.add_param(
"global-max-fast", "0", "&global_max_fast for heuristics", "heap"
)
thread_arena = add_heap_param("thread-arena", "0", "*thread_arena for heuristics")
mp_ = add_heap_param("mp", "0", "&mp_ for heuristics")
tcache = add_heap_param("tcache", "0", "*tcache for heuristics")
global_max_fast = add_heap_param("global-max-fast", "0", "&global_max_fast for heuristics")
symbol_list = [main_arena, thread_arena, mp_, tcache, global_max_fast]
heap_chain_limit = pwndbg.gdblib.config.add_param(
"heap-dereference-limit", 8, "number of bins to dereference", "heap"
)
heap_chain_limit = add_heap_param("heap-dereference-limit", 8, "number of bins to dereference")
resolve_heap_via_heuristic = pwndbg.gdblib.config.add_param(
"resolve-heap-via-heuristic",
False,
"Resolve missing heap related symbols via heuristics",
"heap",
resolve_heap_via_heuristic = add_heap_param(
"resolve-heap-via-heuristic", False, "Resolve missing heap related symbols via heuristics"
)

@ -2,16 +2,41 @@ import collections
from functools import total_ordering
from typing import List
import gdb
PARAM_CLASSES = {
# The Python boolean values, True and False are the only valid values.
bool: gdb.PARAM_BOOLEAN,
# This is like PARAM_INTEGER, except 0 is interpreted as itself.
int: gdb.PARAM_ZINTEGER,
# When the user modifies the string, any escape sequences,
# such as \t, \f, and octal escapes, are translated into
# corresponding characters and encoded into the current host charset.
str: gdb.PARAM_STRING,
}
# @total_ordering allows us to implement `__eq__` and `__lt__` and have all the
# other comparison operators handled for us
@total_ordering
class Parameter:
def __init__(self, name, default, docstring, scope="config"):
def __init__(
self,
name,
default,
docstring,
help_docstring=None,
param_class=None,
enum_sequence=None,
scope="config",
):
self.docstring = docstring.strip()
self.help_docstring = help_docstring.strip() if help_docstring else None
self.name = name
self.default = default
self.value = default
self.param_class = param_class or PARAM_CLASSES[type(default)]
self.enum_sequence = enum_sequence
self.scope = scope
@property
@ -97,11 +122,29 @@ class Config:
self.params = {}
self.triggers = collections.defaultdict(lambda: [])
def add_param(self, name, default, docstring, scope="config"):
def add_param(
self,
name,
default,
docstring,
*,
help_docstring=None,
param_class=None,
enum_sequence=None,
scope="config",
):
# Dictionary keys are going to have underscores, so we can't allow them here
assert "_" not in name
p = Parameter(name, default, docstring, scope)
p = Parameter(
name,
default,
docstring,
help_docstring,
param_class,
enum_sequence,
scope,
)
return self.add_param_obj(p)
def add_param_obj(self, p: Parameter):

@ -6,31 +6,60 @@ import pwndbg.gdblib.config
@pytest.mark.parametrize(
"params",
(("int", 123, "123"), ("bool", True, "on"), ("string", "some-string-val", "some-string-val")),
(
("int", 123, "123", {}),
("bool", True, "on", {}),
("bool", False, "off", {}),
("string", "some-string-val", "some-string-val", {}),
("auto-bool", None, "auto", {"param_class": gdb.PARAM_AUTO_BOOLEAN}),
("unlimited-uint", 0, "unlimited", {"param_class": gdb.PARAM_UINTEGER}),
("unlimited-int", 0, "unlimited", {"param_class": gdb.PARAM_INTEGER}),
("unlimited-zuint", -1, "unlimited", {"param_class": gdb.PARAM_ZUINTEGER_UNLIMITED}),
(
"enum",
"enum1",
"enum1",
{"param_class": gdb.PARAM_ENUM, "enum_sequence": ["enum1", "enum2", "enum3"]},
),
),
)
def test_gdb_parameter_default_value_works(start_binary, params):
name_suffix, default_value, displayed_value = params
name_suffix, default_value, displayed_value, optional_kwargs = params
param_name = f"test-param-{name_suffix}"
help_docstring = f"Help docstring for {param_name}"
param = pwndbg.gdblib.config.add_param(param_name, default_value, "some show string")
param = pwndbg.gdblib.config.add_param(
param_name,
default_value,
"some show string",
help_docstring=help_docstring,
**optional_kwargs,
)
# Initialize and register param in GDB as if it would be done by gdblib.config.init_params
pwndbg.gdblib.config_mod.Parameter(param)
out = gdb.execute(f"show {param_name}", to_string=True)
assert out in (
f"""The current value of '{param_name}' is "{displayed_value}".\n""", # GDB 12.x
f"Show some show string {displayed_value}\n", # GDB 9.x
)
assert gdb.parameter(param_name) == default_value
assert out == f"The current value of {param_name!r} is {displayed_value!r}\n"
if (
optional_kwargs.get("param_class") in (gdb.PARAM_UINTEGER, gdb.PARAM_INTEGER)
and default_value == 0
):
# Note: This is really weird, according to GDB docs, 0 should mean "unlimited" for gdb.PARAM_UINTEGER and gdb.PARAM_INTEGER, but somehow GDB sets the value to `None` actually :/
# And hilarious thing is that GDB won't let you set the default value to `None` when you construct the `gdb.Parameter` object with `gdb.PARAM_UINTEGER` or `gdb.PARAM_INTEGER` lol
# Maybe it's a bug of GDB?
# Anyway, to avoid some unexpected behaviors, we still set pwndbg's Parameter object's value to 0 in `get_set_string()` and `__init__()`
assert gdb.parameter(param_name) is None
else:
assert gdb.parameter(param_name) == default_value
assert param.value == default_value
# TODO/FIXME: We need to add documentation
out = gdb.execute(f"help show {param_name}", to_string=True)
assert out == "Show some show string\nThis command is not documented.\n"
assert out == f"Show some show string\n{help_docstring}\n"
assert (
gdb.execute(f"help set {param_name}", to_string=True)
== "Set some show string\nThis command is not documented.\n"
== f"Set some show string\n{help_docstring}\n"
)
# TODO/FIXME: Is there a way to unregister a GDB parameter defined in Python?

Loading…
Cancel
Save