Silent-failure bugs in all four tools (with line references)
These are bugs where PACK produces a wrong number, drops output, or ignores an
option without reporting an error — so the operator has no signal that
anything went wrong. Line numbers are against master (622dd92).
I've fixed all of these in a maintained Python 3 fork
(bandrel/pack) with regression tests, and
I'm happy to split any of them into individual PRs here if that's useful.
maskgen
-
loadmasks() ignores its own argument (maskgen.py:72). The signature
takes filename, but the body opens args[0]. Since maskgen.py:271-272
loops for arg in args: maskgen.loadmasks(arg), running
maskgen.py a.masks b.masks c.masks parses a.masks three times and never
reads b or c. total_occurrence is inflated N× while self.masks is
overwritten, so the reported "Masks coverage" comes out around 1/N of the
truth and the other files' masks are missing from the output.
-
Masks repeated across files are not aggregated (maskgen.py:95).
self.masks[mask] = dict() is last-write-wins, but :83 adds every
occurrence to the coverage denominator, so merging statsgen outputs that
share masks understates coverage. (Masked today by bug 1; fixing 1 exposes 2.)
-
Runtime truncates to zero (maskgen.py:81, policygen.py:110,124,128).
mask_complexity / self.pps is integer division, so at the default 1e9 pps
any mask under a billion candidates reports 0 seconds: ?d?d?d?d?d?d?d?d
(1e8) and ?l?l?l?l?l?l (3.1e8) both show 0s. --mintime/--maxtime can't
distinguish them and --targettime accumulates zeros and never trips. This
is also the cause of the # TODO: Something wrong here at maskgen.py:166:
getmaskscoverage sums complexity and divides once, while loadmasks
divides per mask, so the two paths genuinely disagree.
-
Unrecognised mask tokens are costed as 1 (maskgen.py:66). The else
branch prints a warning but leaves count unmultiplied, so the mask stays in
the run with an understated complexity and runtime. Two realistic triggers:
?1 without --custom-charset1-len (the and self.customcharset1len guard
at :62 falls through), and literal characters in a .hcmask, since
mask[1:].split("?") turns ?u?l?lpass into a token lpass. Literals also
break mask_length = len(mask)/2 at :79.
-
--targettime overshoots by one mask (maskgen.py:119-128). The mask is
written to the output file before the budget check breaks the loop.
-
Zero-valued options are silently discarded (maskgen.py:243,249-256,265).
These use if options.x: rather than a None check, so --minoccurrence 0,
--mintime 0, --targettime 0 and --pps 0 are ignored. statsgen (:248)
already uses the == None form, so it's inconsistent within the codebase.
statsgen
- The Advanced Masks section ignores
--hiderare (statsgen.py:203). The
other three sections gate on if self.hiderare and not ... > 0, but this one
hardcodes if count*100/self.filter_counter > 0, so sub-1% advanced masks are
always hidden from the display whether or not you asked for it. The file
write at :206 is correctly outside the if, so only the terminal view is
affected.
rulegen
-
Worker processes die on words with no rules (rulegen.py:789).
sorted(words, key=lambda word: len(word["hashcat_rules"][0])) indexes [0]
on a list that generate_hashcat_rules returns empty whenever nothing
survives the max_rule_len filter at :461 — easy to hit with a small
--maxrulelen. password_worker's except only catches KeyboardInterrupt
and SystemExit, so the worker dies, and since passwords_queue is bounded
to the thread count (:882) the parent eventually blocks forever on put().
Observable symptom: the run stops making progress with no error summary.
-
The last rules are dropped (rulegen.py:926).
while not passwords_queue.empty() treats an empty input queue as "work
finished", but it only means the last password was taken, not analyzed. The
parent then pushes the None death pill onto rules_queue, which
rule_worker can consume before the still-running workers finish enqueuing.
Joining the worker processes before pilling the writers fixes it.
-
Ctrl-C skips shutdown entirely (rulegen.py:917-931). :879 advertises
Ctrl-C as the way to finish early and get statistics, but the
try/except/else means an interrupt skips the else block, so no death
pills are sent. Workers and both writer processes are left running while the
parent reads the partially written .rule/.word files and reports
statistics over them.
-
Top 10 words percentages use the rule total (rulegen.py:967).
word_counter_total = sum(rules_counter.values()) should be
words_counter. The shares at :973 are scaled by the wrong denominator
and don't sum to 100%.
-
--maxrules does nothing. self.max_rules is set at :58 and assigned
from the option at :1068, then never read. Only max_rule_len is
enforced, despite the help text promising "Maximum number of rules to
consider".
-
--hashcat does nothing. verify_hashcat_rules (:980) has no caller,
and rulegen.hashcat at :1078 sets an attribute that isn't in __init__
and is never read. Users pass the flag expecting rules to be validated
against hashcat --stdout and silently get no verification.
-
Hashcat offsets past Z produce malformed rules (rulegen.py:410).
int_to_hashcat returns chr(65+N-10), which walks into [, \, ] for
positions above 35 instead of refusing.
-
The progress line reports the wrong elapsed time (rulegen.py:908).
It prints segment_start - analysis_start, the previous segment's start
offset, rather than elapsed time, so it under-reports by one segment.
Lower severity
-
Division by zero on empty or fully-filtered input: statsgen.py:178,
maskgen.py:132,171, rulegen.py:936,940. These crash rather than
mislead, but empty input is a normal thing to hand these tools.
-
Output files are never closed (statsgen.py:257, maskgen.py:246,
policygen.py:178, and the csv.reader(open(...)) at maskgen.py:72).
CPython flushes at normal exit, so this only bites on a kill — but a
Ctrl-C'd maskgen run can leave a truncated .hcmask.
-
hashcat_rule["p"] = lambda x,y: x*y (rulegen.py:106) yields y copies,
whereas hashcat's pN yields N+1. Latent only — nothing emits p.
Silent-failure bugs in all four tools (with line references)
These are bugs where PACK produces a wrong number, drops output, or ignores an
option without reporting an error — so the operator has no signal that
anything went wrong. Line numbers are against
master(622dd92).I've fixed all of these in a maintained Python 3 fork
(bandrel/pack) with regression tests, and
I'm happy to split any of them into individual PRs here if that's useful.
maskgen
loadmasks()ignores its own argument (maskgen.py:72). The signaturetakes
filename, but the body opensargs[0]. Sincemaskgen.py:271-272loops
for arg in args: maskgen.loadmasks(arg), runningmaskgen.py a.masks b.masks c.masksparsesa.masksthree times and neverreads
borc.total_occurrenceis inflated N× whileself.masksisoverwritten, so the reported "Masks coverage" comes out around 1/N of the
truth and the other files' masks are missing from the output.
Masks repeated across files are not aggregated (
maskgen.py:95).self.masks[mask] = dict()is last-write-wins, but:83adds everyoccurrence to the coverage denominator, so merging statsgen outputs that
share masks understates coverage. (Masked today by bug 1; fixing 1 exposes 2.)
Runtime truncates to zero (
maskgen.py:81,policygen.py:110,124,128).mask_complexity / self.ppsis integer division, so at the default 1e9 ppsany mask under a billion candidates reports 0 seconds:
?d?d?d?d?d?d?d?d(1e8) and
?l?l?l?l?l?l(3.1e8) both show 0s.--mintime/--maxtimecan'tdistinguish them and
--targettimeaccumulates zeros and never trips. Thisis also the cause of the
# TODO: Something wrong hereatmaskgen.py:166:getmaskscoveragesums complexity and divides once, whileloadmasksdivides per mask, so the two paths genuinely disagree.
Unrecognised mask tokens are costed as 1 (
maskgen.py:66). Theelsebranch prints a warning but leaves
countunmultiplied, so the mask stays inthe run with an understated complexity and runtime. Two realistic triggers:
?1without--custom-charset1-len(theand self.customcharset1lenguardat
:62falls through), and literal characters in a.hcmask, sincemask[1:].split("?")turns?u?l?lpassinto a tokenlpass. Literals alsobreak
mask_length = len(mask)/2at:79.--targettimeovershoots by one mask (maskgen.py:119-128). The mask iswritten to the output file before the budget check breaks the loop.
Zero-valued options are silently discarded (
maskgen.py:243,249-256,265).These use
if options.x:rather than aNonecheck, so--minoccurrence 0,--mintime 0,--targettime 0and--pps 0are ignored. statsgen (:248)already uses the
== Noneform, so it's inconsistent within the codebase.statsgen
--hiderare(statsgen.py:203). Theother three sections gate on
if self.hiderare and not ... > 0, but this onehardcodes
if count*100/self.filter_counter > 0, so sub-1% advanced masks arealways hidden from the display whether or not you asked for it. The file
write at
:206is correctly outside theif, so only the terminal view isaffected.
rulegen
Worker processes die on words with no rules (
rulegen.py:789).sorted(words, key=lambda word: len(word["hashcat_rules"][0]))indexes[0]on a list that
generate_hashcat_rulesreturns empty whenever nothingsurvives the
max_rule_lenfilter at:461— easy to hit with a small--maxrulelen.password_worker'sexceptonly catchesKeyboardInterruptand
SystemExit, so the worker dies, and sincepasswords_queueis boundedto the thread count (
:882) the parent eventually blocks forever onput().Observable symptom: the run stops making progress with no error summary.
The last rules are dropped (
rulegen.py:926).while not passwords_queue.empty()treats an empty input queue as "workfinished", but it only means the last password was taken, not analyzed. The
parent then pushes the
Nonedeath pill ontorules_queue, whichrule_workercan consume before the still-running workers finish enqueuing.Joining the worker processes before pilling the writers fixes it.
Ctrl-C skips shutdown entirely (
rulegen.py:917-931).:879advertisesCtrl-C as the way to finish early and get statistics, but the
try/except/elsemeans an interrupt skips theelseblock, so no deathpills are sent. Workers and both writer processes are left running while the
parent reads the partially written
.rule/.wordfiles and reportsstatistics over them.
Top 10 words percentages use the rule total (
rulegen.py:967).word_counter_total = sum(rules_counter.values())should bewords_counter. The shares at:973are scaled by the wrong denominatorand don't sum to 100%.
--maxrulesdoes nothing.self.max_rulesis set at:58and assignedfrom the option at
:1068, then never read. Onlymax_rule_lenisenforced, despite the help text promising "Maximum number of rules to
consider".
--hashcatdoes nothing.verify_hashcat_rules(:980) has no caller,and
rulegen.hashcatat:1078sets an attribute that isn't in__init__and is never read. Users pass the flag expecting rules to be validated
against
hashcat --stdoutand silently get no verification.Hashcat offsets past
Zproduce malformed rules (rulegen.py:410).int_to_hashcatreturnschr(65+N-10), which walks into[,\,]forpositions above 35 instead of refusing.
The progress line reports the wrong elapsed time (
rulegen.py:908).It prints
segment_start - analysis_start, the previous segment's startoffset, rather than elapsed time, so it under-reports by one segment.
Lower severity
Division by zero on empty or fully-filtered input:
statsgen.py:178,maskgen.py:132,171,rulegen.py:936,940. These crash rather thanmislead, but empty input is a normal thing to hand these tools.
Output files are never closed (
statsgen.py:257,maskgen.py:246,policygen.py:178, and thecsv.reader(open(...))atmaskgen.py:72).CPython flushes at normal exit, so this only bites on a kill — but a
Ctrl-C'd maskgen run can leave a truncated
.hcmask.hashcat_rule["p"] = lambda x,y: x*y(rulegen.py:106) yieldsycopies,whereas hashcat's
pNyieldsN+1. Latent only — nothing emitsp.