Search code examples
crustffiaddress-sanitizer

rust library returned Box object is automatically freed in C -- EDIT: not freed


I have a feeling that ONE rust Box<Context> (as struct Context * in C) is automatically freed when leaving main.

I have a rust library /home/codes/libspeakdet.so from following code in lib.rs compiled with cargo build

pub struct Context { pub sim: f32 }

#[no_mangle]
pub extern "C" fn make_context() -> Box<Context> {
    Box::new(Context { sim: 0.2733f32 })
}

#[no_mangle]
pub extern "C" fn drop_context(_ctx: Option<Box<Context>>) {}

And test.c compiled with cc -fsanitize=address -fno-omit-frame-pointer -o test -g test.c /home/codes/libspeakdet.so

struct Context *make_context();
void drop_context(struct Context *ctx);

int main(int argc, char *argv[])
{
    struct Context *ctx;

    ctx = make_context();
    //drop_context(ctx);
    ctx = make_context();

    return 0;
}

with the drop_context line commented, running test got output

==44143==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
...

and no leak report with the drop_context line uncommented. Having N make_context calls will have asan report N-1 objects direct leak.

I'm confused, because I think the ownership is a rust feature that frees memory.

How could a C program automatically free that ONE ctx when leaving main?

EDIT I think use Box to receive C pointer is a example from rust document. module level document of Box. Please help me understand it if I'm wrong.

... Here, the struct Foo* type from C is translated to Box, which captures the ownership constraints. ...

the document have EXACT code as mine.

EDIT I added these codes just before return 0 to verify the still reachable explain provided by the answers.

    //ctx = NULL;
    ctx = (struct Context *)malloc(7);
    //free(ctx);
  • setting ctx = NULL has no change to the asan report.
  • setting ctx to malloced 7 bytes made asan report 7 bytes leak.
  • after free the malloced 7 bytes, asan report became same as without these lines.

EDIT MORE

  • parallel experiment

compile the following c code to independent libleaktest.so, and link test.c to libleaktest.so instead, asan reports N leaks.

struct Context *make_context() {
    struct Context *ctx = (struct Context *)malloc(9);
    return ctx;
}

void drop_context(struct Context *ctx) {
    free(ctx);
}

EDIT MORE

some extra experiment result

valgrind is reporting N blocks of definitely lost as expected.

When Context size is <= 460 bytes, the last leak is not reported in asan.

When Context size is >= 461 bytes, the last leak is reported in asan.

Declare ctx as ctx[1], and use ctx[0] instead of ctx in following code, the last leak is reported in asan (with 4B size Context).

CONCLUSION

Thank you for help. I'm pretty sure now, that the N-1 report behavior is asan bug on rust code. If that block is reachable with 460B size, then it cannot become unreachable with 461B size.

P.S. Fortunately, after commenting each log4rs and println line, valgrind is quiet and correct again. That was initial reason why I came to use asan instead.


Solution

  • How could a C program automatically free that ONE ctx when leaving main?

    It can't, and doesn't, but you're misinterpreting the leak sanitizer.

    From the design doc (with my emphasis):

    The leak checker ... first halts the execution of the process. This ensures that the pointer graph does not change while we examine it, and allows us to collect any transient pointers from register contexts of running threads.

    LSan scans this memory for byte patterns that look like pointers to heap blocks ... Those blocks are considered reachable and their contents are also treated as live memory. In this manner we discover all blocks reachable from the root set. We then iterate over all existing heap blocks and report unreachable blocks as leaks

    If the leak sanitizer finds the address of your Context in the C variable ctx, the object will be considered reachable and not reported as a leak.


    The other way to answer this question

    How could a C program automatically free that ONE ctx when leaving main?

    is to point out that everything is freed when you leave main. Program exit reclaims all the program resources (apart from perhaps shared memory and similar cases).

    Technically it even reclaims unreachable allocations, because the whole process address space is getting torn down. The reason unreachable allocations are considered leaks is that they could accumulate invisibly during process runtime.