Mitigating __pointer_chk_guard_local exposure and pointer mangling in ld.so

Anderson Nascimento anderson@allelesecurity.com
Sat May 30 00:46:28 GMT 2026


On Fri, May 29, 2026 at 5:30 PM Adhemerval Zanella Netto
<adhemerval.zanella@linaro.org> wrote:
>
>
>
> On 29/05/26 13:27, Anderson Nascimento wrote:
> > Hello,
> >
> > I have been investigating the behavior of Pointer Guard Encryption
> > when it is configured to use Thread Local Storage (TLS), and I noticed
> > an architectural asymmetry that inadvertently exposes an easier target
> > for attackers looking to bypass pointer mangling.
> >
> > Even when TLS pointer guarding is enabled, glibc exposes the variable
> > __pointer_chk_guard_local to the target binary with its full random
> > value intact.
> >
> > Initially, looking at security_init() in elf/rtld.c, it looks like an
> > unhandled condition. The block that checks if the pointer guard
> > feature is configured to use TLS lacks an alternative assignment
> > mechanism like __stack_chk_guard above it:
> >
> > ```
> > 821 #ifdef THREAD_SET_STACK_GUARD
> > 822   THREAD_SET_STACK_GUARD (stack_chk_guard);
> > 823 #else
> > 824   __stack_chk_guard = stack_chk_guard;
> > 825 #endif
> > 826
> > 827   /* Set up the pointer guard as well, if necessary.  */
> > 828   uintptr_t pointer_chk_guard
> > 829     = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);
> > 830 #ifdef THREAD_SET_POINTER_GUARD
> > 831   THREAD_SET_POINTER_GUARD (pointer_chk_guard);
> > 832 #endif
> > 833   __pointer_chk_guard_local = pointer_chk_guard;
> > ```
> >
> > Consequently, the __pointer_chk_guard_local variable retains the
> > secret guard value throughout the lifetime of the process, which can
> > be observed at runtime via GDB:
> >
> > ```
> > $ gdb test
> > ...
> > Breakpoint 1, 0x000000000040048b in main ()
> > (gdb) p/x __pointer_chk_guard_local
> > $1 = 0x2d24c8353dd46c
> > (gdb)
> > ```
> >
> > While __pointer_chk_guard_local is marked with attribute_hidden and is
> > not exported to the dynamic symbol table (.dynsym), keeping it
> > populated introduces a significant security asymmetry once an attacker
> > gains an arbitrary read primitive:
> >
> > To leak the guard from the Thread Control Block (TCB), an attacker
> > typically needs to locate the thread descriptor (%fs). That seems
> > harder than the approach described here.
> >
> > To leak __pointer_chk_guard_local, an attacker merely needs to
> > calculate the base address of ld.so. Because of how dynamic linking
> > works, pointers to ld.so structures are routinely exposed. An attacker
> > can trivially traverse the link_map from the main binary's GOT to
> > locate ld.so. Once the base is known, __pointer_chk_guard_local
> > resides at a predictable, static offset.
> >
> > This doesn't change much in practice because at least in the case of
> > the exit handlers, attackers armed with an arbitrary read primitive
> > have been able to obtain the pointer guard encryption key through
> > other ways, as discussed in [1] and [2], but by leaving this copy of
> > the secret key populated in memory, we provide attackers with a
> > significantly more accessible target to defeat pointer mangling
> > mitigations.
> >
> > I understand this behavior is needed due to some initialization
> > requirements. At compile time, IS_IN (rtld) forces PTR_MANGLE and
> > PTR_DEMANGLE to use __pointer_chk_guard_local because setjmp must run
> > early in the dynamic linker lifecycle before TLS or the thread
> > descriptor is ready:
> >
> > ```
> > 25 #if IS_IN (rtld)
> > 26 /* We cannot use the thread descriptor because in ld.so we use setjmp
> > 27    earlier than the descriptor is initialized.  */
> > 28 # ifdef __ASSEMBLER__
> > 29 #  define PTR_MANGLE(reg)       xor
> > __pointer_chk_guard_local(%rip), reg;     \
> > 30                                 rol $2*LP_SIZE+1, reg
> > 31 #  define PTR_DEMANGLE(reg)     ror $2*LP_SIZE+1, reg;
> >           \
> > 32                                 xor __pointer_chk_guard_local(%rip), reg
> > 33 # else
> > ```
> >
> > I initially tested a naive patch that nullified
> > __pointer_chk_guard_local inside dl_main right before applying RELRO
> > protections:
> >
> > ```
> > diff --git a/elf/rtld.c b/elf/rtld.c
> > index 12e1b4dd71..d5b7aee00b 100644
> > --- a/elf/rtld.c
> > +++ b/elf/rtld.c
> > @@ -2355,6 +2355,8 @@ dl_main (const ElfW(Phdr) *phdr,
> >         _dl_debug_post_relocate (main_map);
> >       }
> >
> > +  __pointer_chk_guard_local = 0;
> > +
> >     /* All ld.so initialization is complete.  Apply RELRO.  */
> >     _dl_protect_relro (&_dl_rtld_map);
> >
> > ```
> >
> > ```
> > $ gdb test
> > GNU gdb (Fedora Linux) 17.1-4.fc43
> > ...
> > Reading symbols from test...
> > (gdb) b main
> > Breakpoint 1 at 0x40048b
> > (gdb) r
> > Starting program: /tmp/test
> >
> > Breakpoint 1, 0x000000000040048b in main ()
> > (gdb) p/x __pointer_chk_guard_local
> > $1 = 0x0
> > (gdb)
> > ```
> >
> > While this cleanly neutralizes the variable in simple tests and makes
> > a code using atexit() to function properly, I believe it weakens the
> > security. Because ld.so is statically defined to use
> > __pointer_chk_guard_local for its entire lifecycle, zeroing it out
> > might effectively disable pointer mangling for all subsequent ld.so
> > runtime activities. I didn't investigate this behavior.
> >
> > Given that leaving this secret key in memory weakens the efficacy of
> > the pointer guard mitigation against arbitrary read primitives, has
> > any thought been given to refactoring how rtld handles mangling
> > post-initialization?
>
>
> I agree that it weakens the efficacy of the pointer guard. But if an
> attacker already has enough control over the process to traverse the
> link_map and access the main binary's GOT to search for the pointer
> guard address, I think it has enough control that hardening internal
> pointers isn't really effective.
>
> Also, other ABIs that place the pointer guard on TCB (i686, sparc,
> powerpc) just use the generic implementation
> (sysdeps/generic/pointer_guard.h), which is a no-op. So the question
> is whether we really need setjmp/longjmp for the loader (I would say
> it makes sense, and not enabling it across all ABIs is an oversight).
>
> Is this a vector for an exploit? Because it means that ABIs
> that use the global definition (aarch64, etc.) are subject to potential
> issue since libc.so __pointer_chk_guard@GLIBC_PRIVATE is an alias to the
> ld.so one.
>

It seems this approach is not well-known within vulnerability research
circles, as the majority of articles I have read regarding Pointer
Guard encryption bypasses do not utilize it.

Instead, they typically abuse the presence of the default exit handler
(_dl_fini at __exit_funcs) by applying a known plain-text attack.
Because researchers can read the encoded pointer and already know it
represents the encoded value of the _dl_fini function, they can easily
extract the key.

However, that method requires more effort than necessary because, as I
have demonstrated, the key is currently directly exposed anyway.

While this weakness admittedly does not change much in the current
scenario (as you pointed out), I decided to report it because it was
unclear whether this behavior is widely known or considered acceptable
by the community. Furthermore, if future efforts are made to harden
libc and mitigate these attacks, this specific vulnerability would
need to be addressed. Another consideration is that if _dl_fini were
removed from the default __exit_funcs handlers and this weakness were
resolved, it would block two methods for bypassing Pointer Guard
encryption. As far as I can tell, at least in the x86 architecture,
there would not be many alternative bypasses available other than
locating the TCB, which does not appear to be any easier than
exploiting the weakness discussed here.

> >
> > For instance, could PTR_MANGLE within rtld be adapted to dynamically
> > switch to checking the thread descriptor once a global initialization
> > flag is set? Alternatively, is there a cleaner way to isolate or clear
> > this easier target without introducing regressions to runtime linker
> > activities?
> >
>
> I don't think we can easily change the startup sequence to set up the
> thread pointer prior to ELF mapping, and it would most likely be a large
> change that would need to be validated on multiple ABIs.
>
> I think the cleanest solution would be something like we have for malloc:
> loader uses an internal implementation until the libc.so is reallocated
> then it switches to use the libc one.
>
> This would also allow us to consolidate and enable PTR_MANGLE on all ABIs,
> instead of making them an ABI-specific hardening.
>
> I did a POC for this [1], and enabled it only for x86_64. I need to check
> if everything does work correctly for other ABIs, and possibly enable
> on them as well (and remove the usage of the generic implementation).
> Maybe it would be better to *always* enable PTR_MANGLE and remove the
> generic no-op code.
>
> But I am not 100% convinced about this hardening, it does improve the
> information leakage but the thread model is not really clear to me.
> It also does not improve the information leakage on other ABIs, which
> is suboptimal.

It really is an information leakage that, in my view, shouldn't exist.
However, as I mentioned above, the core point is that an alternative,
well-known method to obtain the Pointer Guard encryption key already
exists. If that other weakness were to be addressed—specifically, the
fact that the encoded address of _dl_fini exists by default in
__exit_funcs—the vulnerability we are discussing would become the most
obvious and straightforward way to obtain the key, being significantly
simpler than the methods currently popularized in public research.

I am highlighting this because it was the very first thing that stood
out to me, and I didn't initially invest time into investigating why
_dl_fini needs to reside within __exit_funcs in the first place,
rather than simply being invoked directly.

>
>
> [1] https://sourceware.org/git/?p=glibc.git;a=shortlog;h=refs/heads/azanella/pointer-guard-hardening

Great, this was quick! I will test it.

>
> > I would love to hear thoughts on whether this is an area the community
> > wants to harden, and I am glad to assist in developing and testing a
> > proper patch.
> >
> > References:
> >
> > [1] - Code execution part 1: from exit to system
> > https://blog.rop.la/en/exploiting/2024/06/11/code-exec-part1-from-exit-to-system.html
> >
> > [2] - Notes on abusing exit handlers, bypassing pointer mangling and
> > glibc ptmalloc hooks
> > https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html
> >
> > Regards,
>

--
Anderson Nascimento
Allele Security Intelligence
https://www.allelesecurity.com


More information about the Libc-alpha mailing list