2

(Edit: I have just fixed the getpid cache problem and rerun gdb and valgrind.)

(Edit: I just increase the size of stack for child from 200 bytes to 2000 bytes.)

I wrote the following program to learn how to use clone with CLONE_VM | CLONE_VFORK | CLONE_PARENT on linux x86-64 machine:

// test.c
#define _GNU_SOURCE
#include <stdio.h>
#include <assert.h>
#include <syscall.h>  // For syscall to call getpid
#include <signal.h>   // For SIGCHILD
#include <sys/types.h>// For getppid
#include <unistd.h>   // For getppid and sleep
#include <sched.h>    // For clone
#include <stdlib.h>   // For calloc and free

#define STACK_SIZE 2000

void Puts(const char *str)
{
    assert(fputs(str, stderr) != EOF);
}

void Sleep(unsigned int sec)
{
    do {
        sec = sleep(sec);
    } while(sec > 0);
}

int child(void *useless)
{
    Puts("The new process is created.\n");
    assert(fprintf(stderr, "pid = %d, ppid = %d\n", (pid_t) syscall(SYS_getpid), getppid()) > 0);

    Puts("sleep for 120 secs\n");
    Sleep(120);

    return 0;
}

int main(int argc, char* argv[])
{
    Puts("Allocate stack for new process\n");
    void *stack = calloc(STACK_SIZE, sizeof(char));
    void *stack_top = (void*) ((char*) stack + STACK_SIZE - 1);
    assert(fprintf(stderr, "stack = %p, stack top = %p\n", stack, stack_top) > 0);

    Puts("clone\n");
    int ret = clone(child, stack_top, CLONE_VM | CLONE_VFORK | CLONE_PARENT | SIGCHLD, NULL);
    Puts("clone returns\n");

    Puts("Free the stack\n");
    free(stack);

    if (ret == -1)
        perror("clone(child, stack, CLONE_VM | CLONE_VFORK, NULL)");
    else {
        ret = 0;
        Puts("Child dies...\n");
    }

    return ret;
}

I compiled the program using clang-7 test.c and ran it ./a.out in bash. It returned instantly with the following output:

Allocate stack for new process
stack = 0x492260, stack top = 0x492a2f
clone
The new process is created.
Segmentation fault

And it returns 139 meaning signal SIGSEGV is sent to my process.

Then I recompiled it using -g and use valgrind --trace-children=yes ./a.out to debug it:

|| ==14494== Memcheck, a memory error detector
|| ==14494== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
|| ==14494== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
|| ==14494== Command: ./a.out
|| ==14494== 
|| Allocate stack for new process
|| stack = 0x51f3040, stack top = 0x51f380f
|| clone
|| clone returns
|| Free the stack
|| Child dies...
|| ==14495== Invalid write of size 4
|| ==14495==    at 0x201322: ??? (in /home/nobodyxu/a.out)
|| ==14495==    by 0x4F2FCBE: clone (clone.S:95)
|| ==14495==  Address 0xffffffffffffffdc is not stack'd, malloc'd or (recently) free'd
|| ==14495== 
|| ==14495== 
|| ==14495== Process terminating with default action of signal 11 (SIGSEGV)
|| ==14495==  Access not within mapped region at address 0xFFFFFFFFFFFFFFDC
|| ==14495==    at 0x201322: ??? (in /home/nobodyxu/a.out)
|| ==14495==    by 0x4F2FCBE: clone (clone.S:95)
|| ==14495==  If you believe this happened as a result of a stack
|| ==14495==  overflow in your program's main thread (unlikely but
|| ==14495==  possible), you can try to increase the size of the
|| ==14495==  main thread stack using the --main-stacksize= flag.
|| ==14495==  The main thread stack size used in this run was 8388608.
|| ==14495== 
|| ==14495== HEAP SUMMARY:
|| ==14495==     in use at exit: 2,000 bytes in 1 blocks
|| ==14495==   total heap usage: 1 allocs, 0 frees, 2,000 bytes allocated
|| ==14495== 
|| ==14495== LEAK SUMMARY:
|| ==14495==    definitely lost: 0 bytes in 0 blocks
|| ==14495==    indirectly lost: 0 bytes in 0 blocks
|| ==14495==      possibly lost: 0 bytes in 0 blocks
|| ==14495==    still reachable: 2,000 bytes in 1 blocks
|| ==14495==         suppressed: 0 bytes in 0 blocks
|| ==14495== Rerun with --leak-check=full to see details of leaked memory
|| ==14495== 
|| ==14495== For counts of detected and suppressed errors, rerun with: -v
|| ==14495== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
|| ==14494== 
|| ==14494== HEAP SUMMARY:
|| ==14494==     in use at exit: 0 bytes in 0 blocks
|| ==14494==   total heap usage: 1 allocs, 1 frees, 2,000 bytes allocated
|| ==14494== 
|| ==14494== All heap blocks were freed -- no leaks are possible
|| ==14494== 
|| ==14494== For counts of detected and suppressed errors, rerun with: -v
|| ==14494== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

It also returned instantly and printed these.

I checked the generated assembly for 0x201322 and found out that it belongs to int main(int argc, char* argv[]):

||   20131d:    e8 8e 01 00 00          callq  2014b0 <clone@plt>
||   201322:    89 45 dc                mov    %eax,-0x24(%rbp)
||   201325:    48 bf 54 09 20 00 00    movabs $0x200954,%rdi
||   20132c:    00 00 00 
||   20132f:    e8 dc fd ff ff          callq  201110 <Puts>
||   201334:    48 bf ad 08 20 00 00    movabs $0x2008ad,%rdi
||   20133b:    00 00 00 

I also tried to use set follow-fork-mode child in gdb to debug it, but this doesn't work.

How to fix the segmentation fault?

JiaHao Xu
  • 2,452
  • 16
  • 31
  • I don't know how `clone` works exactly, but should the child process really free its own stack? And are you sure that `200` bytes is enough for the stack? That sounds *very* small. – Some programmer dude Jan 18 '19 at 08:10
  • @Someprogrammerdude I `free` the stack inside `main` right after `clone` returns. Since I specified `CLONE_VFORK`, it won't return until the child process dies or `exec`. – JiaHao Xu Jan 18 '19 at 08:13
  • @Someprogrammerdude So now the strange behavior disappears, but still I have no idea how to fix this. – JiaHao Xu Jan 18 '19 at 08:29
  • Simply put the `free()` in the parent process only. Something along the lines of `if (pid != child_pid) { free(stack); }`. – Lundin Jan 18 '19 at 08:47
  • @Lundin `clone` is nothing like `fork` and `vfork`. It doesn't return twice. `clone` accept a `void (*)(void *)` and `void *` as its args. The child process execute this function directly. – JiaHao Xu Jan 18 '19 at 08:49
  • @JiaHaoXu So are you saying that the actual problem here is the wrong format of the callback function? – Lundin Jan 18 '19 at 08:50
  • @Lundin I honestly don't know. I checked the manpage of `clone` and it provides me with the information I just told you. – JiaHao Xu Jan 18 '19 at 08:52
  • 1
    If I remove the assert line, no segfault. I think it's because of this: https://stackoverflow.com/questions/38067026/segfault-with-clone-and-printf – Hack Saw Jan 18 '19 at 08:57
  • 1
    @HackSaw Ah yeah, most likely. The problem isn't the assert but the fprintf, which isn't thread-safe. – Lundin Jan 18 '19 at 09:01
  • 1
    All hail the old debugging method of "comment out stuff until the error stops" – Hack Saw Jan 18 '19 at 09:08
  • @HackSaw Can function like `dprintf` and `sprintf` work under this situation since it doesn't need any buffer? – JiaHao Xu Jan 18 '19 at 09:09
  • To be honest, I have no idea. It's worth a try, as a learning experience. – Hack Saw Jan 18 '19 at 09:13
  • 1
    @HackSaw `dprintf` seems to work. (At least no `SIGSEGV` on the first run). – JiaHao Xu Jan 18 '19 at 09:14
  • @HackSaw At the end of day, it is the good old ways that save the day. Maybe you can post this as an answer? – JiaHao Xu Jan 18 '19 at 09:16

3 Answers3

1

The function printf and fprintf seem to be not thread safe without various guard rails. This is detailed in segfault with clone() and printf.

I found the problem by the brute force method of noting where the last print happened, and then commenting out lines after that until the error went away.

Hack Saw
  • 2,741
  • 1
  • 18
  • 33
1

This segfault might be specific to glibc. I build this code snippet with musl libc, and it works fine. It doesn't seem like this is related to the thread-safety of fprintf either because clone is passed with CLONE_VFORK, which suspends the parent process.

stensal
  • 401
  • 4
  • 8
  • Did you link statically or dynamically to `musl`? – JiaHao Xu Jan 20 '19 at 19:34
  • In my testing, it was linked statically. it can be linked dynamically to musl too. – stensal Jan 21 '19 at 01:44
  • Since `glibc` is linked dynamically, it may be only fair to link `musl libc` dynamically, too. Linking statically may have additional side effect (like `link-time-optimization`, etc). – JiaHao Xu Jan 21 '19 at 09:46
  • Since dynamic link has additional effect on program state (like stack, memory mapping, etc), I think link dynamically to it will make the test fairer and more useful, since `glibc` here is dynamically linked, too. – JiaHao Xu Jan 21 '19 at 09:55
  • I linked it dynamically with musl, and it works fine. – stensal Jan 21 '19 at 13:51
  • So it looks like it is really a bug in `glibc`. – JiaHao Xu Jan 22 '19 at 01:45
  • 1
    Your use of clone will suspend the parent process and it should not cause any problems for fprintf even if fprintf is not thread safe. There is no concurrent executions. – stensal Jan 22 '19 at 14:17
0

I use gdb to debug your program. The error messages are as follows.

The stack you applied for the child may have been released before the fprintf is real execution in the child function.

In the child function, add fflush(stdout); after the assert may solve your problem.

Continuing.
Allocate stack for new process
stack = 0x602010, stack top = 0x6027df
clone
The new process is created.
sleep for 20 secs
clone returns
Free the stack
*** Error in `test': double free or corruption (out): 0x0000000000602010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7ffff7a847e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7ffff7a8d37a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7ffff7a9153c]
/***/***/tmp/test[0x400969]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7ffff7a2d830]
/***/***/tmp/test[0x400729]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:21 12848672                           /***/***/tmp/test
00600000-00601000 r--p 00000000 08:21 12848672                           /***/***/tmp/test
00601000-00602000 rw-p 00001000 08:21 12848672                           /***/***/tmp/test
00602000-00623000 rw-p 00000000 00:00 0                                  [heap]
7ffff0000000-7ffff0021000 rw-p 00000000 00:00 0
7ffff0021000-7ffff4000000 ---p 00000000 00:00 0
7ffff77f7000-7ffff780d000 r-xp 00000000 08:01 786957                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff780d000-7ffff7a0c000 ---p 00016000 08:01 786957                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff7a0c000-7ffff7a0d000 rw-p 00015000 08:01 786957                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff7a0d000-7ffff7bcd000 r-xp 00000000 08:01 791529                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bcd000-7ffff7dcd000 ---p 001c0000 08:01 791529                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dcd000-7ffff7dd1000 r--p 001c0000 08:01 791529                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd1000-7ffff7dd3000 rw-p 001c4000 08:01 791529                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd3000-7ffff7dd7000 rw-p 00000000 00:00 0
7ffff7dd7000-7ffff7dfd000 r-xp 00000000 08:01 791311                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7fd3000-7ffff7fd6000 rw-p 00000000 00:00 0
7ffff7ff7000-7ffff7ff8000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00025000 08:01 791311                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffd000-7ffff7ffe000 rw-p 00026000 08:01 791311                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

Program received signal SIGSEGV, Segmentation fault.
__GI_abort () at abort.c:125
125     abort.c: No such file or directory.
wzq
  • 9
  • 3
  • As far as I understand my code, the stack is `free`d after the child `exit` or `exec`. Since in both situation, the stack won't be used, so I don't think it is because the stack of child is `free`d before `fprintf`. – JiaHao Xu Jan 21 '19 at 09:49
  • And I print to `stderr` all the time to avoid `buffer` problems and any delay, so `fflush(stdout)` probably won't do any help. – JiaHao Xu Jan 21 '19 at 09:50