Skip to content

fix(packing): dd() raises TypeError on a bytearray destination - #2772

Merged
peace-maker merged 3 commits into
Gallopsled:stablefrom
MaxFreedomPollard:fix-dd-bytearray-dst
Oct 24, 2026
Merged

peace-maker merged 3 commits into
Gallopsled:stablefrom
MaxFreedomPollard:fix-dd-bytearray-dst

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

dd() lists bytearray among its supported destination types, and has an explicit isinstance(dst, ..., bytearray) branch for it, but every bytearray destination raises TypeError on 4.15.0.

>>> from pwn import *
>>> dd(bytearray(b'Hello!'), b'?', skip=5)
Traceback (most recent call last):
  ...
    dst[skip : skip + len(src)] = list(map(p8, bytearray(src)))
    ~~~^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'bytes' object cannot be interpreted as an integer

The cause is the shared list/bytearray branch at

elif isinstance(dst, (list, bytearray)):
dst[skip : skip + len(src)] = list(map(p8, bytearray(src)))
, which fills both destination types with list(map(p8, bytearray(src))). p8() returns a length-1 bytes object, which is exactly what a list destination is documented to hold, as the neighbouring dd(list('Hello!'), (63,), skip = 5) doctest shows. A bytearray slice assignment needs an iterable of int, so it rejects those bytes objects. The other four destination types all work: list, tuple, bytes and str return ['H', 'e', 'l', 'l', 'o', b'?'], ('H', 'e', 'l', 'l', 'o', b'?'), b'Hello?' and 'Hello?' respectively.

The fix assigns the byte string src directly when the destination is a bytearray. Every src branch above the destination dispatch, at packing.py:1039-1089, has already reduced src to a byte string, so the slice assignment is always valid, and this makes a bytearray destination agree with the bytes and str destinations, which already copy src verbatim. dd(bytearray(b'Hello!'), b'?', skip=5) now returns bytearray(b'Hello?') and updates the caller's buffer in place, which is what the "If dst is a mutable type it will be updated" contract in the docstring promises.

Testing

The new doctest in the dd() docstring covers both the returned value and the in-place update. Run the way CI runs it, on the touched module only:

$ PWNLIB_NOTERM=1 python -bb -m sphinx -b doctest docs/source docs/build/doctest docs/source/util/packing.rst
71 tests in 1 items.
71 passed and 0 failed.
Test passed.

Reverting only the one-line code change and keeping the new doctest gives 69 passed and 2 failed, with TypeError: 'bytes' object cannot be interpreted as an integer on dd(buf, b'?', skip = 5) and bytearray(b'Hello!') instead of bytearray(b'Hello?') on the follow-up check, so the doctest is a real regression test rather than a restatement of the new behaviour.

flake8 . --count --select=E9,F63,F7,E71 --show-source --statistics --exclude=android-?dk reports 0, and pylint --exit-zero --errors-only pwnlib/util/packing.py -f parseable produces output identical to stable at 7e5e79d, so the PyLint job sees no new errors. Ran on macOS 15 with Python 3.11.9 and Sphinx 8.2.3.

Target Branch

This targets stable because the crash is present in the released 4.15.0 and in beta and dev as well, which the PR template maps to a stable bug fix.

dd() documents bytearray as a supported destination type, but
dd(bytearray(b'Hello!'), b'?', skip=5) raises
"TypeError: 'bytes' object cannot be interpreted as an integer".

pwnlib/util/packing.py:1108 handled list and bytearray destinations in a
single branch and filled both with list(map(p8, bytearray(src))). p8()
returns a length-1 bytes object, which is what a list destination is
documented to hold (see the existing dd(list('Hello!'), (63,), skip=5)
doctest), but a bytearray slice assignment needs an iterable of int, so
every bytearray destination crashed.

Assign the byte string src directly when the destination is a bytearray.
Every src branch above the destination dispatch (packing.py:1039-1089)
has already reduced src to a byte string, so the slice assignment is
always valid. This also makes a bytearray destination agree with the
bytes and str destinations, which already copy src verbatim.

Add a doctest covering the returned value and the in-place update of the
caller's bytearray.
@MaxFreedomPollard

Copy link
Copy Markdown
Contributor Author

The three failing test jobs fail in libcdb, not in the module this PR touches, which changes pwnlib/util/packing.py and CHANGELOG.md only.

The Python 2.7 job is the one that ran the doctest suite to completion. In it, Document: util/packing reports 71 passed and 0 failed, which includes the two new examples added here, and the only failures in the entire run are three unstrip_libc examples in libcdb: unstrip_libc(filename) returns False instead of True, hex(libc.symbols.main_arena) raises AttributeError: main_arena, and 'main_arena' in ELF(filename).symbols returns False. The 3.10 and 3.12 jobs were cancelled by fail-fast before they reached util/packing; the failures they printed first are the same three pwnlib/libcdb.py examples, plus the conf.py alrm_handler watchdog firing after a doctest hung on a network read.

Those three examples are the exact ones 86cafce marked # doctest: +SKIP on dev, because the external debuginfod servers are slow or unresponsive and the CI cache expires. That commit is not on stable, so stable CI still runs them.

@peace-maker peace-maker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@peace-maker
peace-maker merged commit 3daf28b into Gallopsled:stable Oct 24, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants