AA_CHANGE_HAT(2)

πŸ’… AA_CHANGE_HAT(2) β€” the aa_change_hat page, but make it make sense πŸŽ€

okay so bestie, this is the AppArmor section 2 manual page for aa_change_hat and its little friend group. grab a strawberry drink πŸ“ and let's get into it.


✨ NAME ✨

aa_change_hat β€” change to or from a "hat" within a AppArmor profile 🎩

(yes, "a AppArmor". we don't fix the original, we respect the original. periodt.)


πŸ¦‹ 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, ...);

Link with -lapparmor when compiling. no cap, if you forget that flag you're gonna have a bad time 😭


πŸ’– DESCRIPTION πŸ’–

So. An AppArmor profile applies to an executable program. But what if part of the program needs different access permissions than the other parts? Like, the vibes are different in different rooms of the same house? 🏠✨ That's when the program can "change hats" to a different role β€” also known as a subprofile. it's giving wardrobe change at the Met Gala.

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. That magic_token is what lets you get back out of the subprofile later. it's your VIP wristband, keep it safe 🎟️

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. If the profile is not present, the call will fail with the appropriate error. harsh but fair.

The aa_change_hatv() function lets you pass a NULL terminated vector of pointers to subprofile names, which will be tried in order. The first subprofile in the vector that exists will be transitioned to, and if none of the subprofiles exist, the call will fail with the appropriate error. it's a backlog of options and honestly? relatable.

The aa_change_hat_vargs() function is a convenience wrapper for the aa_change_hatv() function. 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(). lowkey the good one if you're lazy like me.

If a program wants to return out of the current subprofile to the original profile, it calls aa_change_hat() with a pointer to NULL as the subprofile, and the original magic_token value. 🫢

⚠️ bestie READ THIS ONE. If the magic_token does not match the original magic_token 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. πŸ’€ like literally dead. If the magic_token matches the original token, then the process will change back to the original profile. No token, no entry, no mercy.

As both read(2) and write(2) are mediated, a file must be listed in a subprofile definition if the file is to be accessed while the process is in a "hat". no sneaky sneaky πŸ•΅οΈ


🌸 RETURN VALUE 🌸

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


πŸ”₯ ERRORS πŸ”₯

  • EINVAL β€” The apparmor kernel module is not loaded, or the communication via the /proc/*/attr/current file did not conform to protocol. (mutual confusion energy)
  • ENOMEM β€” Insufficient kernel memory was available. kernel said "not today" πŸ˜”
  • EPERM β€” The calling application is not confined by apparmor, the specified subprofile is not a hat profile, the task is being ptraced and the tracing task does not have permission to trace the specified subprofile, or the no_new_privs execution bit is enabled. that's like four separate L's in one error πŸ“
  • ECHILD β€” The application's profile has no hats defined for it. no hats?? no wardrobe?? nothing?? πŸ™ˆ
  • ENOENT β€” The specified subprofile does not exist in this profile, but other hats are defined. "we have hats, just not that hat."
  • EACCES β€” The specified magic token did not match, and permissions to change to the specified subprofile has been denied. This will in most situations also result in the task being killed, to prevent brute force attacks. πŸ’€ yep. dead. we warned you.

🌈 EXAMPLE 🌈

The following code examples shows 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, such as keeping unauthenticated network traffic handling separate from authenticated network traffic handling in OpenSSH, or executing user-supplied CGI scripts in apache. very "don't let the untrusted vibes into the trusted group chat" πŸ’…

The use of random(3) is simply illustrative. Use of /dev/urandom is recommended. (we're not telling you to use random(), we're telling you it's contrived, bestie. it's a demo.)

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 how the file descriptor was valid the whole time and it still said no? that's the hat doing its job. ate. πŸ‘‘


🧚 BUGS 🧚

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

heads up: 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. like, actually separate processes. not just vibes. 😭

⭐ SEE ALSO ⭐

apparmor(7), apparmor.d(5), apparmor_parser(8), aa_change_profile(2), aa_getcon(2) and <https://wiki.apparmor.net>. go read them, they're all in the same friend group πŸ’œ


k bye ily, go change some hats responsibly πŸŽ€βœ¨

β€” signed, sealed, delivered: AppArmor 4.1.8, dated 2026-10-18 πŸ¦‹πŸ’–

πŸ“¦ where this page comes from
  • arch rolling: apparmor 4.1.8-2 Β· GPL-2.0-only AND LGPL-2.0-only AND LGPL-2.1-only
  • πŸ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms