Skip to content

Implement Extended Masking#143

Draft
Quinntyx wants to merge 13 commits into
syssec-utd:devfrom
Quinntyx:dev-extended_masking
Draft

Implement Extended Masking#143
Quinntyx wants to merge 13 commits into
syssec-utd:devfrom
Quinntyx:dev-extended_masking

Conversation

@Quinntyx

@Quinntyx Quinntyx commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

This PR seeks to close #124 by implementing a new Preprocessor pass prior to Masking, which rewrites bytecode into a valid version with statically-reconstructable container types (dict, set, list, tuple) statically folded.

Preliminary testing indicates that it works; out of 2711 cases with container type related failures drawn from PyLingual prod MongoDB, 99.4% of them are solved by this fix.

There is also an included set of test cases that confirm that the Preprocessor correctly processes programs.

Model retraining shouldn't be required by this change, as the way the Preprocessor works rewrites bytecode into another valid (though unmarshallable) form before it is passed to the Masking pipeline, which is minimally changed and produces a compatible display format to the existing PyLingual models. I haven't seen any problems about this, the model seems to properly decompile everything with no retraining needed or performance regressions.

Remaining items: Full benchmark still needs to be run to evaluate any potential regressions

Quinntyx added 13 commits June 18, 2026 12:41
Adds pylingual/masking/container_recovery/ package mirroring
control_flow_reconstruction/ structure:
- segment.py: Recovery + Segment dataclasses
- stack_analysis.py: stack_effect (with 3.12+ NULL-push fixes),
  analyze_stack, parse_*_recursive
- recovery.py: @register_recovery_strategy decorator + dispatch
- utils.py: build-size and pattern helpers
- strategies/: 12 recovery strategies split by type
  (dict, list, set, tuple, const), glob auto-imported

Includes adjacent container parsing fix (cur_elem_id <= 0 break
condition) and frozenset-to-set conversion in recover_const_container.
Rewrites container construction bytecode into single LOAD_CONST instructions.
For each code object, analyzes the stack to identify container construction
spans, recovers their values via strategy-based recovery, and replaces the
bytecode span with a single LOAD_CONST referencing the materialized value
in co_consts.

Key behaviors:
- Walks Segment tree recursively: if parent recovery is incomplete, tries
  children right-to-left. Only collapses children with container tags.
- eval() with empty namespace (no frozenset builtin)
- Co_consts deduplication: is (identity) then ==
- All exceptions propagate (no catch)
- Skips empty containers after eval
- preprocess() returns None
Add Preprocessor.preprocess() call in mask_bytecode() before
create_global_masker(). Preprocessor exceptions are caught with
graceful degradation — decompilation continues without container
recovery if preprocessing fails. Separate log line for the
preprocessing stage.
- Empty container skip uses len(value) == 0 instead of not value
  (not value was truthy-check, incorrectly skipped non-empty falsy
  containers like [0] or [""])
- co_consts dedup uses type-aware equality to prevent set/frozenset
  collision (set({1,2}) == frozenset({1,2}) is True in Python)
- TypeSensitiveDict: use repr() for unhashable keys via try/except hash(),
  eval() with full builtins to restore
- Remove replace_list() from get_model_view() — containers get single mask
  token with type wrapper (e.g. list(<mask_42>))
- Remove KWARG padding logic (orphaned after replace_list removal)
- Add dict to container type check in get_model_view()
- Remove container flattening from create_global_masker() in model_disasm.py
  (slice decomposition kept)
- Replace global_tab.update({k:v}) with global_tab[k]=v (dict literal fails
  on unhashable keys)
Add 30 fixture files ported from recursive-decompile-demo and two test
modules:

- test_preprocessor.py: parametrized co_consts comparison test that
  extracts container literals from AST, runs the Preprocessor, and
  verifies each value appears in co_consts (with recursive search for
  nested containers).
- test_edge_cases.py: 11 edge case tests covering empty containers,
  falsy non-empty containers, set/frozenset recovery, nested containers,
  deduplication, and type-aware dedup.

36 passed, 5 skipped (fixtures with no non-empty container literals).

Closes syssec-utd#5
Replace try/except hash() with isinstance guard for container types
(list, set, dict, tuple, frozenset) to prevent custom __repr__ from
reaching repr(). Replace eval(value, {__builtins__: builtins}) with
ast.literal_eval in _key_restore.
…te eval

Move the Preprocessor class and container_recovery package from
pylingual/masking/container_recovery/ to pylingual/preprocessor/,
presenting the Preprocessor as a first-class pass with container_recovery
as a subpackage it uses.

Refactor Recovery dataclass to carry actual Python objects (Recovery.value)
instead of string expressions (Recovery.expr). All recovery strategies now
build objects directly, and _collapse_segment uses the value directly
instead of calling eval(). Remove INDENT and string formatting from
strategies that no longer need them.

Add comment noting xdis oppush/oppop tables are incorrect for null-pushing
and variable-size opcodes, explaining why our custom stack_effect is needed.
…hashable code objects in TypeSensitiveDict

- Remove custom stack_effect() in stack_analysis.py (~46 lines), replaced
  with xdis.cross_dis.xstack_effect which handles the same null-pushing
  opcodes (LOAD_GLOBAL, LOAD_ATTR, LOAD_SUPER_ATTR, CALL) and container
  opcodes (BUILD_MAP, BUILD_CONST_KEY_MAP, BUILD_LIST/SET/TUPLE) correctly.
  xdis also covers more opcodes (MAKE_FUNCTION, UNPACK_SEQUENCE, etc.)
  and fixes an off-by-one bug in our UNPACK_EX handling.
- Add fallback for xstack_effect returning None (MAKE_FUNCTION with
  unusual opargs) by using the raw oppush/oppop tables.
- Fix TypeSensitiveDict._key_transform to handle any unhashable type
  (not just the five container types) via try/hash/except fallback. This
  fixes a crash where code objects with dicts in co_consts (added by the
  Preprocessor) are unhashable on Python 3.12+.
- Fix _key_restore to gracefully handle repr strings that can't be
  evaluated by ast.literal_eval (e.g. code object reprs).
@Quinntyx Quinntyx changed the title Implements Extended Masking Implement Extended Masking Jul 2, 2026
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.

1 participant