blob: f166c16fca4cb7c0f5bcd8c15c1f3c1c1b0b8e22 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi' as ffi;
import 'dart:typed_data';
import '../bindings.dart' show extractBoringSslError, opensslAllocator;
import 'bindings/boringssl.g.dart' as bssl;
/// A scoped [ffi.Allocator] and resource tracker for calling BoringSSL.
///
/// All memory is allocated with [opensslAllocator] ([bssl.OPENSSL_malloc]), so
/// it is scrubbed with [bssl.OPENSSL_cleanse] when released, and it may be
/// handed to BoringSSL functions that take ownership and later call
/// [bssl.OPENSSL_free].
///
/// Allocations, resources registered with [register], and callbacks registered
/// with [onReleaseAll] are released in reverse order by [releaseAll]. Prefer
/// [BoringArena.using] and [BoringArena.stream], which call [releaseAll] once
/// the computation has completed.
///
/// BoringSSL functions with `set0` semantics (for example [bssl.RSA_set0_key]
/// or [bssl.EVP_PKEY_CTX_set0_rsa_oaep_label]) take ownership of their
/// arguments on success. Call [move] after such a call, so the arena does not
/// free the resource a second time:
///
/// ```dart
/// BoringArena.using((arena) {
/// final ctx = arena.register(
/// EVP_PKEY_CTX_new(pkey, nullptr),
/// EVP_PKEY_CTX_free,
/// );
/// final label = arena.copyBytes<Uint8>(labelBytes);
/// final len = labelBytes.length;
/// if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, label, len) == 1) {
/// arena.move(label); // `ctx` now owns `label`.
/// }
/// });
/// ```
final class BoringArena implements ffi.Allocator {
final List<_Registration> _registrations = [];
bool _released = false;
/// Runs [computation] with a new [BoringArena] and releases it afterwards.
///
/// If [computation] returns a [Future], the arena is released when that
/// future completes, so `async` computations can keep using the arena across
/// `await`s. Returning a [Stream] is rejected, because the arena would be
/// released before the stream is listened to; use [BoringArena.stream]
/// instead.
static R using<R>(R Function(BoringArena arena) computation) {
final arena = BoringArena();
var releaseLater = false;
try {
final result = computation(arena);
if (result is Future) {
releaseLater = true;
return result.whenComplete(arena.releaseAll) as R;
}
if (result is Stream) {
throw ArgumentError.value(
result,
'computation',
'must not return a Stream, use BoringArena.stream instead',
);
}
return result;
} finally {
if (!releaseLater) {
arena.releaseAll();
}
}
}
/// Returns a stream of the events from the stream returned by [computation],
/// which is called with a new [BoringArena] when the stream is listened to.
///
/// The arena is released when the stream is done, or when the subscription
/// is cancelled.
static Stream<T> stream<T>(
Stream<T> Function(BoringArena arena) computation,
) async* {
final arena = BoringArena();
try {
yield* computation(arena);
} finally {
arena.releaseAll();
}
}
/// Allocates [byteCount] bytes with [opensslAllocator], freed by
/// [releaseAll].
@override
ffi.Pointer<T> allocate<T extends ffi.NativeType>(
int byteCount, {
int? alignment,
}) {
_ensureInUse();
final pointer = opensslAllocator.allocate<T>(
byteCount,
alignment: alignment,
);
_registrations.add(
_Registration(pointer, () => opensslAllocator.free(pointer)),
);
return pointer;
}
/// Does nothing, memory is freed by [releaseAll].
@override
void free(ffi.Pointer<ffi.NativeType> pointer) {}
/// Registers [release] to be called with [resource] by [releaseAll], and
/// returns [resource].
///
/// This is typically used with BoringSSL's `X_new` / `X_free` pairs:
/// `arena.register(EC_KEY_new(), EC_KEY_free)`.
T register<T extends Object>(T resource, void Function(T resource) release) {
_ensureInUse();
_registrations.add(_Registration(resource, () => release(resource)));
return resource;
}
/// Registers [callback] to be called by [releaseAll].
void onReleaseAll(void Function() callback) {
_ensureInUse();
_registrations.add(_Registration(null, callback));
}
/// Transfers ownership of [resource] out of this arena, and returns it.
///
/// Removes every allocation or [register] registration for [resource], so
/// [releaseAll] no longer frees it. Call this after passing [resource] to a
/// BoringSSL function that takes ownership of it.
///
/// Throws an [ArgumentError] if [resource] is not owned by this arena.
T move<T extends Object>(T resource) {
_ensureInUse();
final count = _registrations.length;
_registrations.removeWhere((r) => r.resource == resource);
if (_registrations.length == count) {
throw ArgumentError.value(
resource,
'resource',
'is not owned by this BoringArena',
);
}
return resource;
}
/// Allocates a [bssl.CBS] reading a copy of [data] owned by this arena.
ffi.Pointer<bssl.CBS> cbs(List<int> data) {
final result = this<bssl.CBS>();
result.ref
..data = copyBytes<ffi.Uint8>(data)
..len = data.length;
return result;
}
/// Allocates a growable [bssl.CBB] with [initialCapacity], which is cleaned
/// up with [bssl.CBB_cleanup] by [releaseAll].
///
/// Use [CbbToBytes.toBytes] to copy the contents into a [Uint8List].
ffi.Pointer<bssl.CBB> cbb([int initialCapacity = 4096]) {
final result = this<bssl.CBB>();
// CBB_zero makes CBB_cleanup safe, even if CBB_init fails.
bssl.CBB_zero(result);
register(result, bssl.CBB_cleanup);
if (bssl.CBB_init(result, initialCapacity) != 1) {
bssl.ERR_clear_error();
throw const OutOfMemoryError();
}
return result;
}
/// Releases all allocations and resources in reverse registration order.
///
/// If a release callback throws, the remaining resources are still
/// released, and the first error is rethrown afterwards. The arena cannot be
/// used after this has been called.
void releaseAll() {
_released = true;
Object? error;
StackTrace? stackTrace;
while (_registrations.isNotEmpty) {
try {
_registrations.removeLast().release();
} catch (e, st) {
error ??= e;
stackTrace ??= st;
}
}
if (error != null) {
Error.throwWithStackTrace(error, stackTrace!);
}
}
void _ensureInUse() {
if (_released) {
throw StateError('BoringArena has already been released');
}
}
}
final class _Registration {
final Object? resource;
final void Function() release;
_Registration(this.resource, this.release);
}
/// Copies Dart bytes into native memory.
extension AllocatorCopyBytes on ffi.Allocator {
/// Allocates `data.length` bytes and copies [data] into them.
///
/// Use a [BoringArena] (or [opensslAllocator]) for inputs that may be
/// secret, so the copy is scrubbed when it is freed.
ffi.Pointer<T> copyBytes<T extends ffi.NativeType>(List<int> data) {
final pointer = this<ffi.Uint8>(data.length);
if (data.isNotEmpty) {
pointer.asTypedList(data.length).setAll(0, data);
}
return pointer.cast<T>();
}
}
/// Reads the contents of a [bssl.CBB].
extension CbbToBytes on ffi.Pointer<bssl.CBB> {
/// Flushes this [bssl.CBB] and returns a Dart-owned copy of its contents.
///
/// Throws a [StateError] if [bssl.CBB_flush] fails.
Uint8List toBytes() {
if (bssl.CBB_flush(this) != 1) {
final error = extractBoringSslError();
throw StateError('CBB_flush failed${error == null ? '' : ': $error'}');
}
final length = bssl.CBB_len(this);
if (length == 0) {
return Uint8List(0);
}
return Uint8List.fromList(bssl.CBB_data(this).asTypedList(length));
}
}