https://bugs.kde.org/show_bug.cgi?id=523335

            Bug ID: 523335
           Summary: Duplicate allocation records for calloc on musl/Alpine
                    due to interposed internal malloc call
    Classification: Applications
           Product: Heaptrack
      Version First 1.5.0
       Reported In:
          Platform: Other
                OS: Other
            Status: REPORTED
          Severity: normal
          Priority: NOR
         Component: general
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

DESCRIPTION

When Heaptrack is used through `LD_PRELOAD` on Alpine Linux with musl libc, a
single call to `calloc()` is recorded as two allocations for the same pointer.

This happens because musl's `calloc()` implementation calls the public
`malloc()` symbol internally after calculating the total allocation size. Since
Heaptrack is preloaded, this internal `malloc()` call is intercepted by
Heaptrack's `malloc` wrapper and recorded as an allocation.

After musl's `calloc()` returns, Heaptrack's outer `calloc` wrapper records the
same pointer and size again.

The sequence is:

1. The application calls `calloc(m, n)`.
2. Heaptrack's `calloc` wrapper calls the real musl `calloc` using `RTLD_NEXT`.
3. musl's `calloc` calls the public `malloc(total_size)` symbol.
4. That call resolves to Heaptrack's preloaded `malloc` wrapper.
5. Heaptrack records the allocation returned by the real musl `malloc`.
6. Control returns to musl's `calloc`, which returns the same pointer.
7. Heaptrack's outer `calloc` wrapper records the same pointer again.
8. The application later calls `free()` once, producing only one free record.

The resulting raw Heaptrack data is conceptually:

```text
+ size trace(heaptrack malloc -> musl calloc -> caller) P
+ size trace(heaptrack calloc -> caller) P
- P
```

Both allocation records refer to the same pointer and requested size, but may
have different stack traces.

This appears to be specific to libc implementations where `calloc()` calls an
interposable public `malloc()` symbol internally, such as musl.

STEPS TO REPRODUCE

1. Build the following test program on Alpine Linux:

```c
#include <stdlib.h>

int main(void)
{
    void *ptr = calloc(4, 16);
    free(ptr);
    return 0;
}
```

2. Run the program under Heaptrack using its preload-based tracking mode:

```bash
heaptrack ./calloc-test
```

3. Inspect the raw Heaptrack allocation data or add temporary logging around
Heaptrack's `malloc`, `calloc`, and `free` recording functions.

4. Observe that the pointer returned by `calloc()` is recorded twice as an
allocation but is freed only once.

OBSERVED RESULT

Heaptrack records two allocation events for one `calloc()` call:

* One allocation is recorded by Heaptrack's `malloc` wrapper when musl's
`calloc()` internally calls `malloc()`.
* A second allocation is recorded by Heaptrack's outer `calloc` wrapper after
musl's `calloc()` returns.

Both records contain the same pointer and allocation size.

Only one corresponding free event is recorded when the application calls
`free()`.

This can cause incorrect allocation counts and may affect leak detection,
stack-trace attribution, peak-memory calculations, and other analysis based on
the allocation diary.

EXPECTED RESULT

A single application call to `calloc()` should produce exactly one allocation
record in Heaptrack.

Heaptrack should avoid recording the internal `malloc()` call made by musl's
`calloc()`, or otherwise detect that the outer `calloc()` record refers to an
allocation that was already recorded.

SOFTWARE/OS VERSIONS

Operating System: Alpine Linux [VERSION]
musl libc version: [VERSION]
Heaptrack version or Git commit: [VERSION OR COMMIT]

KDE Plasma Version: Not applicable
KDE Frameworks Version: [VERSION, IF APPLICABLE]
Qt Version: [VERSION, IF APPLICABLE]

Compiler: [COMPILER AND VERSION]
Architecture: [FOR EXAMPLE x86_64 OR aarch64]

ADDITIONAL INFORMATION

Relevant Heaptrack wrapper behavior:

* Heaptrack's `calloc` wrapper calls the real `calloc` through `RTLD_NEXT` and
then calls `heaptrack_malloc(ret, num * size)`.
* Heaptrack's `malloc` wrapper calls the real `malloc` and independently calls
`heaptrack_malloc()` for the returned pointer.

Relevant musl behavior:

musl's `calloc()` implementation calculates the total size and then invokes the
public `malloc()` symbol. Because Heaptrack is loaded with `LD_PRELOAD`, that
internal call is interposed by Heaptrack.

musl source:

```text
https://git.musl-libc.org/cgit/musl/tree/src/malloc/calloc.c
```

The duplicate records appear in this order:

```text
inner malloc allocation
outer calloc allocation
single free
```

The inner allocation's stack includes Heaptrack's `malloc` wrapper and musl's
`calloc`, while the outer allocation's stack includes Heaptrack's `calloc`
wrapper and the application caller.

The problem does not appear to be an application double allocation or double
free. It is caused by nested allocator interposition within musl's `calloc()`
implementation.

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to