feat(errno): resolve errno values for the requested context (#2788) (#2789)

`pwn errno` only ever consulted the host's errno module, so numbers from a
target of a different architecture were reported as unknown (131 is
ECONNRESET on mips but unused here).

Add -c/--context, built on the same context_arg helper as asm, constgrep and
cyclic: number and name lookups now resolve against pwnlib.constants for the
requested os/arch, and --list/--search honour the flag too. --search also
matches errno names, so the flag stays useful on hosts whose strerror cannot
spell out the target's codes.

Covered with three inline doctests, as TESTING.md asks for.
This commit is contained in:
Eapp1e authored and GitHub committed 2026-11-02 08:02:18 +01:00
1 parent b83a0ddc8c
commit 01f5cf08d7
3 files changed
+123 -23

No files matched your search

+2
View File
@@ -76,6 +76,7 @@ The table below shows which release corresponds to each branch, and what date th
## 5.0.0 (`dev`)
- [#2780][2780] fix(encoders): handle unavailable xor encodings and fallback
- [#2789][2789] feat(errno): support `-c/--context` to look up errno values for other architectures
- [#2784][2784] docs: put plain doctest blocks in the default group for Sphinx 9
- [#2762][2762] fix(srop): correct amd64 SigreturnFrame `uc_sigmask` offset
- [#2753][2753] docs(args): clarify reserved args (DEBUG/NOASLR) map to context, not args
@@ -233,6 +234,7 @@ The table below shows which release corresponds to each branch, and what date th
[2768]: https://github.com/Gallopsled/pwntools/pull/2768
[2769]: https://github.com/Gallopsled/pwntools/pull/2769
[2780]: https://github.com/Gallopsled/pwntools/pull/2780
[2789]: https://github.com/Gallopsled/pwntools/pull/2789
[2750]: https://github.com/Gallopsled/pwntools/pull/2750
[2761]: https://github.com/Gallopsled/pwntools/pull/2761
[2727]: https://github.com/Gallopsled/pwntools/pull/2727
+6 -1
View File
@@ -5,7 +5,8 @@
.. testcleanup:: *
context.defaults.copy = old
context.defaults.clear()
context.defaults.update(old)
Command Line Tools
========================
@@ -18,3 +19,7 @@ If these tools do not appear to be installed, make sure that you have added ``~/
.. autoprogram:: pwnlib.commandline.main:parser
:prog: pwn
.. only:: doctest
.. automodule:: pwnlib.commandline.errno
+115 -22
View File
@@ -1,8 +1,23 @@
"""Look up ``errno`` values for the architecture selected by ``-c``.
The examples below run in the documentation doctest suite.
>>> from pwnlib.context import context
>>> from pwnlib.commandline.errno import _context_table
>>> with context.local(arch='amd64', os='linux'):
... _context_table()['ECONNRESET']
104
>>> with context.local(arch='mips', os='linux'):
... _context_table()['ECONNRESET']
131
"""
from argparse import Namespace
from typing import Generator
import os
import errno
import pwnlib.constants as constants
from pwnlib.context import context
from pwnlib.commandline import common
parser = common.parser_commands.add_parser(
@@ -21,16 +36,76 @@ parser.add_argument(
)
parser.add_argument(
'-s', '--search', action='store_true',
help='Treat the argument as a perror substring and print every matching errno.'
help='Treat the argument as an errno name or perror substring and print every match.'
)
parser.add_argument(
'-c', '--context',
metavar = 'context',
type = common.context_arg,
help = 'Look the errno values up for this architecture or os, e.g. `-c aarch64`.',
)
def _iter_known_errnos() -> Generator[tuple[int, str, str], None, None]:
"""Yield ``(value, name, message)`` tuples for every errno code visible to
the standard library on this platform, sorted by numeric value.
def _host_table() -> dict[str, int]:
"""Return ``{name: value}`` for every errno the host standard library knows."""
return {name: getattr(errno, name) for name in dir(errno)
if name.isupper() and isinstance(getattr(errno, name), int)}
def _context_table() -> dict[str, int]:
"""Return ``{name: value}`` for the errno numbers ``context`` uses."""
if context.os == 'freebsd':
module = getattr(constants, 'freebsd', None)
else:
module = getattr(getattr(constants, context.os, None), context.arch, None)
if module is None:
return {}
table = {}
for name in _host_table():
value = getattr(module, name, None)
if value is not None:
table[name] = int(getattr(value, 'value', value))
return table
def _message(name: str) -> str:
"""Return the perror string of ``name`` according to the host, or ``''``."""
value = getattr(errno, name, None)
if not isinstance(value, int):
return ''
try:
return os.strerror(value)
except ValueError:
return ''
def _iter_known_errnos(table: dict[str, int]) -> Generator[tuple[int, str, str], None, None]:
"""Yield ``(value, name, message)`` tuples sorted by numeric value.
Aliases sharing a number are reported once: the canonical name from
`errno.errorcode` wins when it maps to the same value, otherwise the
first name alphabetically does.
>>> with context.local(arch='aarch64', os='linux'):
... [(value, name) for value, name, _ in _iter_known_errnos(_context_table())][:3]
[(1, 'EPERM'), (2, 'ENOENT'), (3, 'ESRCH')]
"""
for value, name in sorted(errno.errorcode.items()):
yield value, name, os.strerror(value)
# ``errno.errorcode`` is the canonical name for a number, so prefer it over the
# alphabetically first alias to keep the default listing identical to before.
canonical = {value: name for value, name in errno.errorcode.items() if table.get(name) == value}
seen = set()
for name, value in sorted(table.items(), key=lambda item: (item[1], item[0])):
if value in seen:
continue
seen.add(value)
name = canonical.get(value, name)
yield value, name, _message(name)
def _print_errno(value: int, name: str | None = None, message: str | None = None) -> None:
@@ -42,18 +117,37 @@ def _print_errno(value: int, name: str | None = None, message: str | None = None
print('#define %s %d' % (name, value))
else:
print('#define <unknown> %d' % value)
print(message)
if message:
print(message)
def _search_errnos(needle: str) -> list[tuple[int, str, str]]:
"""Return errnos whose ``strerror`` text contains ``needle`` (case-insensitive)."""
def _search_errnos(needle: str, table: dict[str, int]) -> list[tuple[int, str, str]]:
"""Return errnos whose name or ``strerror`` text contains ``needle``.
The match is case-insensitive, so ``pwn errno -c mips -s conn`` finds the
connection-oriented errnos even when the host cannot spell them out.
>>> with context.local(arch='mips', os='linux'):
... [(value, name) for value, name, _ in _search_errnos('econnreset', _context_table())]
[(131, 'ECONNRESET')]
"""
needle = needle.lower()
return [(v, n, m) for v, n, m in _iter_known_errnos() if needle in m.lower()]
return [(v, n, m) for v, n, m in _iter_known_errnos(table)
if needle in n.lower() or needle in m.lower()]
def main(args: Namespace) -> None:
if args.context is None:
table = _host_table()
else:
table = _context_table()
if not table:
print('No errno values known for %s/%s' % (context.os, context.arch))
return
if args.list:
for value, name, message in _iter_known_errnos():
for value, name, message in _iter_known_errnos(table):
print('%-3d %-10s %s' % (value, name, message))
return
@@ -61,7 +155,7 @@ def main(args: Namespace) -> None:
parser.error('the following arguments are required: error (or pass --list)')
if args.search:
matches = _search_errnos(args.error)
matches = _search_errnos(args.error, table)
if not matches:
print("No errno matched %r" % args.error)
return
@@ -79,22 +173,20 @@ def main(args: Namespace) -> None:
if 0x100000000 - value < 0x200:
value = 0x100000000 - value
if value not in errno.errorcode:
name = next((n for n, v in sorted(table.items()) if v == value), None)
if name is None:
print("No errno for %s" % value)
return
name = errno.errorcode[value]
except ValueError:
candidate = args.error.upper()
name = args.error.upper()
value = table.get(name)
if hasattr(errno, candidate):
name = candidate
value = getattr(errno, name)
else:
if value is None:
# Fall back to a substring search against perror messages so
# ``pwn errno 'Cannot allocate memory'`` resolves to ENOMEM.
matches = _search_errnos(args.error)
matches = _search_errnos(args.error, table)
if not matches:
print("No errno for %s" % args.error)
return
@@ -104,8 +196,9 @@ def main(args: Namespace) -> None:
print()
return
_print_errno(value, name)
_print_errno(value, name, _message(name))
if __name__ == '__main__':
common.main(__file__, main)