Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libbpf-tools: Add new feature doublefree #4286

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Bojun-Seo
Copy link
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)

libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
Copy link
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.

libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Show resolved Hide resolved
libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
@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
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
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
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
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
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
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
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.

libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.bpf.c Show resolved Hide resolved
libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.c Show resolved Hide resolved
Copy link
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 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
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
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
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
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
Contributor

@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?

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

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
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
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.


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

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
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
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

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

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
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.

libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
return;
}
syms = syms_cache__get_syms(syms_cache, env.pid);
if (syms != NULL) {
Copy link
Contributor

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
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.

libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
int deallocs_fd = bpf_map__fd(obj->maps.deallocs);

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

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
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
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
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.

p_err("Failed to poll perf_buffer");
break;
}
if (getpgid(env.pid) < 0) {
Copy link
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
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.

libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
execve(filepath, argv, NULL);
free(argv);

return -1;
Copy link
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
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
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 🤣!

libbpf-tools/doublefree.c Outdated Show resolved Hide resolved
Copy link
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 on lines 305 to 313
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
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
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
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!

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>

  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:
          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)
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.

None yet

4 participants