Skip to content

libbpf-tools: Add new feature doublefree#4286

Open
Bojun-Seo wants to merge 1 commit intoiovisor:masterfrom
Bojun-Seo:doublefree
Open

libbpf-tools: Add new feature doublefree#4286
Bojun-Seo wants to merge 1 commit intoiovisor:masterfrom
Bojun-Seo:doublefree

Conversation

@Bojun-Seo
Copy link
Copy Markdown
Contributor

@Bojun-Seo Bojun-Seo commented Oct 19, 2022

Add doublefree tool to detect double free. It could detect user level double
free error currently and can be expanded to detect kernel level double free
error. Followings are the usage and example.

Usage:

Usage: doublefree [OPTION...]
Detect and report doublefree error.

-c or -p is a mandatory option
EXAMPLES:
    doublefree -p 1234             # Detect doublefree on process id 1234
    doublefree -c a.out            # Detect doublefree on a.out
    doublefree -c 'a.out arg'      # Detect doublefree on a.out with argument

  -c, --command=COMMAND      Execute the command and detect doublefree
  -p, --pid=PID              Detect doublefree on the specified process
  -v, --verbose              Verbose debug output
  -?, --help                 Give this help list
      --usage                Give a short usage message
  -V, --version              Print program version

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

Report bugs to https://github.com/iovisor/bcc/tree/master/libbpf-tools.

Example:

$ cat doublefree_generator.c
#include <unistd.h>
#include <stdlib.h>

int* foo() {
  return (int*)malloc(sizeof(int));
}

void bar(int* p) {
  free(p);
}

int main(int argc, char* argv[]) {
  sleep(10);
  int *val = foo();
  *val = 33;
  bar(val);
  *val = 84;
  bar(val);
  return 0;
}
$ gcc doublefree_generator.c
$ sudo ./doublefree -c a.out
2024-May-15 22:30:24 INFO Execute command: a.out(pid 99584)
Tracing doublefree... Hit Ctrl-C to stop
free(): double free detected in tcache 2

Allocation:
        #1 0x00564d1455819b foo+0x12 (/home/bojun/doublefree/libbpf-tools/a.out+0x119b)
        #2 0x00564d145581e3 main+0x27 (/home/bojun/doublefree/libbpf-tools/a.out+0x11e3)
        #3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)

First deallocation:
        #1 0x0070c4e94a53e0 free+0x0 (/usr/lib/x86_64-linux-gnu/libc.so.6+0xa53e0)
        #2 0x00564d145581fd main+0x41 (/home/bojun/doublefree/libbpf-tools/a.out+0x11fd)
        #3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)

Second deallocation:
        #1 0x0070c4e94a53e0 free+0x0 (/usr/lib/x86_64-linux-gnu/libc.so.6+0xa53e0)
        #2 0x00564d14558213 main+0x57 (/home/bojun/doublefree/libbpf-tools/a.out+0x1213)
        #3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)

Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
/* default log level */
static enum log_level log_level = ERROR;

void sig_handler(int signo)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

static ? Here and below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a reason why I use static here and below. (below means static functions, right?)
I could remove static but I'd like to use static in here and below, if there is no problem.

I use this tool and other tools in my company, and manage the source code bit differently.
I compile every tool into one binary for user to use easily. So symbol can conflict on compilation if I don't use static.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I removed static on this enum value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have restored the static that I had previously removed. If there is a reason why static needs to be removed, please let me know.

Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

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

Hi!

Thank you for it, this tool is cool!
I tested it and it worked fine:

$ ./a.out &
[2] 99365
$ sudo ./doublefree -p 99365
bar: 0x55c9bd2912a0
baz: 0x55c9bd2912a0
bazz: 84
free 84
free(): double free detected in tcache 2
[2]  + abort (core dumped)  ./a.out
Warn: Is this process alive? pid: 99365
Found double free...
Allocation happended on:
stack_id: 30033
        #1 0x0055c9bc24724b foo
        #2 0x0055c9bc2472a4 main
        #3 0x007fabf0d8c083 __libc_start_main


First deallocation happended on:
stack_id: 9246
        #1 0x007fabf0e026d0 free
        #2 0x0055c9bc2472be main
        #3 0x007fabf0d8c083 __libc_start_main


Second deallocation happended on:
stack_id: 3231
        #1 0x007fabf0e026d0 free
        #2 0x0055c9bc247236 baz
        #3 0x0055c9bc2472d4 main
        #4 0x007fabf0d8c083 __libc_start_main

I nonetheless have a question regarding the memptrs map.

Best regards.

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c
Comment thread libbpf-tools/doublefree.c Outdated
@Bojun-Seo Bojun-Seo force-pushed the doublefree branch 2 times, most recently from a01519a to aa8a849 Compare November 7, 2022 05:08
@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

Add feature to detect doublefree on kernel(currently support arm and arm64)
Print report with symbol offset and module offset.

@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

I found some issues on detecting kernel doublefree. So I remove it.
I'll implement kernel doublefree detection later.

@eiffel-fl
Copy link
Copy Markdown
Contributor

I took a slightly new look at this tool and I am wondering if this can be merged with lsan to create an eBPF swiss army knife to detect problem with memory.
What do you think?

@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

Sounds great to merging tools. In fact, it could be possible to merge memleak, lsan and doublefree, since they hook(attach uprobes) the same points.
But my(and my company's) current interest is on embedded devices, and embedded devices are totally sensitive on memory usage.
So my team decided to split tools to use minimal memory.
But still, I think merging them is cool. So maybe I could merge them later.

@eiffel-fl
Copy link
Copy Markdown
Contributor

But still, I think merging them is cool. So maybe I could merge them later.

Sure, this can definitely be done in a future PR!
The tools themselves are really cool as they are right now!

@Bojun-Seo Bojun-Seo changed the title bpf-tools: Add new feature(doublefree) libbpf-tools: Add new feature doublefree Sep 11, 2023
@Bojun-Seo Bojun-Seo force-pushed the doublefree branch 3 times, most recently from ce9f147 to af89da4 Compare December 21, 2023 02:13
@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

I simplify the usage and the example, and I updated it on commit message as well as description.

@Bojun-Seo Bojun-Seo force-pushed the doublefree branch 2 times, most recently from aae151a to 86e5525 Compare December 21, 2023 04:51
Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

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

Hi!

I have some comments but they are mainly nits.
I will test it again later.

Best regards.

Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c Outdated
Comment thread libbpf-tools/doublefree.bpf.c
Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c
Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

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

Hi!

I have one comment regarding the macros which I think can be replaced by a function.
Otherwise, it looks good to me.
Let me know about my comment and I will test it later.

Best regards.

Comment thread libbpf-tools/doublefree.c Outdated
Comment on lines +83 to +89
#define ATTACH_URETPROBE_CHECKED(obj, lib_path, func_name, pid) \
do { \
off_t func_off = get_elf_func_offset(lib_path, #func_name); \
_CHECK_OFFSET(func_off); \
_ATTACH_UPROBE(obj, lib_path, func_name##_return, true, func_off, pid); \
_CHECK_PROGRAM(obj, func_name##_return); \
} while (false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cannot you use a function for this? Something like this:

int attach_uprobe(struct bpf_link **func_link, const struct bpf_program *func_prog, char *lib_path, char * func_name, int pid, bool is_ret, bool check)
{
	off_t func_off = get_elf_func_offset(lib_path, func_name);
	if (func_off < 0) {
		p_warn("Failed to get func_offset");
		return func_off;
	}
	
	*func_link = bpf_program__attach_uprobe(func_prog, is_ret, pid, lib_path, func_off);
	if (check) {
		int err = libbpf_get_error(func_link);
		if (err) {
			p_warn("Failed to attach %s: %d", #func_name, err);
			return err;
		}
	}
		
	return 0;
}

You would call it like the following:

/* Think to check the return code. */
attach_uprobe(&obj->link.malloc_return, obj->progs.malloc_return, libc_path, "malloc" env.pid, true, true);
attach_uprobe(&obj->link.free_entry, obj->progs.free_entry, libc_path, "free", env.pid, false, true);

/* ... */

attach_uprobe(&obj->link.aligned_alloc_return, obj->progs.aligned_alloc_return, libc_path, "alligned_alloc", env.pid, true, false);
attach_uprobe(&obj->link.valloc_return, obj->progs.valloc_return, libc_path, "valloc", env.pid, true, false);

You could even declare a struct to hold everything:

struct probe {
	struct bpf_link **link;
	struct bpf_program *prog;
	const char *name;
	bool is_ret;
	bool check;
};

/* ... */

struct probe probes[] = {
	{ 
		.link = &obj->link.malloc_return, 
		.prog = obj->progs.malloc_return,
		.name = "malloc",
		.is_ret = true,
		.check = true,
	},
	/* ... */
};

for (int i = 0; i < sizeof(probes) / sizeof(probes[0]); i++) {
	attach_uprobe(probes[i].link, probes[i].prog, libc_path, probes[i].name, env.pid, probes[i].is_ret, probes[i].check);
}

What do you think?

Copy link
Copy Markdown
Contributor Author

@Bojun-Seo Bojun-Seo Jan 30, 2024

Choose a reason for hiding this comment

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

Sounds great! I always appreciate your valuable opinion.
I prefer the second code you suggested(using struct).
And I think it would be better to pass struct probe instance(using pointer) instead of each members to attach_uprobe function, like followings.

for (int i = 0; i < sizeof(probes) / sizeof(probes[0]); i++) {
	attach_uprobe(&probes[i], libc_path);
}

What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And I think it would be better to pass struct probe instance(using pointer) instead of each members to attach_uprobe function, like followings.

Far better approach, it is easier to read as we have less arguments and also easier to maintain!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I changed uprobe attaching codes. I tried implementing it while incorporating your opinion, but in a slightly different way.

Copy link
Copy Markdown
Collaborator

@ekyooo ekyooo left a comment

Choose a reason for hiding this comment

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

"ERROR Failed to poll perf_buffer" in usage example always displayed?

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c
Comment thread libbpf-tools/doublefree.c Outdated
p_err("Failed to poll perf_buffer");
break;
}
if (getpgid(env.pid) < 0) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I have a question. Is there no need to limit it to the ESRCH errno case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no need to limit it to the ESRCH errno case according to the following manual page.
https://linux.die.net/man/2/getpgid
getpgid returns -1 on error and errno is set appropriately.
And there is only one errno for getpgid, which is ESRCH.
Which means, if getpgid fails, the errno should be ESRCH.

Thanks for your question.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And I found that calling getpgid is not necessary. So I remove that code.

Comment thread libbpf-tools/doublefree.c Outdated

err = bpf_map_lookup_elem(fd, &key, &val);
if (err < 0) {
p_err("Failed to lookup elem on fd");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you intend the following code?

p_err("Failed` to lookup elem on fd: %d", fd);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right. I'll change the code. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd rather change the code to print strerror(errno) instead of fd

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
ptr = strtok(NULL, delim);
} else {
p_err("failed to exec %s", cmd);
goto cleanup;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about handling the error cases first?
This sentence duplicates line 196.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can and will remove duplicates but cannot handle the error cases first.
Because error case can be checked after tokenizing or calling fork.

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
return;
}
syms = syms_cache__get_syms(syms_cache, env.pid);
if (syms != NULL) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how about handling err case first?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sounds nice, that change could reduce one indent as well.

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
int deallocs_fd = bpf_map__fd(obj->maps.deallocs);

if (e->err == -1) {
p_err("This message should not be printed..");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about removing code that shouldn't happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's a good point. However, thinking that something will never be executed is just the developer's assumption. In software development, it is not uncommon for code that should not be executed to actually be executed. Especially when contributing code to open source projects, where multiple developers are involved, there is a higher possibility of unintentionally executing code that should not be executed. Therefore, I believe that this code should be kept as a precaution for future mistakes by someone else. However, the message itself seems lengthy and lacks specific details. I will consider possible improvements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't remove the message but changed it.

Copy link
Copy Markdown
Contributor Author

@Bojun-Seo Bojun-Seo left a comment

Choose a reason for hiding this comment

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

"ERROR Failed to poll perf_buffer" in usage example always displayed?

Yes, but it would be better not to be printed in normal case.

Comment thread libbpf-tools/doublefree.c Outdated
p_err("Failed to poll perf_buffer");
break;
}
if (getpgid(env.pid) < 0) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no need to limit it to the ESRCH errno case according to the following manual page.
https://linux.die.net/man/2/getpgid
getpgid returns -1 on error and errno is set appropriately.
And there is only one errno for getpgid, which is ESRCH.
Which means, if getpgid fails, the errno should be ESRCH.

Thanks for your question.

@Bojun-Seo Bojun-Seo force-pushed the doublefree branch 2 times, most recently from e661c38 to d451f55 Compare February 29, 2024 09:22
Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

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

Hi!

I just found some small nits, but I think we are ready to go:

$ sudo ./doublefree -c /tmp/doublefree_generator                                                                                                                       (remotes/bojun/doublefree) *%
[sudo] Mot de passe de francis : 
Désolé, essayez de nouveau.
[sudo] Mot de passe de francis : 
2024-Feb-29 16:20:13 INFO Execute command: /tmp/doublefree_generator(pid 94846)
Tracing doublefree... Hit Ctrl-C to stop
free(): double free detected in tcache 2

Allocation:
        #1 0x0056477a65b19b foo+0x12
        #2 0x0056477a65b1e3 main+0x27
        #3 0x007f2259229d90 [unknown]

First deallocation:
        #1 0x007f22592a53e0 free+0
        #2 0x0056477a65b1fd main+0x41
        #3 0x007f2259229d90 [unknown]

Second deallocation:
        #1 0x007f22592a53e0 free+0
        #2 0x0056477a65b213 main+0x57
        #3 0x007f2259229d90 [unknown]

Best regards.

Comment thread libbpf-tools/doublefree.c Outdated
Comment thread libbpf-tools/doublefree.c Outdated
execve(filepath, argv, NULL);
free(argv);

return -1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even though the fork()/exec() is successful, you still return -1 here.
Is this what you want to achieve?

Copy link
Copy Markdown
Contributor Author

@Bojun-Seo Bojun-Seo Feb 29, 2024

Choose a reason for hiding this comment

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

Thank you for the good question.
When execve succeeds, it launches a new program and the current process(child)'s execution flow is not passed to subsequent lines of code. As a result, employing an if statement for error checking is unnecessary; if the execution flow reaches beyond the execve invocation, it inherently signifies an error has occurred.

Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl Mar 1, 2024

Choose a reason for hiding this comment

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

When execve succeeds, it launches a new program and the current process(child)'s execution flow is not passed to subsequent lines of code. As a result, employing an if statement for error checking is unnecessary; if the execution flow reaches beyond the execve invocation, it inherently signifies an error has occurred.

Indeed! Long time I did not write C code and fork()/exec(), seems I begin to rust 🤣!

Comment thread libbpf-tools/doublefree.c Outdated
Copy link
Copy Markdown
Contributor

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

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

Hi!

I just have one comment with your latest push, but you can ignore it.

Best regards.

Comment thread libbpf-tools/doublefree.c
Comment on lines +305 to +312
if (!err) {
if (sinfo.sym_name)
printf(" %s+0x%lx", sinfo.sym_name, sinfo.sym_offset);
else
printf(" [unknown]");

printf(" (%s+0x%lx)\n", sinfo.dso_name, sinfo.dso_offset);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!err) {
if (sinfo.sym_name)
printf(" %s+0x%lx", sinfo.sym_name, sinfo.sym_offset);
else
printf(" [unknown]");
printf(" (%s+0x%lx)\n", sinfo.dso_name, sinfo.dso_offset);
}
if (err)
continue
if (sinfo.sym_name)
printf(" %s+0x%lx (%s+0x%lx)\n", sinfo.sym_name, sinfo.sym_offset, sinfo.dso_name, sinfo.dso_offset);
else
printf(" [unknown] (%s+0x%lx)\n", sinfo.dso_name, sinfo.dso_offset);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems more readable. I'll change the code as you suggested.
Thank you!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I found that the location of newline is not proper in my code.
So I change it. I can find it thanks for your help!

@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

Bojun-Seo commented Sep 29, 2025

I just rebased this PR and I have confirmed that this feature is still working well, and I hope it gets merged into the mainline.

I will discuss this PR at OSS Korea 2025.
https://osskorea2025.sched.com/event/2913v/detecting-double-free-with-bpf-bojun-seo-lg-electronics?iframe=yes&w=100%&sidebar=yes&bg=no
If you're interested, I would love for you to join and share your thoughts.

@baonq-me
Copy link
Copy Markdown

I just rebased this PR and I have confirmed that this feature is still working well, and I hope it gets merged into the mainline.

I will discuss this PR at OSS Korea 2025. https://osskorea2025.sched.com/event/2913v/detecting-double-free-with-bpf-bojun-seo-lg-electronics?iframe=yes&w=100%&sidebar=yes&bg=no If you're interested, I would love for you to join and share your thoughts.

Interesting. I come here from the OSS Korea event.

@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

Here is the video explaining this tool on OSS Korea.
https://youtu.be/5_OmthBoTo4?si=OvCRzREbPHsz8yz9

@Bojun-Seo Bojun-Seo force-pushed the doublefree branch 2 times, most recently from e7f5da2 to 2aace57 Compare April 29, 2026 02:39
Add doublefree tool to detect double free. It could detect user level double
free error currently and can be expanded to detect kernel level double free
error. Followings are the usage and example.

Usage:

  $ ./doublefree -h
  Usage: doublefree [OPTION...]
  Detect and report doublefree error.

  -c or -p is a mandatory option
  EXAMPLES:
      doublefree -p 1234             # Detect doublefree on process id 1234
      doublefree -c a.out            # Detect doublefree on a.out
      doublefree -c 'a.out arg'      # Detect doublefree on a.out with argument

    -c, --command=COMMAND      Execute the command and detect doublefree
    -p, --pid=PID              Detect doublefree on the specified process
    -v, --verbose              Verbose debug output
    -?, --help                 Give this help list
        --usage                Give a short usage message
    -V, --version              Print program version

  Mandatory or optional arguments to long options are also mandatory or optional
  for any corresponding short options.

  Report bugs to https://github.com/iovisor/bcc/tree/master/libbpf-tools.

Example:

  $ cat doublefree_generator.c
  #include <unistd.h>
  #include <stdlib.h>
  #include <stdio.h>

  int* foo() {
    printf("foo\n");
    return (int*)malloc(sizeof(int));
  }

  void bar(int* p) {
    printf("bar\n");
    free(p);
  }

  int main(int argc, char* argv[]) {
    sleep(10);
    int *val = foo();
    *val = 33;
    bar(val);
    *val = 84;
    bar(val);
    return 0;
  }
  $ gcc doublefree_generator.c
  $ sudo ./doublefree -c a.out
  2024-May-15 22:30:24 INFO Execute command: a.out(pid 99584)
  Tracing doublefree... Hit Ctrl-C to stop
  free(): double free detected in tcache 2

  Allocation:
          iovisor#1 0x00564d1455819b foo+0x12 (/home/bojun/doublefree/libbpf-tools/a.out+0x119b)
          iovisor#2 0x00564d145581e3 main+0x27 (/home/bojun/doublefree/libbpf-tools/a.out+0x11e3)
          iovisor#3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)

  First deallocation:
          iovisor#1 0x0070c4e94a53e0 free+0x0 (/usr/lib/x86_64-linux-gnu/libc.so.6+0xa53e0)
          iovisor#2 0x00564d145581fd main+0x41 (/home/bojun/doublefree/libbpf-tools/a.out+0x11fd)
          iovisor#3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)

  Second deallocation:
          iovisor#1 0x0070c4e94a53e0 free+0x0 (/usr/lib/x86_64-linux-gnu/libc.so.6+0xa53e0)
          iovisor#2 0x00564d14558213 main+0x57 (/home/bojun/doublefree/libbpf-tools/a.out+0x1213)
          iovisor#3 0x0070c4e9429d90 [unknown] (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29d90)
@Bojun-Seo
Copy link
Copy Markdown
Contributor Author

This patch is an updated version of the original submission. The changes listed below are assisted by GitHub Copilot, and the full patch is reviewed by Copilot before posting.

Changes compared to the original commit:

Bug fixes:

  • Wrong BPF probe macros: The original code used BPF_KPROBE/BPF_KRETPROBE for user-space probes, which are incorrect. Changed to BPF_UPROBE/BPF_URETPROBE.
  • calloc argument order: strlen(cmd) / 2 + 1 was off by one — the do/while loop always appends a NULL terminator, which could write one element past the allocated array. Changed to strlen(cmd) / 2 + 2 with an explanatory comment.
  • get_stackid type mismatch: The single get_stackid() function was used for both allocs (value: struct doublefree_info_t, 8 bytes) and frees (value: u32, 4 bytes) maps. This only worked accidentally due to little-endian layout. Split into get_alloc_stackid() and get_free_stackid() so each uses the correct value type.

Improvements:

  • Runtime timestamps: Replaced compile-time DATE/TIME macros with time()/strftime() so log timestamps reflect when the tool actually runs, not when it was compiled.
  • ARRAY_SIZE macro: Used ARRAY_SIZE(probes) instead of a hardcoded count for the probe attachment loop.
    Naming: Renamed dealloc/deallocs/deallocation to free/frees/free throughout for consistency with standard C terminology.

Changes:

  • Use a non-atomic counter for simplicity. This could cause a race condition, but it is very unlikely. It can be improved in the future.

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.

5 participants