You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Argon2id.deriveKey gives segmentation fault for large parameter values #177
I am testing out some Dart cryptography libraries for deriving Argon2id keys with large parameter values, such as the maximum allowed settings in the Bitwarden password manager, which as of today are 10 iterations, 16 parallelism, and 1024 MiB memory.
The following leads to a segmentation fault. Running the script with --old_gen_heap_size=2g sometimes gives a segmentation fault, but sometimes it works.
An alternative solution would be to use the hashlib package, which is able to handle the aforementioned KDF settings (10 iterations, 16 parallelism, and 1024 MiB memory).
I hit this too, and I think I have the root cause. It is not memory exhaustion — it is a use-after-free, and it reproduces well below "large parameter values".
Evidence that pointed at the mechanism
Nine independent CI failures, same parameters (parallelism: 4, memory: 262144 (256 MiB), iterations: 3, hashLength: 32), on a 2-vCPU Linux runner. The interval between the start of the derivation and the crash was:
Nine out of nine within 90 ms of each other. That is not resource pressure — it is the hardcoded Duration(seconds: 10) in _sendSegmentsToIsolate (lib/src/dart/argon2_impl_default.dart).
The mechanism
getByteBuffer() allocates the block buffer with malloc and hands worker isolates only the raw address, which they rebuild with Pointer.fromAddress. The GC cannot see that reference.
If a segment exceeds the 10 s deadline, the controlling isolate throws StateError('Segment processing timeout'). The worker is never told, and keeps writing.
The finally in iterate() only schedules the kills — isolateFuture.then(... isolate.kill(...)) is not awaited, and Isolate.kill is itself asynchronous.
Unwinding continues: deriveKeyBytesFromPrehashingDigest's finally sets _isBufferUsed = false, which disarms the guard in tryReleaseMemory(); deriveKey's finally then calls it and frees the buffer.
glibc munmaps an allocation that size, so the region becomes unmapped. The still-running worker touches it and the process aborts.
Typical crash:
si_signo=Segmentation fault(11), si_code=SEGV_MAPERR(1), si_addr=0x7f7968700010
isolate=argon2-0(...)
pc ... [Optimized] DartArgon2State.processBlock+0x575
pc ... [Optimized] DartArgon2State.processSegment+0x379
pc ... [Unoptimized] DartArgon2StateImplFfi._segmentProcessingEntryPoint.<anonymous closure>
Exit code 134 (SIGABRT). Sometimes two isolates die at once and the dump is truncated with Aborting reentrant request for stack trace.
Why it is not out-of-bounds indexing
Workers access the buffer through a typed-data view (asTypedList), which the VM bounds-checks — an out-of-range index would raise RangeError. A segfault through a bounds-checked view means the memory is no longer mapped.
Why "large parameter values" is a red herring
Large parameters make it likelier by making segments slower, but nothing has to be large: the deadline only has to be missed once. At 256 MiB with parallelism: 4 on a 2-vCPU machine it happened in 9 of 72 runs (12.5%). On a fast, idle machine the same code never crashed, which is why it reads as random.
Workaround that does not require leaving the package
Setting maxIsolates: 0 on DartArgon2id (from package:cryptography/dart.dart) avoids it entirely:
import'package:cryptography/dart.dart';
final algorithm =DartArgon2id(
parallelism:4,
memory:262144,
iterations:3,
hashLength:32,
maxIsolates:0, // no isolates -> heap Uint32List, no raw pointer, no deadline, no free()
);
With isolateCount == 0, getByteBuffer() takes the other branch — a GC-managed Uint32List — so the raw pointer, the 10 s deadline and the free() all disappear.
The digest is unchanged; I verified byte-for-byte against the isolate path at 8 MiB and 64 MiB (parallelism: 4), and against independently generated Argon2id reference vectors (Python argon2-cffi and Node argon2) at 256 MiB / 3 iterations / parallelism: 4.
Cost is a slowdown, and not a uniform one: at 8 MiB it was actually faster (163 ms → 70 ms, isolate startup dominates), while at 64 MiB it was ~1.9× slower (289 ms → 542 ms).
Possible fixes
Await the isolate kills in iterate()'s finally before returning, so tryReleaseMemory() cannot race a live worker.
Or never free on the timeout path — treat a timed-out state as poisoned and leak it, rather than freeing memory of unknown liveness.
Have the worker acknowledge cancellation before the controller proceeds.
Independently: make the deadline configurable and scale its default with memory × iterations. On its own this only makes the race rarer, but a fixed 10 s is easy to miss on a loaded machine.
Happy to open a PR for (1) if that is the direction you would prefer.
Activity
elliotwutingfeng commentedon Oct 19, 2024
An alternative solution would be to use the hashlib package, which is able to handle the aforementioned KDF settings (
10iterations,16parallelism, and1024 MiBmemory).terrier989 commentedon Nov 21, 2025
Thank you for the issue report! 👍 I'll look at this later.
diogogfre94-ai commentedon Aug 22, 2026
I hit this too, and I think I have the root cause. It is not memory exhaustion — it is a use-after-free, and it reproduces well below "large parameter values".
Evidence that pointed at the mechanism
Nine independent CI failures, same parameters (
parallelism: 4,memory: 262144(256 MiB),iterations: 3,hashLength: 32), on a 2-vCPU Linux runner. The interval between the start of the derivation and the crash was:Nine out of nine within 90 ms of each other. That is not resource pressure — it is the hardcoded
Duration(seconds: 10)in_sendSegmentsToIsolate(lib/src/dart/argon2_impl_default.dart).The mechanism
getByteBuffer()allocates the block buffer withmallocand hands worker isolates only the raw address, which they rebuild withPointer.fromAddress. The GC cannot see that reference.StateError('Segment processing timeout'). The worker is never told, and keeps writing.finallyiniterate()only schedules the kills —isolateFuture.then(... isolate.kill(...))is not awaited, andIsolate.killis itself asynchronous.deriveKeyBytesFromPrehashingDigest'sfinallysets_isBufferUsed = false, which disarms the guard intryReleaseMemory();deriveKey'sfinallythen calls it and frees the buffer.munmaps an allocation that size, so the region becomes unmapped. The still-running worker touches it and the process aborts.Typical crash:
Exit code 134 (SIGABRT). Sometimes two isolates die at once and the dump is truncated with
Aborting reentrant request for stack trace.Why it is not out-of-bounds indexing
Workers access the buffer through a typed-data view (
asTypedList), which the VM bounds-checks — an out-of-range index would raiseRangeError. A segfault through a bounds-checked view means the memory is no longer mapped.Why "large parameter values" is a red herring
Large parameters make it likelier by making segments slower, but nothing has to be large: the deadline only has to be missed once. At 256 MiB with
parallelism: 4on a 2-vCPU machine it happened in 9 of 72 runs (12.5%). On a fast, idle machine the same code never crashed, which is why it reads as random.Workaround that does not require leaving the package
Setting
maxIsolates: 0onDartArgon2id(frompackage:cryptography/dart.dart) avoids it entirely:With
isolateCount == 0,getByteBuffer()takes the other branch — a GC-managedUint32List— so the raw pointer, the 10 s deadline and thefree()all disappear.The digest is unchanged; I verified byte-for-byte against the isolate path at 8 MiB and 64 MiB (
parallelism: 4), and against independently generated Argon2id reference vectors (Pythonargon2-cffiand Nodeargon2) at 256 MiB / 3 iterations /parallelism: 4.Cost is a slowdown, and not a uniform one: at 8 MiB it was actually faster (163 ms → 70 ms, isolate startup dominates), while at 64 MiB it was ~1.9× slower (289 ms → 542 ms).
Possible fixes
iterate()'sfinallybefore returning, sotryReleaseMemory()cannot race a live worker.memory × iterations. On its own this only makes the race rarer, but a fixed 10 s is easy to miss on a loaded machine.Happy to open a PR for (1) if that is the direction you would prefer.