Skip to content

rulegen cannot start any worker on macOS/Windows (spawn + PicklingError), plus worker crash and shutdown ordering - #2

Merged
briangillan merged 1 commit into
briangillan:masterfrom
bandrel:pr/spawn-and-worker-fixes
Aug 1, 2026
Merged

rulegen cannot start any worker on macOS/Windows (spawn + PicklingError), plus worker crash and shutdown ordering#2
briangillan merged 1 commit into
briangillan:masterfrom
bandrel:pr/spawn-and-worker-fixes

Conversation

@bandrel

@bandrel bandrel commented Jul 31, 2026

Copy link
Copy Markdown

Nice work on the modernization pass — I went looking for silent-failure bugs
across the PACK forks and yours had already fixed the maskgen filter directions,
the missing ?b/?h/?H/?1-?4 charsets, the word_counter_total
denominator and the '/x rule key collision. These are the ones I still found,
ported onto your master and tested against it.

rulegen cannot start at all on macOS or Windows

Since Python 3.8, multiprocessing defaults to the spawn start method on
those platforms. Spawn pickles the RuleGen instance to reach each worker, and
the instance holds self.hashcat_rule (a dict of lambdas) plus a native Enchant
handle — neither is picklable, so every Process() call dies before any analysis
runs:

_pickle.PicklingError: Can't pickle local object <function RuleGen.__init__.<locals>.<lambda>
when serializing dict item ':'
when serializing dict item 'hashcat_rule'
when serializing RuleGen state

Verified on macOS with Python 3.14 and pyenchant 3.3.0, where
multiprocessing.get_start_method() is spawn. Fix: the rule engine, leet map
and preanalysis rules move into build_rule_engine(), and
__getstate__/__setstate__ drop the unpicklable attributes so the child
rebuilds them. A custom --wordlist is recorded so workers rebuild the same
dictionary instead of silently falling back to the system one. With this applied,
a real run completes end to end on your code under spawn.

Worker crash on words with no rules

for word in sorted(words, key=lambda w: len(w["hashcat_rules"][0])):

generate_hashcat_rules returns [] whenever nothing survives the
max_rule_len filter — easy to reach with a small --maxrulelen. Indexing [0]
raises IndexError inside password_worker, whose except only catches
KeyboardInterrupt and SystemExit. The worker dies, and because
passwords_queue is bounded to the thread count the parent can then block
forever on put() once enough consumers are gone. The visible symptom is a run
that stops making progress with no error summary.

Shutdown ordering drops the tail of the analysis

while not passwords_queue.empty() treats a drained input queue as "work
finished", but it only means the last password was taken, not analyzed. The
parent then pushes None onto rules_queue, which rule_worker can consume
before the still-running workers finish enqueuing their rules. Shutdown now stops
the analysis workers and joins them, then pills and joins the writers, so the
output files are complete and closed before being read back.

Death pills are delivered with retries rather than abandoned on the first
queue.Full, since the bounded input queue is normally full on an early exit.

Ctrl-C did not end a run

analyze_passwords_file advertises "Press Ctrl-C to end execution and generate
statistical analysis", but the try/except/else means an interrupt skips the
else entirely, so no death pills are sent and the children are leaked. In
practice a single SIGINT did not stop the read loop at all. SIGINT is now handled
as a request to stop reading and shutdown runs from a finally, on every path.
Tested on your code: interrupting a 30,000-password run stops after ~2,600
passwords, shuts down in under a second, writes all four output files and leaves
no orphan processes.

statsgen: Advanced Masks ignored --hiderare

That section hardcodes if count*100//self.filter_counter > 0 while the other
three gate on self.hiderare, so sub-1% advanced masks were always hidden from
the display regardless of the flag. Now gated consistently; the output file was
and remains complete.

Testing

Regression tests covering these (plus the maskgen items you'd already fixed) are
in bandrel/pack as tests/test_pack.py. I
left them out of this PR since they're written against a different base — happy
to port them here if you'd like.

Related: Hydraze#9, Hydraze#10, iphelix#31.

…order

rulegen cannot start a single worker on macOS or Windows. Since Python 3.8
multiprocessing defaults to the 'spawn' start method there, which pickles
the RuleGen instance to reach each worker, and the instance holds a dict
of lambdas (the hashcat rule engine) plus a native Enchant handle. Every
Process() call dies with PicklingError before any analysis runs. The rule
engine moves into build_rule_engine() and __getstate__/__setstate__ drop
the unpicklable attributes so the child rebuilds them.

A source word with no surviving rules crashed the worker on an empty-list
index; generate_hashcat_rules returns [] whenever nothing survives the
max_rule_len filter. The worker's except clause only catches
KeyboardInterrupt and SystemExit, so it died and the bounded feeder queue
could then block the parent.

while not passwords_queue.empty() treated a drained input queue as "work
finished", but it only means the last password was taken, not analyzed,
so the writers could be stopped before the workers finished enqueuing.
Shutdown now stops the analysis workers, joins them, then pills and joins
the writers.

Ctrl-C is documented as the way to end a run early and still generate
statistics, but a single SIGINT did not stop the read loop. It is now
handled as a request to stop reading, and shutdown runs on every path.

statsgen: the Advanced Masks section ignored --hiderare and always hid
entries under 1%; it is now gated like every other section.
@briangillan
briangillan merged commit 36dd0f7 into briangillan:master Aug 1, 2026
@briangillan

Copy link
Copy Markdown
Owner

Thanks for this — really appreciate the depth here! I tested your branch before merging: ran it end-to-end under Windows spawn (no PicklingError, all output files complete, no orphan processes), and ran your test_pack.py suite from bandrel/pack against it — everything covering these five issues passes.

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