[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