Issue 77096
Summary [mlir][bufferization] Double free when using private-function-dynamic-ownership=true
Labels mlir
Assignees
Reporter sabauma
    When using `ownership-based-buffer-deallocation=private-function-dynamic-ownership=true`, the generated deallocations produce a double-free for `memref` function arguments which are not subsequently returned.

## Reproducer

```mlir
// RUN:   mlir-opt %s -buffer-deallocation-pipeline=private-function-dynamic-ownership=true -test-lower-to-llvm \
// RUN:   | mlir-cpu-runner -entry-point-result=i32

func.func private @private_callee(%arg0: memref<f32>) -> memref<f32> {
  %alloc = memref.alloc() : memref<f32>
 return %alloc : memref<f32>
}

func.func @caller() -> (f32) {
 %alloc = memref.alloc() : memref<f32>
  %ret = call @private_callee(%alloc) : (memref<f32>) -> memref<f32>

  %val = memref.load %ret[] : memref<f32>
  return %val : f32
}

// Driver main function for mlir-cpu-runner
func.func @main() -> i32 {
  %res = func.call @caller() : () -> f32
  %val = arith.fptosi %res : f32 to i32
  return %val : i32
}
```

Executing this example crashes the `mlir-cpu-runner` with the error: `free(): double free detected in tcache 2`.

## What happens

`ownership-based-buffer-deallocation=private-function-dynamic-ownership=true` generates the following code for the caller and callee:

```mlir
func.func private @private_callee(%arg0: memref<f32>, %arg1: i1) -> (memref<f32>, i1) {
  %true = arith.constant true
  %alloc = memref.alloc() : memref<f32>
  %base_buffer, %offset = memref.extract_strided_metadata %arg0 : memref<f32> -> memref<f32>, index
 %base_buffer_0, %offset_1 = memref.extract_strided_metadata %alloc : memref<f32> -> memref<f32>, index
  %0 = bufferization.dealloc (%base_buffer, %base_buffer_0 : memref<f32>, memref<f32>) if (%arg1, %true) retain (%alloc : memref<f32>)
  return %alloc, %0 : memref<f32>, i1
}
func.func @caller() -> f32 {
  %true = arith.constant true
 %alloc = memref.alloc() : memref<f32>
  %0:2 = call @private_callee(%alloc, %true) : (memref<f32>, i1) -> (memref<f32>, i1)
 %1 = memref.load %0#0[] : memref<f32>
  %base_buffer, %offset = memref.extract_strided_metadata %alloc : memref<f32> -> memref<f32>, index
  %base_buffer_0, %offset_1 = memref.extract_strided_metadata %0#0 : memref<f32> -> memref<f32>, index
  bufferization.dealloc (%base_buffer, %base_buffer_0 : memref<f32>, memref<f32>) if (%true, %0#1)
  return %1 : f32
}
```

Two deallocations are inserted: one in `@private_callee`

```mlir
%0 = bufferization.dealloc (%base_buffer, %base_buffer_0 : memref<f32>, memref<f32>) if (%arg1, %true) retain (%alloc : memref<f32>)
```

and the other in `@caller`

```mlir
bufferization.dealloc (%base_buffer, %base_buffer_0 : memref<f32>, memref<f32>) if (%true, %0#1)
```

In both cases, `%base_buffer` refers to the allocation in `@caller` and the ownership indicator is `true`. At runtime, both `@private_callee` and `@caller` always free the memref value allocated within `@caller`.

## Other observations

Changing caller to the following:

```mlir
func.func @caller() -> (f32) {
  %alloc = memref.alloc() : memref<f32>
  %ret = call @private_callee(%alloc) : (memref<f32>) -> memref<f32>

  // Read from %alloc rather than %ret
  %val = memref.load %alloc[] : memref<f32>
  return %val : f32
}
```

Results in a read-after-free of `%alloc`, where `%alloc` is still deallocated within `@private_callee`, but has a use after the call.

_______________________________________________
llvm-bugs mailing list
llvm-bugs@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to