Repository navigation
secp256k1: fix zero-scalar OOB read and zero-input hang, add safegcd inverse; add opt-in FORCE_NO_INLINE - #4763
Conversation
…th-upstream-pr Sync copyfromhashcat with the upstream secp256k1 PR (hashcat/hashcat#4763)
|
Thanks, this is a good find and a clear write up. I reproduced the same bug independently while working on a different secp256k1 kernel. I hit the same crash, and compute sanitizer pointed directly to point_mul_xy(). Your clamp fixes it. I also reviewed the rest. Items 2, 4, 5, and 6 look good, and safegcd matched the binary GCD in all our tests. I'm also fine with keeping safegcd as the default. The only thing blocking the merge is that the PR predates the shared core plugin changes and no longer merges cleanly. Could you rebase it onto current master? There are three conflicts, and one is a little tricky. In OpenCL/inc_vendor.h, master now has this additional branch: This needs to stay exactly as it is, without HC_NOINLINE. Please add HC_NOINLINE only to the other four branches. Otherwise every plugin fails to link against the core. For src/shared.c and include/shared.h, hc_memchr_* has moved to src/memchr.c and include/memchr.h. You can just drop that part of the diff and keep hc_env_flag. Please also leave hc_env_flag undecorated, since both callers are in the core and it does not need to be part of the plugin interface. |
|
Thanks! Rebased onto current master (
Verified with a full build: 0 errors, 0 warnings, all 593 plugin modules link against the core. I also confirmed the failure you predicted. Dropping the Beyond that, this code is running in my main project, BitcoinAddressFinder, which vendors The commit content itself is unchanged from before the rebase. |
|
Many thanks for the contribution! |
secp256k1: fix two kernel bugs, add a fixed-iteration inverse, add an opt-in noinline switch
Four fixes and improvements to the secp256k1 kernel code (1–4), plus two changes that are not secp256k1-specific: an opt-in noinline switch (5) and the small helper it shares with three existing call sites (6). Each is described separately so any single part can be dropped.
1. Bug: out-of-bounds read in
point_mul_xy()when the scalar is zeroOpenCL/inc_ecc_secp256k1.clWith
multiplier == 0:odd = 0,(0 - 1) >> 1 = 0x7FFFFFFF,* 24putsx_posabout 3.2e9words past
tmps->xy[], and the next 16 statements read from there. CPU OpenCL devices fault, GPUsread whatever is there.
Reachable:
-m 28501/28502/28505/28506and-m 30901/30902/30905/30906deriveprv_keystraightfrom the candidate, so a wordlist entry encoding the key 0 with a valid base58 checksum reaches it.
For
-m 21700/21800it is not realistically reachable.Fixed by clamping a zero first-window digit to 1.
k == 0is not a valid private key, so the valuedoes not matter, only that the index stays in bounds.
Reproduced with AddressSanitizer against the unpatched code (details under Verification).
2. Bug:
inv_mod()never terminates on a zero inputOpenCL/inc_ecc_secp256k1.clThe file already carried the check, commented out:
It is not almost-never, and the consequence is worse than a wrong result.
t0 = 0is always even,so the shift keeps it 0 and the exit condition
t0 == pis never reached. On a CPU OpenCL devicethe work-item spins forever and
clFinishhangs; GPUs happen to mask it.z == 0is reachablefrom
point_addof P + (-P) and from a zero scalar. The guard is 7 ORs and a compare, uniformacross a warp.
3.
inv_mod()now uses a fixed-iteration safegcd inverseOpenCL/inc_ecc_secp256k1.cl,OpenCL/inc_ecc_secp256k1.hThe existing binary extended GCD has an input-dependent iteration count. Under SIMT every lane
in a warp runs to the slowest lane's count.
inv_mod_safegcd()is a port of libsecp256k1'smodinv32(bitcoin-core/secp256k1,src/modinv32_impl.h, MIT) and runs a fixed 20 x 30 = 600divsteps for every input, so a warp finishes in lock-step. Same result for every input.
The file is now structured as two implementations plus a selector:
Build with
-D USE_LEGACY_BINARY_GCD_INV_MODto go back to the binary GCD, either to A/B them orfor a device whose signed right shift is not arithmetic (safegcd assumes it is; the binary GCD does
not).
Two notes for review:
safegcd_i64. hashcat has no signed counterpart tou64, and plainlongis not portablehere: under CUDA it follows the host ABI and is 32-bit on Windows, which is exactly why
inc_types.hspellsullongasunsigned long long. This file is additionally compiled on thehost by
src/emu_inc_ecc_secp256k1.c. The typedef therefore keys off the same backend macrosinc_vendor.hselects.style, so it can be diffed against the upstream reference. Happy to reformat if preferred.
No throughput measurement on hashcat's kernels is included. The motivation is the divergence
argument plus measurements from a downstream project with a different kernel shape, which do not
transfer directly. safegcd also uses four 9-element
intarrays; on register-tight kernels thatcould cost occupancy. If you would rather have the code in but the default unchanged, inverting the
#ifdefininv_mod()is a one-line change.4.
point_to_affine()OpenCL/inc_ecc_secp256k1.clThe Jacobian to affine conversion (
inv_mod+ threemul_mod) appeared four times verbatim: threetimes in
point_get_coords()and once at the tail ofpoint_mul_xy(). Now one function, four callsites, 28 lines fewer. Behavior identical; the only difference is that the three sites in
point_get_coords()no longer clobbernegas scratch, which they overwrote immediately anyway.Not exported in the header, matching the file's existing split: the header declares what module
kernels consume, not the field and point primitives.
5.
FORCE_NO_INLINE(opt-in, off by default)OpenCL/inc_vendor.h,src/backend.cNO_INLINEonly drops theinline statichint. LLVM-based OpenCL back-ends still inline everyDECLSPEChelper at-O3and merge a kernel built from many large helpers into a single hugefunction; greedy register allocation and SelectionDAG scheduling scale roughly super-linearly per
function, so compile time explodes.
-cl-opt-disableis not an alternative — it fails to link thestatic/DECLSPEChelpers withld.lld: undefined hidden symbol.FORCE_NO_INLINEemits the hardnoinlineattribute. It is deliberately separate fromNO_INLINE, which already has a meaning-m 33000relies on.Off by default and byte-identical to today when off:
HC_NOINLINEexpands to nothing, so all fourDECLSPECbranches are unchanged. Turned on withHASHCAT_FORCE_NO_INLINE, following the existingHASHCAT_PIPE/HASHCAT_MEMORYconvention. Out-of-line calls cost runtime throughput, so thismust never become a default.
Honest scope note: the multi-minute AMD compile was measured on a downstream project's larger
kernel, not on hashcat's. I have no evidence hashcat's kernels hit it today. This adds the escape
hatch and documents the trap; it does not claim to fix an observed hashcat problem. Easy to drop —
it is confined to these two files.
6.
hc_env_flag()src/shared.c,include/shared.h,src/backend.c,src/pwpipe.cItem 5 would have been a fourth copy of this:
Extracted instead. The cache slot stays with the caller, because one static inside a shared function
would be a single slot shared by every variable.
pipe_enabled,memory_debug_enabledandpw_pipe_syncwere converted; each drops from 7 lines to 3.HASHCAT_SALT_INNERwas left alone —it parses a value rather than testing presence and has an extra condition.
This is the one part that touches code unrelated to secp256k1. Split out on request.
Verification
Built and run in Docker (Ubuntu 24.04, gcc, pocl 3.0 CPU device). Clean build, no new warnings under
-W -Wall -Wextra.Self-test plus a real crack against each module's own
ST_HASH/ST_PASS,-D 1on pocl:-m 21700 21800 28501 28502 28505 28506 30901 30902 30905 30906-D USE_LEGACY_BINARY_GCD_INV_MOD-m 28501 30901 21700withHASHCAT_FORCE_NO_INLINE=1-m 0 100 1000 1400 1700 3200 5500 13100 22000 33000(regression, sinceinc_vendor.his included everywhere)-m 33000with and withoutHASHCAT_FORCE_NO_INLINEFORCE_NO_INLINEis confirmed to reach the compiler and not silently no-op: the kernel cache hashchanges from
m28501_a0-pure.6bd5126cto2de7abca.Native harness over the kernel sources (host
IS_NATIVEpath), ASan + UBSan:[1] checks both inverses against each other and
a * a^-1 == 1 mod p.Negative controls — the same harness with each guard removed, to show the tests are not vacuous:
inv_modzero guard: hangs after [1], killed at 20 spoint_mul_xyclamp:Not verified
signed long longbranch ofsafegcd_i64isthe one that matters for Windows + CUDA and it has not been executed — only constructed to mirror
inc_types.h's ownullonghandling. Worth a check on your side.register-pressure question (see item 3).