KEMBAR78
Fix memory hook installation by Baraa-Hasheesh · Pull Request #1269 · async-profiler/async-profiler · GitHub
Skip to content

Conversation

Baraa-Hasheesh
Copy link
Contributor

Description

async-profiler patches dynamic references to memory allocation calls to monitor these calls to help detect possible memory leaks.
However it's possible to create custom memory allocation functions to replace the existing ones.

#include <dlfcn.h>
#include "stdlib.h"
#include "string.h"

void* malloc(size_t size) {
    void* ptr = calloc(1, size);
    memset(ptr, 0xFF, size);
    return ptr;
}

If this wrapper is defined within the scope of the main executable no problems will occur as the async-profiler only patches dynamic references as such the malloc won't be patched while the internal calloc would be patched, so everything will work as expected.

However if the above wrapper was preloaded into the executable via LD_PRELOAD then the dynamic reference of the main will originally resolve to the custom malloc, however when async-profiler is started it will resolve the malloc via a dlsym call which returns the original reference of malloc rather than the custom declaration.

This in turn means the async profiler will potentially change the behaviour (which methods are called) of a running program if it's used to memory profile the executable which is risky & could potentially lead to crashes & undefined behaviour.

Related issues

#1266

Motivation and context

Avoid changing the behaviour of running processes when attaching the profiler to them.

How has this been tested?

make test
=> A new test case has been added to check possible combination of the async-profiler loading with the library


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@Baraa-Hasheesh Baraa-Hasheesh marked this pull request as draft April 28, 2025 08:54
@Baraa-Hasheesh Baraa-Hasheesh force-pushed the fix-memory-hook-installation branch from 9b4545d to c11c87a Compare April 28, 2025 09:10
@Baraa-Hasheesh Baraa-Hasheesh marked this pull request as ready for review April 28, 2025 09:15
memset(ptr, 0xFF, size);
return ptr;
}

No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no empty lines in the end, but the last line should terminate with EOL character (check other files too).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled here 22e8737

Comment on lines 61 to 62
// On Linux ARMx64 the char is dereferenced differently (-1 = 0xFF = 255 unsigned)
if (*ptr != -1 && *ptr != 255) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use unsigned char then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled here 421d617

CodeCache* cc = (*native_libs)[_patched_libs++];

if (cc->contains((const void*)MallocTracer::initialize)) {
// Let libasyncProfiler always use original allocation methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async-profiler will not be able to trace its own allocations. That's unfortunate, although acceptable.


extern "C" void* malloc_hook(size_t size) {
void* ret = _orig_malloc(size);
void* ret = malloc(size);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested your solution on Alpine Linux and on macOS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is working on alpine, however there are some problems in the created test
I will update the test to work on alpine

For MacOs it's not relevant as nativememory profiling isn't supported on MacOs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested & fixed here 22e8737

Using calloc inside malloc was causing a stack overflow in alpine as calloc internally call malloc :)
(malloc -> calloc -> malloc -> calloc .....)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nativememory profiling isn't supported on MacOs

It is supported to some extent. At least, allocations in JDK should be handled correctly.
My ask here is to verify that it did not get worse with this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to double check on this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apangin I've confirmed that the java libs are still being patched

Just as a note here, the patching logic is still the same as before, the only real change we created is that we used to call malloc using it's evaluated address via dlsym & now we just let the compiler & linker do that for us which less error prone

@Baraa-Hasheesh
Copy link
Contributor Author

@apangin code updated & tests pass locally on alpine


extern "C" void* malloc_hook(size_t size) {
void* ret = _orig_malloc(size);
void* ret = malloc(size);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nativememory profiling isn't supported on MacOs

It is supported to some extent. At least, allocations in JDK should be handled correctly.
My ask here is to verify that it did not get worse with this PR.

__attribute__((visibility("default")))
void* malloc(size_t size) {
if (_orig_malloc == NULL) {
_orig_malloc = (malloc_t) dlsym(((void *) -1l), "malloc");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is (void *) -1l? Why not a named constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the value of RLTD_NEXT

The compile command used to compile the test isn't making the definitions needed to bundle it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check & update the compile command as needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, _GNU_SOURCE must be defined to use this constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solved here fb003af

unsigned char* ptr = (unsigned char*)malloc(1999993);

for (int i = 0; i < 1999993; i++) {
if (*ptr != 255) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You check the same byte every time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a slight inconsistency between what you set (0xFF) and what you test (255). Try keeping the same style (async-profiler typically uses lowercase for hex numbers).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated f270377

Thanks for the catch about pointer value check :)

initAsyncProfiler();

char start_cmd[2048] = {0};
snprintf(start_cmd, sizeof(start_cmd), "start,nativemem,cstack=dwarf,file=%s", filename);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is cstack=dwarf essential here? Async-profiler does not supported it on all platforms (e.g. PPC64), so it's preferable not to use this option unnecessarily in order to avoid excluding test on these platforms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remvoed f270377

@@ -0,0 +1,95 @@
/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two similarly named files: malloc_wrapper.c and mallocwrapper.c
This is confusing, not to say that "wrapper" is a weasel word.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed here f270377

Makefile Outdated
$(CC) -shared -fPIC -o $(TEST_LIB_DIR)/libreladyn.$(SOEXT) test/native/libs/reladyn.c
$(CC) -shared -fPIC -o $(TEST_LIB_DIR)/libcallsmalloc.$(SOEXT) test/native/libs/callsmalloc.c
$(CC) -shared -fPIC $(INCLUDES) -Isrc -o $(TEST_LIB_DIR)/libjnimalloc.$(SOEXT) test/native/libs/jnimalloc.c
$(CXX) -shared -fPIC -o $(TEST_LIB_DIR)/libmalloc.$(SOEXT) test/native/libs/malloc.cpp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This simple file does not use any of C++ features, let's compile it as plain C.
To make RTLD_NEXT declaration available, simply add #define _GNU_SOURCE before includes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied c8f2638

@Test(os = Os.LINUX, sh = "LD_PRELOAD=\"%testlib/libmalloc.so %lib\" ASPROF_COMMAND=start,nativemem,file=%f.jfr %testbin/malloc_reference preload %f.jfr",
output = true, env = {"LD_LIBRARY_PATH=build/lib"}, nameSuffix="LD_PRELOAD+profiler_second")
@Test(os = Os.LINUX, sh = "LD_PRELOAD=%testlib/libmalloc.so %testbin/malloc_reference api %f.jfr", output = true, env = {"LD_LIBRARY_PATH=build/lib"}, nameSuffix="api_test")
public void mallocWrapper(TestProcess p) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call it preloadMalloc as it will better reflect the essence of the test. Similarly, malloc_reference.c can be renamed to preload_malloc.c

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied c8f2638

}

@Test(os = Os.LINUX, sh = "LD_PRELOAD=\"%lib %testlib/libmalloc.so\" ASPROF_COMMAND=start,nativemem,file=%f.jfr %testbin/malloc_reference preload %f.jfr",
output = true, env = {"LD_LIBRARY_PATH=build/lib"}, nameSuffix="LD_PRELOAD+profiler_first")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output = true is typically set when a test consumes output of the target program, which does not seem to be the case here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed c8f2638

@Test(os = Os.LINUX, sh = "LD_PRELOAD=\"%lib %testlib/libmalloc.so\" ASPROF_COMMAND=start,nativemem,file=%f.jfr %testbin/malloc_reference preload %f.jfr",
output = true, env = {"LD_LIBRARY_PATH=build/lib"}, nameSuffix="LD_PRELOAD+profiler_first")
@Test(os = Os.LINUX, sh = "LD_PRELOAD=\"%testlib/libmalloc.so %lib\" ASPROF_COMMAND=start,nativemem,file=%f.jfr %testbin/malloc_reference preload %f.jfr",
output = true, env = {"LD_LIBRARY_PATH=build/lib"}, nameSuffix="LD_PRELOAD+profiler_second")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaces are missing: nameSuffix_=_

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied c8f2638


void preloadOrderTest() {
unsigned char* ptr = (unsigned char*)malloc(1999993);
int size = 1999993 / sizeof(unsigned char);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't. Size of unsigned char is known to be 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied c8f2638

@apangin apangin merged commit 8c15cba into async-profiler:master Apr 29, 2025
15 checks passed
@apangin
Copy link
Member

apangin commented Apr 29, 2025

Thanks! Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants