-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[vm/ffi] Pointer.asTypedList
shared across isolates causes use after free
#55800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Pointer.asTypedList
shared across isolates causes use after freePointer.asTypedList
shared across isolates causes use after free
Can we prioritize this? |
Indeed, the typed-data is not copied correctly. The following example prints flakily all kinds of things. Output: Repro: // Copyright (c) 2025, 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.
// SharedObjects=ffi_test_functions
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:expect/expect.dart';
import 'dylib_utils.dart';
void main() {
// Force dlopen so @Native lookups in DynamicLibrary.process() succeed.
dlopenGlobalPlatformSpecific('ffi_test_functions');
test();
}
Future<void> test() async {
const length = 10;
final typedList = await Isolate.run(() {
final ptr = calloc(length, sizeOf<Int16>()).cast<Int16>();
final typedList = ptr.asTypedList(length, finalizer: freePointer);
Timer(Duration(milliseconds: 1), () {
for (int i = 0; i < length; i++) {
// overwrite initialized zeros
typedList[0] = i;
}
});
return typedList;
});
for (int i = 0; i < length; i++) {
Expect.equals(typedList[0], 0);
}
final completer = Completer<void>();
Timer(Duration(milliseconds: 1), () {
completer.complete();
});
for (int i = 0; i < length; i++) {
Expect.equals(typedList[0], 0);
}
await completer.future;
print(typedList);
}
@Native<Pointer<Void> Function(IntPtr num, IntPtr size)>(isLeaf: true)
external Pointer<Void> calloc(int num, int size);
final freePointer = DynamicLibrary.process()
.lookup<NativeFunction<Void Function(Pointer<Void>)>>('free'); Edit, more specifically, the error is in // Copyright (c) 2025, 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.
// SharedObjects=ffi_test_functions
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:expect/expect.dart';
import 'dylib_utils.dart';
void main() {
// Force dlopen so @Native lookups in DynamicLibrary.process() succeed.
dlopenGlobalPlatformSpecific('ffi_test_functions');
test();
}
Future<void> test() async {
const length = 10;
var result = Completer<Int16List>();
final resultPort = RawReceivePort();
resultPort.handler = (dynamic response) {
resultPort.close();
result.complete(response as Int16List);
};
unawaited(
Isolate.spawn((SendPort sendPort) async {
final ptr = calloc(length, sizeOf<Int16>()).cast<Int16>();
final typedList = ptr.asTypedList(length, finalizer: freePointer);
Isolate.exit(sendPort, typedList); // Repros.
// sendPort.send(typedList); // Doesn't repro.
}, resultPort.sendPort),
);
final typedList = await result.future;
print(typedList);
for (int i = 0; i < length; i++) {
Expect.equals(typedList[i], 0);
}
}
@Native<Pointer<Void> Function(IntPtr num, IntPtr size)>(isLeaf: true)
external Pointer<Void> calloc(int num, int size);
final freePointer = DynamicLibrary.process()
.lookup<NativeFunction<Void Function(Pointer<Void>)>>('free'); |
TL;DR: Attaching I've tracked down two issues: 1. Isolate.exit shares TypedData without sharing attached NativeFinalizer
We could potentially fix this by (A) during the finalization pass finding the (We need to make sure we can do this in a reasonable time complexity from the VM. The VM does not have access to the relevant finalizer entries attached to an object, so it would have to loop through all entries in the VM. A possible better approach would be to have the validate function fill a list of typed-datas, and then in the run-finalizers-eager method consume that list and pass in a target isolate.) Alternatively, we could fix it by (B) changing the finalizer to be a finalizers from the 2. We eagerly run
|
// Ensure native finalizers are run before isolate has shutdown message is | |
// sent. This way users can rely on the exit message that an isolate will not | |
// run any Dart code anymore _and_ will not run any native finalizers anymore. | |
RunAndCleanupFinalizersOnShutdown(); | |
// Post message before LowLevelShutdown that sends onExit message. | |
// This ensures that exit message comes last. | |
if (bequest_ != nullptr) { | |
auto beneficiary = bequest_->beneficiary(); | |
auto handle = bequest_->TakeHandle(); | |
PortMap::PostMessage( | |
Message::New(beneficiary, handle, Message::kNormalPriority)); | |
bequest_.reset(); | |
} |
For (A) we would need to change those semantics to native finalizers are run eagerly, except for the ones attached to objects being send on exit.
For (B) we don't need to change anything.
Solutions
A. Migrating NativeFinalizer
s on exit. (However, we decided against it in #55050 for the general case.)
B. Use isolate-group C API finalizers.
Option B is technically a breaking change, as someone could be using asTypedList
with a finalizer, and relying on the fact that if they don't send the object to another isolate with Isolate.exit
the native finalizer is guaranteed to run.
Proposal:
- Change the semantics of
asTypedList
finalizers to be isolate group scoped. - Breaking change announcement.
- (In some future: make them isolate scoped with [vm/ffi] Introduce pragma's 'vm:shareable' and 'vm:shared' and make
NativeFinalizer
s live in an isolate group instead of isolate. #55062)
We should actually tighten typing of
You could probably do that reasonably fast by collecting all encountered external I think the change you propose is okay. |
I realized we implemented this already before, but did not land it: https://dart-review.googlesource.com/c/sdk/+/229544/11. I think we can reuse that implementation with a few modifications. (Rough sketch, I might overlook something.)
|
The native finalizers in
asTypedList
are bound to an isolate, not an isolate group:sdk/sdk/lib/ffi/ffi.dart
Lines 368 to 373 in 4dd6ee6
sdk/sdk/lib/_internal/vm/lib/ffi_native_finalizer_patch.dart
Line 115 in 4dd6ee6
Hypothesis: The TypedData we create out of Pointer is marked unmodifiable.We need to either mark the typed data as mutable so that the view does not consider the typed data as unmodifiable, and we copy instead of share the object.Or, we need to attach finalizers in the isolate group instead of the isolate.
Context:
TODO: verify hypothesis. (Filing issue so that I don't forget.)The text was updated successfully, but these errors were encountered: