[LTP] [PATCH] lapi/tls: reserve pre-TCB space to avoid undefined behavior in clone10.c

Changwei Zou changwei.zou@canonical.com
Tue Feb 10 13:40:40 CET 2026


Hi Petr,
I have rewritten the touch_tls_in_child() function and pushed it to my 
GitHub account.
https://github.com/sheisc/ltp
The motivation was to avoid interacting with pthread’s memory model.
I have tested it on both x86_64 and arm64.
The command clone10 -i 100000 passed successfully on both platforms.
Could you please take a look at it?
Thank you very much.
Kine regards,
Changwei
1 On x86_64
```sh
x86_64_ltp$ ./testcases/kernel/syscalls/clone/clone10-i100000
clone10.c:66: TPASS:Parent(PID: 655699,TID:655699): TLS value correct: 100
clone10.c:66: TPASS:Parent(PID: 655699,TID:655699): TLS value correct: 100
Summary:
passed 100000
failed 0
broken 0
skipped 0
warnings 0
```
2 On arm64
```sh
arm64_ltp$ ./testcases/kernel/syscalls/clone/clone10-i100000
clone10.c:66: TPASS:Parent(PID: 222184,TID:222184): TLS value correct: 100
clone10.c:66: TPASS:Parent(PID: 222184,TID:222184): TLS value correct: 100
Summary:
passed 100000
failed 0
broken 0
skipped 0
warnings 0
```


On 2/10/26 18:03, Changwei Zou wrote:
> Hi Petr,
> LTP uses glibc.
> Even a simple library function like write(),
> which wraps the sys_write system call,
> is involved in the memory model of pthreads.
> Therefore, touch_tls_in_child() must behave almost exactly like a 
> pthread.
> Otherwise, memory corruption can occur when executing functions inside 
> glibc.
> In the current version of clone10.c, according to the assembly code,
> the statement tls_var = 0x65; already constitutes a buffer overflow on 
> x86_64.
> Unfortunately, the struct pthread is opaque and may vary between 
> different versions of glibc.
> I assume the purpose of clone10.c is to test whether the CLONE_SETTLS 
> flag works.
> Making touch_tls_in_child() behave exactly like a pthread, however, is 
> extremely difficult.
> static __thread int tls_var;
> static int touch_tls_in_child(void *arg)
> {
> // 0xfffffffffffffffc is -4
> // movl $0x65,%fs:0xfffffffffffffffc
> tls_var = 0x65;
> }
> ```sh
> (gdb) disassemble touch_tls_in_child
> Dump ofassemblercodeforfunctiontouch_tls_in_child:
> 0x000055555555be40 <+0>:endbr64
> 0x000055555555be44 <+4>:push%rbx
> 0x000055555555be45 <+5>:mov0x33c0c(%rip),%rdx# 0x55555558fa58 <tls_ptr>
> 0x000055555555be4c <+12>:xor%eax,%eax
> 0x000055555555be4e <+14>:mov$0x1002,%esi
> 0x000055555555be53 <+19>:mov$0x9e,%edi
> 0x000055555555be58 <+24>:call0x55555555b500<syscall@plt>
> 0x000055555555be5d <+29>:cmp$0xffffffffffffffff,%rax
> 0x000055555555be61 <+33>:je0x55555555bf1d<touch_tls_in_child+221>
> 0x000055555555be67 <+39>:movl$0x65,%fs:0xfffffffffffffffc//bufferoverflow?
> ```
> On 2/9/26 22:47, Petr Vorel wrote:
>> Hi Changwei,
>>
>>> Hi Petr,
>>> With the original upstream LTP,
>>> I ran clone10 -i 1000 on three machines (including AArch64 and AMD64), and
>>> it failed on all of them.
>>> This suggests there may be another bug that we still need to identify.
>> Yes, it's a separate bug, not relevant to your fix. I trigger it on x86_64.
>>
>> Kind regards,
>> Petr
>>
>>> Thank you very much for your invaluable information.
>>> Kind regards,
>>> Changwei
>>> *1. On an AArch64 cloud instance*
>>> ```sh
>>> azure@clone10-aarch64-kcp:~/orig/ltp$
>>> ./testcases/kernel/syscalls/clone/clone10-i1000
>>> clone10.c:68:TPASS:Parent(PID: 106163,TID:106163): TLS value correct: 100
>>> clone10.c:48:TINFO:Child(PID: 106163,TID:106200): TLS value set to: 101
>>> tst_test.c:1953:TBROK:TestkilledbySIGBUS!
>>> Summary:
>>> passed 36
>>> failed 0
>>> broken 1
>>> skipped 0
>>> warnings 0
>>> ```
>>> *2. On an AMD64 machine*
>>> ```sh
>>> ubuntu@ZBook:~/orig/ltp$ ./testcases/kernel/syscalls/clone/clone10-i1000
>>> clone10.c:48:TINFO:Child(PID: 125560,TID:125870): TLS value set to: 101
>>> clone10.c:68:TPASS:Parent(PID: 125560,TID:125560): TLS value correct: 100
>>> double freeorcorruption(out)
>>> clone10.c:48:TINFO:Child(PID: 125560,TID:125871): TLS value set to: 101
>>> clone10.c:68:TPASS:Parent(PID: 125560,TID:125560): TLS value correct: 100
>>> tst_test.c:1953:TBROK:TestkilledbySIGIOT/SIGABRT!
>>> Summary:
>>> passed 311
>>> failed 0
>>> broken 1
>>> skipped 0
>>> warnings 0
>>> ```
>>> *3. On an AArch64 machine*
>>> ```sh
>>> ubuntu@asus-pe100a:~/orig/ltp$
>>> ./testcases/kernel/syscalls/clone/clone10-i1000
>>> clone10.c:68:TPASS:Parent(PID: 158953,TID:158953): TLS value correct: 100
>>> clone10.c:48:TINFO:Child(PID: 158953,TID:159029): TLS value set to: 101
>>> tst_test.c:1953:TBROK:TestkilledbySIGSEGV!
>>> Summary:
>>> passed 75
>>> failed 0
>>> broken 1
>>> skipped 0
>>> warnings 0
>>> ```
>>> On 2/9/26 18:51, Petr Vorel wrote:
>>>> Hi Changwei,
>>>>> Allocate extra space before the TLS area to hold a struct pthread, ensuring
>>>>> THREAD_SELF->cancelhandling is initialized to 0. This prevents undefined
>>>>> behavior in __pthread_disable_asynccancel(), which is called at thread
>>>>> cancellation points such as write().
>>>>> Without this, touch_tls_in_child() could get stuck in tst_res().
>>>> LGTM, but I'd prefer others had a look on it.
>>>> Acked-by: Petr Vorel<pvorel@suse.cz>
>>>> BTW clone10.c segfaults w/a the patch when run with more iterations:
>>>> ./clone10 -i200
>>>> clone10.c:48: TINFO: Child (PID: 4271, TID: 4285): TLS value set to: 101
>>>> clone10.c:68: TPASS: Parent (PID: 4271, TID: 4271): TLS value correct: 100
>>>> clone10.c:48: TINFO: Child (PID: 4271, TID: 4286): TLS value set to: 101
>>>> clone10.c:68: TPASS: Parent (PID: 4271, TID: 4271): TLS value correct: 100
>>>> tst_test.c:1953: TBROK: Test killed by SIGSEGV!
>>>> Summary:
>>>> passed   15
>>>> failed   0
>>>> broken   1
>>>> skipped  0
>>>> warnings 0
>>>> Kind regards,
>>>> Petr
>>>>> (gdb) bt
>>>>>     0  futex_wait () at ../sysdeps/nptl/futex-internal.h:141
>>>>>     1  futex_wait_simple () at ../sysdeps/nptl/futex-internal.h:172
>>>>>     2  __libc_disable_asynccancel () at ../nptl/cancellation.c:100
>>>>>     3  __GI___libc_write () at ../sysdeps/unix/sysv/linux/write.c:26
>>>>>     4  __GI___libc_write () at ../sysdeps/unix/sysv/linux/write.c:24
>>>>>     5  print_result ()  at tst_test.c:387
>>>>>     6  tst_vres_ () at tst_test.c:401
>>>>>     7  tst_res_ () at tst_test.c:512
>>>>>     8  touch_tls_in_child (arg=<optimized out>)atclone10.c:48
>>>>>     9  thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:78
>>>>> Signed-off-by: Changwei Zou<changwei.zou@canonical.com>
>>>>> ---
>>>>>    include/lapi/tls.h | 16 +++++++++++++---
>>>>>    1 file changed, 13 insertions(+), 3 deletions(-)
>>>>> diff --git a/include/lapi/tls.h b/include/lapi/tls.h
>>>>> index 468fe3086..7f2fa18a1 100644
>>>>> --- a/include/lapi/tls.h
>>>>> +++ b/include/lapi/tls.h
>>>>> @@ -22,6 +22,15 @@
>>>>>    #define TLS_SIZE 4096
>>>>>    #define TLS_ALIGN 16
>>>>> +/*
>>>>> + * Space allocated large enough to hold a struct pthread.
>>>>> + *
>>>>> + * Zero-initialized to ensure THREAD_SELF->cancelhandling starts at 0,
>>>>> + * avoiding undefined behavior (e.g., in clone10.c) in __pthread_disable_asynccancel(),
>>>>> + * which is called at thread cancellation points such as write().
>>>>> + */
>>>>> +#define TLS_PRE_TCB_SIZE (TLS_ALIGN * 256)
>>>>> +
>>>>>    #if defined(__x86_64__)
>>>>>    typedef struct {
>>>>>    	void *tcb;
>>>>> @@ -36,10 +45,11 @@ extern void *tls_ptr;
>>>>>    static inline void *allocate_tls_area(void)
>>>>>    {
>>>>> -	void *tls_area = aligned_alloc(TLS_ALIGN, TLS_SIZE);
>>>>> +	char *tls_area = aligned_alloc(TLS_ALIGN, TLS_PRE_TCB_SIZE + TLS_SIZE);
>>>>>    	if (!tls_area)
>>>>>    		tst_brk(TBROK | TERRNO, "aligned_alloc failed");
>>>>> -	memset(tls_area, 0, TLS_SIZE);
>>>>> +	memset(tls_area, 0, TLS_PRE_TCB_SIZE + TLS_SIZE);
>>>>> +	tls_area += TLS_PRE_TCB_SIZE;
>>>>>    #if defined(__x86_64__)
>>>>>    	tcb_t *tcb = (tcb_t *)tls_area;
>>>>> @@ -59,7 +69,7 @@ static inline void free_tls(void)
>>>>>    {
>>>>>    	usleep(10000);
>>>>>    	if (tls_ptr) {
>>>>> -		free(tls_ptr);
>>>>> +		free(((char *)tls_ptr) - TLS_PRE_TCB_SIZE);
>>>>>    		tls_ptr = NULL;
>>>>>    	}
>>>>>    }


More information about the ltp mailing list