ud2
ud2
DDeno
Created by ud2 on 7/12/2023 in #help
Avoiding use-after-free
I encountered a use-after-free issue when writing an FFI binding. The code looks like this.
const lib = Deno.dlopen("libexample.dylib", {
/**
* Fills newly allocated memory with data sampled from an external source.
* @param {*mut usize} len Pointer to which the number of samples will be written.
* @returns {*mut f32} Pointer to the data.
*/
example_sample_data: {
parameters: ["buffer"],
result: "pointer",
},
/**
* Frees an allocation created by `example_sample_data`.
* @param {*mut f32} ptr Pointer to the data.
* @returns {()}
*/
example_free_data: {
parameters: ["pointer"],
result: "void",
},
});
const { example_sample_data, example_free_data } = lib.symbols;
const lenCell = new BigUint64Array(1);
const lenBuf = new Uint8Array(lenCell.buffer);
const finalizer = new FinalizationRegistry(example_free_data);

export function sampleData(): Float32Array {
const ptr = example_sample_data(lenBuf)!;
const len = Number(lenCell[0]);
let buf = Deno.UnsafePointerView.getArrayBuffer(
ptr,
len * Float32Array.BYTES_PER_ELEMENT,
);
// I could have avoided the issue by switching this to `false`, but I would like to minimize unnecessary copies.
const zeroCopy = true;
if (zeroCopy) {
finalizer.register(buf, ptr);
} else {
buf = buf.slice(0);
example_free_data(ptr);
}
return new Float32Array(buf);
}
const lib = Deno.dlopen("libexample.dylib", {
/**
* Fills newly allocated memory with data sampled from an external source.
* @param {*mut usize} len Pointer to which the number of samples will be written.
* @returns {*mut f32} Pointer to the data.
*/
example_sample_data: {
parameters: ["buffer"],
result: "pointer",
},
/**
* Frees an allocation created by `example_sample_data`.
* @param {*mut f32} ptr Pointer to the data.
* @returns {()}
*/
example_free_data: {
parameters: ["pointer"],
result: "void",
},
});
const { example_sample_data, example_free_data } = lib.symbols;
const lenCell = new BigUint64Array(1);
const lenBuf = new Uint8Array(lenCell.buffer);
const finalizer = new FinalizationRegistry(example_free_data);

export function sampleData(): Float32Array {
const ptr = example_sample_data(lenBuf)!;
const len = Number(lenCell[0]);
let buf = Deno.UnsafePointerView.getArrayBuffer(
ptr,
len * Float32Array.BYTES_PER_ELEMENT,
);
// I could have avoided the issue by switching this to `false`, but I would like to minimize unnecessary copies.
const zeroCopy = true;
if (zeroCopy) {
finalizer.register(buf, ptr);
} else {
buf = buf.slice(0);
example_free_data(ptr);
}
return new Float32Array(buf);
}
But the user could then do this.
let data = sampleData();
data = structuredClone(data, { transfer: [data.buffer as ArrayBuffer] });
// Original `ArrayBuffer` is no longer reachable.
// …
// Later, its cleanup callback is called, which frees the allocation.
console.log(data); // Use after free.
let data = sampleData();
data = structuredClone(data, { transfer: [data.buffer as ArrayBuffer] });
// Original `ArrayBuffer` is no longer reachable.
// …
// Later, its cleanup callback is called, which frees the allocation.
console.log(data); // Use after free.
How do I ensure that example_free_data is called only when no ArrayBuffer points to the allocation?
3 replies