Skip to content

fix(context): bits/endian stop following arch after a second switch - #2764

Open
shariqueahmad108-ship-it wants to merge 2 commits into
Gallopsled:devfrom
shariqueahmad108-ship-it:fix/context-arch-bits-cascade
Open

fix(context): bits/endian stop following arch after a second switch#2764
shariqueahmad108-ship-it wants to merge 2 commits into
Gallopsled:devfrom
shariqueahmad108-ship-it:fix/context-arch-bits-cascade

Conversation

@shariqueahmad108-ship-it

Copy link
Copy Markdown

Fixes #2498.

Repro from the issue: set arch to amd64 then back to i386, and bits stays at 64 — assembling anything after that blows up with Invalid arch/bits combination: i386/64. The arch setter's cascade check (if k not in self._tls) can't tell "user explicitly set bits" apart from "bits only has a value because a previous arch assignment put it there".

Went with the fix @peace-maker sketched out in the issue thread: track which attributes were set directly by the user (a new _tls_explicit dict-stack, mirroring _tls) and only skip the archbits/endian cascade for those. Also had to add _tls_explicit to __slots__, since ContextType doesn't allow arbitrary attributes.

What I checked

  • context.local(), context.clear(), and Thread context inheritance all push/pop/reset this new tracking in lockstep with the existing _tls — manually reproduced all three scenarios, since none of pwntools' existing doctests exercise them together with a second arch switch.
  • Added a doctest matching the issue's exact repro, right next to the existing bits/arch examples in the arch setter's docstring.
  • Ran the full context.rst doctest suite before and after my change — same 8 pre-existing failures both times (all environmental: my local macOS box is missing the cross-arch binutils packages CI installs, and macOS's /bin/bash isn't an ELF file — TESTING.md already says Ubuntu is the expected environment for this).
  • mypy | mypy-baseline filter — 0 new issues.

One thing worth flagging

os's setter has the exact same cascade bug for its own defaults (e.g. newline) — same root cause, just never got reported. Left it alone since the issue didn't ask for it and I didn't want to grow this PR unasked; happy to open a separate issue/PR for it if you'd rather have it fixed too.

Branch

Opened against dev since that's where I developed and tested this. I confirmed the identical cascade bug also exists on stable/beta (same code there) — let me know if you'd like this backported and I'll open that PR too, since dev has diverged enough from stable (type hints, Python 2 compat removed) that I didn't want to guess at a port without it being reviewed first.

Changelog commit to follow once this has a PR number, per CONTRIBUTING.md.

Repro from the issue: set arch to amd64 then back to i386, and bits
stays at 64 - assembling anything after that blows up with "Invalid
arch/bits combination: i386/64". Root cause is the arch setter's
cascade check (`if k not in self._tls`) can't tell the difference
between "user explicitly set bits" and "bits only has a value because
a previous arch assignment put it there".

Went with the fix peace-maker sketched in the issue thread: track which
attributes were set directly by the user (a new _tls_explicit
dict-stack, mirroring _tls) and only skip the cascade for those. Had to
add _tls_explicit to __slots__ too since ContextType doesn't allow
arbitrary attributes.

Checked context.local()/clear()/Thread inheritance don't leak or lose
this tracking across pushes/pops/thread boundaries - all three have
manual reproductions plus a new doctest matching the issue's exact
repro. Ran the full context.rst doctest suite before and after my
change to confirm the same 8 failures exist on both (they're a local
macOS environment thing - missing cross-arch binutils and /bin/bash not
being an ELF - not anything I touched), and mypy-baseline shows 0 new
issues.

The os setter has the identical cascade bug for its own defaults (e.g.
newline), just never got reported. Left it alone since the issue didn't
ask for it and I didn't want to grow this PR - happy to open a separate
one if that's wanted.

Fixes Gallopsled#2498
@shariqueahmad108-ship-it

Copy link
Copy Markdown
Author

Heads up on the red Lint check — it's not from this PR. The MyPy type hint baseline step fails identically on dev itself: run 30757647900 on commit 508283e (the commit I branched from) reports the same two errors and the same counts:

pwnlib/internal/dochelper.py:4: error: Library stubs not installed for "docutils"  [import-untyped]
pwnlib/internal/dochelper.py:6: error: Library stubs not installed for "docutils.parsers.rst"  [import-untyped]
Found 22 errors in 9 files (checked 211 source files)
  new: 5
  unresolved: 20

Looks like types-docutils just isn't in the dev dependency group, so mypy counts those two import-untyped errors (plus their 3 attached notes) as 5 "new" violations on every run. I hit it locally too — after pip install types-docutils, mypy | mypy-baseline filter gives me new: 0 with my change applied, so the fix itself is clean.

Happy to add types-docutils to the dev group in a separate PR if you want it off the board, but it seemed out of scope to slip into this one.

@shariqueahmad108-ship-it

Copy link
Copy Markdown
Author

The failing test (3.10) job isn't from this PR either — it's a runner-dependent flake in an unrelated doctest:

File "../../pwnlib/tubes/ssh.py", line ?, in default
Failed example:
    s.ibt
Expected:
    False
Got:
    True

ssh.ibt greps the runner's own /proc/cpuinfo for the ibt CPU flag, so it comes out True whenever the job lands on a host whose CPU supports Indirect Branch Tracking — nothing to do with context.arch/bits. (My change also doesn't touch the os cascade, which is the only other thing that property reads.)

The dependabot PR that only bumps Actions versions hit the identical failure on Aug 1: run 30699220265 (test (3.13), same s.ibt / expected False / got True). So it's already showing up on PRs that touch no Python at all — it just depends which runner you draw.

Since fail-fast is on, that one job cancelled 3.12/3.13/3.14 before they finished, so the matrix result here isn't meaningful. windows-test passed.

Worth a separate issue for s.ibt — the expectation is hardcoded to False but the value is a property of the host CPU. Happy to open one (and take a swing at it, maybe # doctest: +SKIP or asserting the type rather than the value) if that'd be useful.

@RocketMaDev

Copy link
Copy Markdown
Contributor

Can you explain why you are outputing LLM CoT into comments?

@shariqueahmad108-ship-it

Copy link
Copy Markdown
Author

Fair question, and yes — I use an AI assistant for a lot of this, including those two comments. That's on me for not saying so upfront, and I can see how the length and formatting read as noise rather than signal.

The underlying work is mine to stand behind: I can walk through why _tls_explicit is needed, why __slots__ had to change, and what happens to it across local()/clear()/thread boundaries. And the CI claims are all checkable — the s.ibt failure shows up on the dependabot PR that touches no Python, and the mypy failure reproduces on dev at my base commit.

Happy to trim the comments down, or to close this and let someone else take #2498 if AI-assisted PRs aren't welcome here. Just tell me which you'd prefer.

@RocketMaDev

Copy link
Copy Markdown
Contributor

Are you an agent or a person? Did you, or the person behind review your code? Can you, or the person behind take the responsiblity of what you write?

@shariqueahmad108-ship-it

Copy link
Copy Markdown
Author

Are you an agent or a person? Did you, or the person behind review your code? Can you, or the person behind take the responsiblity of what you write?

what was the problem bruh

@peace-maker

Copy link
Copy Markdown
Member

Thank you, this looks cleaner than a separate attribute for every attribute set implicitly. Yes, please include os and newline. The original PR was against stable which does not have those changes yet. But on dev we want the same guards for changing context.os.

I'll have a closer look at the code later.

@Arusekk

Arusekk commented Aug 10, 2026

Copy link
Copy Markdown
Member

Thank you for contributing. What is your opinion on the approach I suggested here?
#2710 (comment)

Thanks for contributing! I think a slightly different approach could be better.

The _defaultdict class could special-case key == 'bits' and key in ('endianness', 'endian') and return arch-dependent values instead of a fixed default. This would make it unnecessary to separately track whether something was set or not. No need to think about special cases like context.arch = 'amd64'; del context.bits or whatever.

Or not special-case them but get the default values from a dict of special cases.

Have you... seen that PR?

Instead of tracking what's set and what is not set, we could make the default an actual default, when nothing on the stack set it, impossible to unset etc.

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.

context.bits will not automatically switch when setting context.arch twice.

4 participants