ud2U
Deno3y ago
2 replies
ud2

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);
}

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.

How do I ensure that example_free_data is called only when no ArrayBuffer points to the allocation?
Was this page helpful?