blob: a15c8ca93e8034825d2f9403cb19625b84280b41 [file] [edit]
// 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.
/// Raw `ffigen`-generated BoringSSL C bindings (`bssl_dart`),
/// [bssl.OPENSSL_cleanse]-backed [opensslAllocator], scoped [BoringArena], and
/// [NativeHandle] finalizer wrapper.
library;
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'src/arena.dart' show BoringArena;
import 'src/bindings/boringssl.g.dart' as bssl;
export 'src/arena.dart';
export 'src/bindings/boringssl.g.dart';
/// An [ffi.Allocator] backed by BoringSSL's [bssl.OPENSSL_malloc] and
/// [bssl.OPENSSL_free].
///
/// BoringSSL's [bssl.OPENSSL_free] automatically scrubs every allocation with
/// [bssl.OPENSSL_cleanse] before returning memory to the OS heap.
const OpenSslAllocator opensslAllocator = OpenSslAllocator._();
/// Implementation of [ffi.Allocator] using [bssl.OPENSSL_malloc] and
/// [bssl.OPENSSL_free].
final class OpenSslAllocator implements ffi.Allocator {
const OpenSslAllocator._();
@override
ffi.Pointer<T> allocate<T extends ffi.NativeType>(
int byteCount, {
int? alignment,
}) {
final ptr = bssl.OPENSSL_malloc(byteCount);
if (ptr == ffi.nullptr) {
bssl.ERR_clear_error();
throw const OutOfMemoryError();
}
return ptr.cast<T>();
}
@override
void free(ffi.Pointer<ffi.NativeType> pointer) {
bssl.OPENSSL_free(pointer.cast());
}
/// Pointer to [bssl.OPENSSL_free] suitable for [ffi.NativeFinalizer] or
/// `Pointer<Uint8>.asTypedList(len, finalizer: opensslAllocator.nativeFree)`.
ffi.Pointer<ffi.NativeFinalizerFunction> get nativeFree =>
bssl.addresses.OPENSSL_free;
}
/// A GC-managed wrapper around a BoringSSL `Pointer<T>` that attaches a shared
/// [ffi.NativeFinalizer] and supports deterministic [dispose].
///
/// Because [NativeHandle] implements [ffi.Finalizable], calling [use] ensures
/// that the Dart VM compiler inserts a reachability fence keeping `this` alive
/// until [use] returns, preventing premature finalization while a native call
/// is in progress.
final class NativeHandle<T extends ffi.NativeType> implements ffi.Finalizable {
static final _finalizers = <int, ffi.NativeFinalizer>{};
final ffi.Pointer<T> _ptr;
final ffi.NativeFinalizer _finalizer;
final void Function(ffi.Pointer<ffi.Void>) _rawFree;
bool _disposed = false;
/// Wraps [_ptr] and attaches a [ffi.NativeFinalizer] calling [freeFn] (for
/// example, `addresses.EVP_PKEY_free` or `addresses.X509_free`).
NativeHandle(
this._ptr,
ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<T>)>> freeFn, {
int externalSize = 4096,
}) : _rawFree = freeFn
.cast<ffi.NativeFinalizerFunction>()
.asFunction<void Function(ffi.Pointer<ffi.Void>)>(),
_finalizer = _finalizers.putIfAbsent(
freeFn.address,
() => ffi.NativeFinalizer(freeFn.cast()),
) {
if (_ptr == ffi.nullptr) {
throw StateError('Cannot wrap nullptr in NativeHandle<$T>');
}
_finalizer.attach(
this,
_ptr.cast(),
detach: this,
externalSize: externalSize,
);
}
/// Whether [dispose] has been called on this handle.
bool get isDisposed => _disposed;
/// Invokes [fn] with the underlying `Pointer<T>`, keeping `this` reachable
/// until [fn] completes so the GC finalizer cannot run mid-call.
R use<R>(R Function(ffi.Pointer<T> ptr) fn) {
if (_disposed) {
throw StateError('NativeHandle<$T> has already been disposed');
}
return fn(_ptr);
}
/// Deterministically frees the native resource and detaches the GC finalizer.
///
/// Safe to call multiple times (subsequent calls are no-ops).
void dispose() {
if (_disposed) return;
_disposed = true;
_finalizer.detach(this);
_rawFree(_ptr.cast());
}
}
/// Convenience extension to invoke a 1-argument C function taking `Pointer<P>`
/// with a [NativeHandle].
///
/// ```dart
/// final pkey = NativeHandle(EVP_PKEY_new(), addresses.EVP_PKEY_free);
/// final keyId = EVP_PKEY_id.invoke(pkey);
/// ```
extension NativeHandleInvoke1<R, P extends ffi.NativeType>
on R Function(ffi.Pointer<P>) {
/// Invokes `this` with the unwrapped pointer from [handle].
R invoke(NativeHandle<P> handle) => handle.use(this);
}
/// Convenience extension to invoke a 2-argument C function
/// `(Pointer<EVP_PKEY>, A1)` with a [NativeHandle] (for example,
/// `EVP_PKEY_CTX_new.invoke(pkey, nullptr)`).
extension EvpPKeyInvoke2First<R, A1>
on R Function(ffi.Pointer<bssl.EVP_PKEY>, A1) {
/// Invokes `this` with the unwrapped pointer from [handle] and [arg1].
R invoke(NativeHandle<bssl.EVP_PKEY> handle, A1 arg1) =>
handle.use((ptr) => this(ptr, arg1));
}
/// Convenience extension to invoke a 2-argument C function
/// `(A1, Pointer<EVP_PKEY>)` with a [NativeHandle] (for example,
/// `EVP_marshal_public_key.invoke(cbb, pkey)`).
extension EvpPKeyInvoke2Last<R, A1>
on R Function(A1, ffi.Pointer<bssl.EVP_PKEY>) {
/// Invokes `this` with [arg1] and the unwrapped pointer from [handle].
R invoke(A1 arg1, NativeHandle<bssl.EVP_PKEY> handle) =>
handle.use((ptr) => this(arg1, ptr));
}
/// Convenience extension to invoke a 5-argument C function
/// `(A1, A2, A3, A4, Pointer<P>)` with a [NativeHandle] (for example,
/// `EVP_DigestVerifyInit.invoke(ctx, pctx, md, nullptr, pkey)`).
extension NativeHandleInvoke5Last<R, A1, A2, A3, A4, P extends ffi.NativeType>
on R Function(A1, A2, A3, A4, ffi.Pointer<P>) {
/// Invokes `this` with [arg1]..[arg4] and the unwrapped pointer from
/// [handle].
R invoke(A1 arg1, A2 arg2, A3 arg3, A4 arg4, NativeHandle<P> handle) =>
handle.use((ptr) => this(arg1, arg2, arg3, arg4, ptr));
}
/// Formats the least recent (and most specific) error on the current thread's
/// BoringSSL error queue, and clears the queue before returning.
///
/// Returns `null` if the error queue is empty.
///
/// The error queue is thread-local, and an isolate may resume on a different
/// OS thread after an `await`. Call this right after the failing BoringSSL
/// call, with no `await` in between, rather than from a `finally` that may run
/// after one (such as the release of an `async` [BoringArena.using]).
///
/// Use [bssl.ERR_clear_error] to discard errors you ignore, for example when a
/// failed signature verification just means `false`. The queue is shared by
/// every package using `package:boring` on that thread, so leftover errors
/// would be reported for the next, unrelated failure.
String? extractBoringSslError() {
try {
final err = bssl.ERR_get_error();
if (err == 0) {
return null;
}
const maxLen = 4096;
final out = opensslAllocator<ffi.Char>(maxLen);
try {
bssl.ERR_error_string_n(err, out, maxLen);
final data = out.cast<ffi.Uint8>().asTypedList(maxLen);
var len = 0;
while (len < maxLen && data[len] != 0) {
len++;
}
return utf8.decode(data.sublist(0, len), allowMalformed: true);
} finally {
opensslAllocator.free(out);
}
} finally {
bssl.ERR_clear_error();
}
}
/// Returns the library code (`ERR_LIB_*`) for a packed BoringSSL error code.
// ignore: non_constant_identifier_names
int ERR_GET_LIB(int packedError) => (packedError >> 24) & 0xff;
/// Returns the library-specific reason code (`*_R_*`) for a packed BoringSSL
/// error code.
// ignore: non_constant_identifier_names
int ERR_GET_REASON(int packedError) => packedError & 0xfff;