Why can't I release the malloced memory space of the parent process in the child thread?

Viewed 149

In this program, I clone a child thread with the flags: CLONE_VM | CLONE_SETTLS. The child's stack space is malloced in the father process ahead.

In the child thread, I want to switch its stack by changing the rsp register directly. And the new stack space is also malloced in advance in father.

The bug is:
When I try to free the old stack space, I got a "Segmentation fault", like this screenshot of segfault at malloc.c:2981: No such file or directory

The following four operations will make the bug disappear:

  1. Don't switch the stack space
  2. malloc the new stack space after the old stack space and before the clone method
  3. malloc the new stack space in the child thread
  4. Don't use the TLS

the C program:

#define _GNU_SOURCE

#include <sched.h>
#include <unistd.h>
#include <asm/prctl.h>
#include <string.h>
#include <malloc.h>

extern void gogo_switch_new_free_old_and_jmp(void *new_stack, void *old_stack, void *new_pc);

int arch_prctl(int code, unsigned long *addr);

typedef struct {
    void *stack_space;
    void *stack_top;
} stack_struct;

typedef struct {
    stack_struct stack;
    stack_struct stack2;
} T;

void get_tls(void *tls_addr) {
    arch_prctl(ARCH_GET_FS, tls_addr);
}

void task_done() {
    T *t;
    get_tls(&t);
    void *old_stack_space = t->stack.stack_space;
    t->stack.stack_space = t->stack2.stack_space;
    t->stack.stack_top = t->stack2.stack_top;
    gogo_switch_new_free_old_and_jmp(t->stack.stack_top, old_stack_space, NULL);
}

#define stack_size 128

int main() {
    stack_struct stack2;
    stack2.stack_space = malloc(stack_size);
    stack2.stack_top = stack2.stack_space + stack_size;

    T *t = malloc(sizeof(T));
    t->stack.stack_space = malloc(stack_size);
    t->stack.stack_top = t->stack.stack_space + stack_size;
    t->stack2 = stack2;

    clone((void *) task_done, t->stack.stack_top,
          CLONE_VM |
          CLONE_SETTLS,
          NULL, NULL, t, NULL);
    sleep(1000000000);
    return 0;
}

the Assembler:

#void gogo_switch_new_free_old_and_jmp(void *new_stack, void *old_stack, void *new_pc);
    .text
    .globl  gogo_switch_new_free_old_and_jmp
    .type   gogo_switch_new_free_old_and_jmp, @function
gogo_switch_new_free_old_and_jmp:
    # switch stack
    movq    %rdi,%rsp
    pushq   %rdx
    # free old space
    movq    %rsi,%rdi
    call    free@PLT
    #JMP 
    popq    %rax
    movq    %rsp,   %rdi
    addq    $8,     %rdi
    pushq   %rax
    ret
3 Answers

You cannot call clone with CLONE_VM or CLONE_SETTLS in a process using libc; it's only meaningful if you're implementing libc or writing your entire program in assembly or non-hosted C with all your syscalls written in inline assembly. This is because all of the functions in libc have a contract to call them with the ABI constraints satisfied, and those include the thread pointer (%fs segment register on x86_64) pointing to a valid TCB (thread control block) created by the implementation (libc) uniquely for the thread it's being used in, and in the format expected by the implementation.

Also, your stack size is way too small; passing a stack that's too small, allocated by malloc, will surely result in heap-based buffer overflow, clobbering unrelated memory belonging to the allocator or other parts of your program.

Concerning the ".... in tcache_init () at malloc.c:2981" error message.

For the sake of performances, the GLIBC's dynamic memory allocator uses the pthread's TLS to maintain pointer on a per-thread arena (i.e. a per-thread cache) into which the thread can make memory allocations with a dedicated mutex to increase the parallelism (otherwise all the dynamic allocations of the threads would be serialized if they systematically use the global mutex of the allocator).

As a consequence, when your "home made" thread created through clone() calls any malloc()/free() function, the dynamic memory allocator looks for the pthread's information stored in the TLS of the current thread in order to retrieve the thread's pointer on the arena. But those information are not coherent as the current thread is not pthread based. Hence, the crash.

Instead of the GLIBC's dynamic memory allocator, you should use the mmap() system call which does not rely on any pthread specific information.

You seem to be confused about CLONE_SETTLS and the tls argument to clone:

clone(2) manual page:

     CLONE_SETTLS (since Linux 2.5.32)
         The newtls argument is  the  new  TLS  (Thread  Local  Storage)  descriptor.   (See
         set_thread_area(2).)

set_thread_area(2) manual page:

       struct user_desc {
           unsigned int  entry_number;
           unsigned long base_addr;
           unsigned int  limit;
           unsigned int  seg_32bit:1;
           unsigned int  contents:2;
           unsigned int  read_exec_only:1;
           unsigned int  limit_in_pages:1;
           unsigned int  seg_not_present:1;
           unsigned int  useable:1; };

You've defined your own structure T which looks nothing like this, so things are getting confused.

Related