AA_CHANGE_HAT(2)

πŸ“¦ version 4.1.7, in opensuse-leap 16.0

πŸŽ€ AA_CHANGE_HAT(2) πŸŽ€

okay so bestie, welcome to aa_change_hat(2) β€” that's the AppArmor page (yes, the <italic>aa_change_hat</italic> section, manual 2, we're in syscall-wrapper territory πŸ’…). Sit down, grab a strawberry drink πŸ“, and let's get into it.


✨ NAME ✨

aa_change_hat β€” the way you change to or from a "hat" inside an AppArmor profile. yes, hats. we love a costume change πŸ‘‘


🌸 SYNOPSIS 🌸

#include <sys/apparmor.h>

int aa_change_hat (char *subprofile, unsigned long magic_token);

int aa_change_hatv (char *subprofiles[], unsigned long magic_token);

int aa_change_hat_vargs (unsigned long magic_token, ...);

and like, don't forget to link with -lapparmor when compiling or nothing works, no cap 🫢


πŸ¦‹ DESCRIPTION πŸ¦‹

So an AppArmor profile applies to an executable program, right? And sometimes one little portion of that program needs different access permissions than all the other portions. Rude of it, but okay. In that case the program can "change hats" to a different role β€” which is also called a subprofile. it's giving main character switches outfits mid-scene πŸ’…

To change into a new hat, it calls one of the family of change_hat functions. It passes in a pointer to the subprofile it wants to change into, plus a 64bit magic_token. And what's the token for? It's how you get back out of the subprofile later. keep it safe, bestie, it's literally your return ticket 🎟️

The aa_change_hat() function lets you specify the name of a single subprofile that the application wants to change into. A pointer to the name of the subprofile is passed along with the magic_token. And if the profile isn't there? The call fails with the appropriate error. sad but honest 😭

The aa_change_hatv() function lets you pass a NULL terminated vector of pointers to subprofile names, which get tried in order. The first subprofile in the vector that actually exists will be transitioned to, and if none of the subprofiles exist, the call fails with the appropriate error. like a queue at the club and nobody's on the list πŸ’€

The aa_change_hat_vargs() function is a convenience wrapper for aa_change_hatv(). After the magic_token it takes an arbitrary number of pointers to subprofile names. Similar to execl(3), aa_change_hat_vargs() assembles the list of subprofile names into a vector and calls aa_change_hatv(). it's the "I can't be bothered to build the array myself" option and honestly? relatable 🧚

Now β€” if a program wants to return out of the current subprofile back to the original profile, it calls aa_change_hat() with a pointer to NULL as the subprofile, plus the original magic_token value. And listen carefully here 🚨 If the magic_token does not match the original magic_token that was passed into the kernel when the program entered the subprofile, the change back to the original profile will not happen, and the current task will be killed. not a vibe check you want to fail, fr fr. If the magic_token matches the original token, then the process changes back to the original profile. slay βœ…

Also, since both read(2) and write(2) are mediated, a file must be listed in a subprofile definition if that file is going to be accessed while the process is in a "hat". no listing = no access, periodt 🚫


πŸ’– RETURN VALUE πŸ’–

On success, zero is returned. On error, -1 is returned, and errno(3) is set appropriately. classic.


πŸ”₯ ERRORS πŸ”₯

  • EINVAL β€” the apparmor kernel module isn't loaded, or the communication via the /proc/*/attr/current file didn't conform to protocol. the vibe was off, protocol-wise πŸ™ˆ
  • ENOMEM β€” insufficient kernel memory was available. kernel said "no thoughts, head empty" 😭
  • EPERM β€” the calling application isn't confined by apparmor, or the specified subprofile isn't a hat profile, or the task is being ptraced and the tracing task doesn't have permission to trace the specified subprofile, or the no_new_privs execution bit is enabled. multiple reasons, all of them a no πŸ’€
  • ECHILD β€” the application's profile has no hats defined for it. literally zero hats. not even one 🎩❌
  • ENOENT β€” the specified subprofile doesn't exist in this profile, but other hats are defined. so close yet so far πŸ˜”
  • EACCES β€” the specified magic token didn't match, and permissions to change to the specified subprofile have been denied. And heads up: this will in most situations also result in the task being killed, to prevent brute force attacks. yeah. don't brute force it. πŸ”ͺ

🌈 EXAMPLE 🌈

The following code examples show simple, if contrived, uses of aa_change_hat(); a typical use of aa_change_hat() will separate privileged portions of a process from unprivileged portions of a process β€” like keeping unauthenticated network traffic handling separate from authenticated network traffic handling in OpenSSH, or executing user-supplied CGI scripts in apache. see? actually useful πŸ‘‘

Quick note: the use of random(3) here is simply illustrative. Use of /dev/urandom is recommended. don't come for me, I'm just the messenger 🫢

First, a simple high-level overview of aa_change_hat() use:

 void foo (void) {
        unsigned long magic_token;

        /* get a random magic token value
        from our huge entropy pool */
        magic_token = random_function();

        /* change into the subprofile while
         * we do stuff we don't trust */
        aa_change_hat("stuff_we_dont_trust", magic_token);

        /* Go do stuff we don't trust -- this is all
         * done in *this* process space, no separate
         * fork()/exec()'s are done. */
        interpret_perl_stuff(stuff_from_user);

        /* now change back to our original profile */
        aa_change_hat(NULL, magic_token);
 }

Second, an example to show that files not listed in a subprofile ("hat") aren't accessible after an aa_change_hat() call:

 #include <stdlib.h>
 #include <string.h>
 #include <sys/apparmor.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <unistd.h>

 int main(int argc, char *argv[]) {
        int fd;
        unsigned long tok;
        char buf[10];

        /* random() is a poor choice */
        tok = random();

        /* open /etc/passwd outside of any hat */
        if ((fd=open("/etc/passwd", O_RDONLY)) < 0)
                perror("Failure opening /etc/passwd");

        /* confirm for ourselves that we can really read /etc/passwd */
        memset(&buf, 0, 10);
        if (read(fd, &buf, 10) == -1) {
                perror("Failure reading /etc/passwd pre-hat");
                _exit(1);
        }
        buf[9] = '\0';
        printf("/etc/passwd: %s\n", buf);

        /* change hat to the "hat" subprofile, which should not have
         * read access to /etc/passwd -- even though we have a valid
         * file descriptor at the time of the aa_change_hat() call. */
        if (aa_change_hat("hat", tok)) {
                perror("Failure changing hat -- aborting");
                _exit(1);
        }

        /* confirm that we cannot read /etc/passwd */
        lseek(fd,0,SEEK_SET);
        memset(&buf, 0, 10);
        if (read(fd, &buf, 10) == -1)
                perror("Failure reading /etc/passwd post-hat");
        buf[9] = '\0';
        printf("/etc/passwd: %s\n", buf);

        return 0;
 }

This code example requires the following profile to be loaded with apparmor_parser(8):

 /tmp/ch {
   /etc/ld.so.cache               mr,
   /etc/locale/**                 r,
   /etc/localtime                 r,
   /usr/share/locale/**           r,
   /usr/share/zoneinfo/**         r,
   /usr/lib/locale/**             mr,
   /usr/lib/gconv/*.so            mr,
   /usr/lib/gconv/gconv-modules*  mr,

   /lib/ld-*.so*         mrix,
   /lib/libc*.so*        mr,
   /lib/libapparmor*.so* mr,
   /dev/pts/*            rw,
   /tmp/ch               mr,

   /etc/passwd           r,

   ^hat {
     /dev/pts/*     rw,
   }
 }

The output when run:

 $ /tmp/ch
 /etc/passwd: root:x:0:
 Failure reading /etc/passwd post-hat: Permission denied
 /etc/passwd:
 $

see that last one? the Permission denied is the hat doing exactly what it's supposed to. iconic behaviour honestly πŸ’…βœ¨


πŸ’œ BUGS πŸ’œ

None known. If you find any, please report them at <https://gitlab.com/apparmor/apparmor/-/issues>. πŸ™

But β€” important aside β€” note that aa_change_hat(2) provides no memory barriers between different areas of a program; if address space separation is required, then separate processes should be used. so like, don't expect this to be a wall between your memory, because it is not 🌸


⭐ SEE ALSO ⭐

apparmor(7), apparmor.d(5), apparmor_parser(8), aa_change_profile(2), aa_getcon(2) and <https://wiki.apparmor.net>.


okay that's the whole page bestie, go forth and change hats responsibly πŸ‘‘πŸ¦‹

AppArmor 4.1.7 β€” 2026-05-19 β€” mwah, love u, bye πŸ’–