Storage protect keys
Storage protect keys provide a mechanism for you to improve the reliability of your programs.
Protect keys apply to memory pages and work at the page level of granularity, similar to the mprotect subroutine, which can be used to read- or write-protect one or more pages. However, with storage keys you can mark sections of your data for specific levels of read and write access protection. Protection by storage keys is a function not only of the data page, but also of the thread attempting access. You can enable certain well-defined code paths to access data that is unavailable to your larger program, thereby encapsulating critical program data and protecting it against accidental damage.
Because access to key-protected pages is an attribute of the running thread, this mechanism extends naturally to multithreaded applications, but with the restriction that these use only 1:1 (or system scope) pthreads. The mprotect subroutine approach does not work reliably in a multithreaded environment, because you have to remove protection for all threads when you want to grant access to any thread. You can use both mechanisms simultaneously, and both are fully enforced; therefore, your program cannot write to a write-protected page even if a protect key would otherwise allow this.
- Encapsulate your program's private data completely, limiting access to just selected code paths.
- Protect your program's private data from accidental damage by always running with read access granted, but granting write access only when you intend to modify the data. This can be especially useful when code in a core engine allows calls out to untrusted code.
- When multiple private keys are available, additional granularity of data protection is possible.
You can simplify debugging by designing your application with key protection in mind. Setting a page's protect key and setting your active user keyset are both system calls, and therefore relatively expensive operations. You should design your program so that the frequency of these operations is not excessive.
User protect keys
- Pages that are exported read-only from the kernel will continue to be visible to your program. These pages have a protect key of UKEY_SYSTEM. This protect key is not a protect key that is under your program's control, but is always accessible by your program.
- All of your program's memory pages initially have the user public key assigned to them. As noted above, access to key 0 storage is always granted, making this the user public key.
- You can set protect keys only for your normal and shared data. You cannot, for example, protect library data, low memory shared with the kernel, or program text.
- Depending on the underlying hardware and administrative choice, only a limited number of user private keys (typically just one) are available. When your program assigns a private key to one or more of its pages, the data in those pages is no longer available by default. You must explicitly grant read or write access to this data by surrounding code paths that require access with calls to a new service to manage your active user keyset.
- The physical hardware likely supports additional protect keys that are not available for use as user protect keys.
- No special privilege is needed to assign protect keys to a page. The only requirement is current write access to the page.
- There is no control of execute authority with protect keys.
If your program accesses key protected data in violation of the access rights expressed in its active user keyset, it receives a SIGSEGV signal, as is already the case for violating read- or write-protected pages. If you choose to handle this signal, be aware that signal handlers are invoked without access to private keys. Signal handling code must add any needed access rights to the active user keyset before referencing key-protected data.
Child processes, created by the fork system call, logically inherit their parent's memory and running state. This includes the protect key associated with each page, as well as the parent thread's active user keyset at the time of fork.
Regions protected by user keys
System prerequisites for key protection
- Be running on physical hardware that provides storage key protection
- Be running the 64-bit kernel
- Enable the use of user protect keys
Program prerequisites for key protection
- Declare itself user-key aware and determine how many user protect keys are available, if any, with the ukey_enable subroutine.
- Organize its protected data on page boundaries.
- Assign a private key to each page you want to protect with the ukey_protect subroutine.
- If you protect the malloc'd data, remember to unprotect it before you free it.
- Prepare one or more keysets with the ukeyset_init subroutine.
- Possibly add the required keys to your keyset with the ukeyset_add_key subroutine, to enable future read or write accesses as required.
- Make a keyset active with the ukeyset_activate subroutine to grant the access rights defined in a keyset.
- Include any M:N (process scope) pthreads
- Be able to have a checkpoint performed on it (for example have CHECKPOINT=yes in the environment)
- Signal handlers receiving a ucontext_t structure. The previously active user keyset is in ucontext_t.__extctx.__ukeys, an array of two ints containing a 64-bit user keyset value
- User context structures compiled with __EXTABI__ defined (used by setcontext, getcontext, makecontext, swapcontext)
Subroutines
Subroutine | Description |
---|---|
sysconf | Use with _SC_AIX_UKEYS to determine the number of user keys supported (can be called on older versions of AIX) |
ukey_enable | Enable the user-key aware programming environment for your process, and report how many user keys are available |
ukeyset_init | Initialize a user keyset, which will represent a set of access rights to your private key or keys |
ukeyset_add_key | Add read or write access, or both for a specified key to a keyset |
ukeyset_remove_key | Remove or write access, or both for a specified key from a keyset |
ukeyset_add_set | Add all the access rights in one keyset to another |
ukeyset_remove_set | Remove all the access rights in one keyset from another |
ukeyset_activate | Apply the access rights in a keyset to the running thread |
ukeyset_ismember | Test if a given access right is contained in a keyset |
ukey_setjmp | Extended form of setjmp that preserves the active keyset (uses a ukey_jmp_buf structure) |
pthread_attr_getukeyset_np | Get the keyset attribute of a pthread |
pthread_attr_setukeyset_np | Set the keyset attribute for a pthread |
ukey_protect | Set a user protect key for a page-aligned range of user memory |
ukey_getkey | Retrieve the user protect key for a specified address |
Debugging
- When debugging a running program:
- The ukeyset subcommand displays the active keyset.
- The ukeyvalue subcommand displays the protect key associated with a given memory location.
- When debugging a core file, the ukeyexcept subcommand reports the active keyset, effective address of the key exception, and the storage key involved.
Hardware details
- The AMR is a 64-bit register comprising 32-bit pairs, one pair
per key, for a maximum of 32 keys numbered 0 through 31.
- The first bit of each pair represents write access to the corresponding numbered key.
- Similarly, the second bit of each pair represents read access to the corresponding numbered key.
- A bit value of 0 grants the corresponding access, and a bit value of 1 denies it.
- The bit pair granting access to key 0 is not controlled by your program. User key 0 is the user public key, and all threads always have full access to data in this key, without regard to your settings in the active user keyset.
- All the other bit pairs represent user private keys, which, subject to availability, you can use to protect your data as you see fit.
Sample program
The following is a sample user-key aware program:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <sys/ukeys.h>
#include <sys/syspest.h>
#include <sys/signal.h>
#include <sys/vminfo.h>
#define ROUND_UP(size,psize) ((size)+(psize)-1 & ~((psize)-1))
/*
* This is an example skeleton for a user key aware program.
*
* The private_data_1 structure will map a malloc'd key protected area
* which the main program can access freely, while the "untrusted"
* subroutine will only have read access.
*/
struct private_data_1 {
int some_data;
};
struct private_data_1 *p1; /* pointer to structure for protected data */
ukeyset_t keyset1RW; /* keyset to give signal handler access */
/*
* The untrusted function here should successfully read protected data.
*
* When the count is 0, it will just return so the caller can write
* the incremented value back to the protected field.
*
* When the count is 1, it will try to update the protected field itself.
* This should result in a SIGSEGV.
*/
int untrusted(struct private_data_1 *p1) {
int count = p1->some_data; /* We can read protected data */
if (count == 1)
p1->some_data = count; /* But should not be able to write it */
return count + 1;
}
/*
* Signal handler to catch the deliberate protection violation in the
* untrusted function above when count == 1.
* Note that the handler is entered with NO access to our private data.
*/
void handler(int signo, siginfo_t *sip, void *ucp) {
printf("siginfo: signo %d code %d\n", sip->si_signo, sip->si_code);
(void)ukeyset_activate(keyset1RW, UKA_REPLACE_KEYS);
exit(1);
}
main() {
int nkeys;
int pagesize = 4096; /* hardware data page size */
int padded_protsize_1; /* page padded size of protected data */
struct vm_page_info page_info;
ukey_t key1 = UKEY_PRIVATE1;
ukeyset_t keyset1W, oldset;
int rc;
int count = 0;
struct sigaction sa;
/*
* Attempt to become user key aware.
*/
nkeys = ukey_enable();
if (nkeys == -1) {
perror("ukey_enable");
exit(1);
}
assert(nkeys >= 2);
/*
* Determine the data region page size.
*/
page_info.addr = (long)&p1; /* address in data region */
rc = vmgetinfo(&page_info, VM_PAGE_INFO, sizeof(struct vm_page_info));
if (rc)
perror("vmgetinfo");
else
pagesize = page_info.pagesize; /* pick up actual page size */
/*
* We need to allocate page aligned, page padded storage
* for any area that is going to be key protected.
*/
padded_protsize_1 = ROUND_UP(sizeof(struct private_data_1), pagesize);
rc = posix_memalign((void **)&p1, pagesize, padded_protsize_1);
if (rc) {
perror("posix_memalign");
exit(1);
}
/*
* Initialize the private data.
* We can do this before protecting it if we want.
*
* Note that the pointer to the private data is in public storage.
* We only protect the data itself.
*/
p1->some_data = count;
/*
* Construct keysets to use to access the protected structure.
* Note that these keysets will be in public storage.
*/
rc = ukeyset_init(&keyset1W, 0);
if (rc) {
perror("ukeyset_init");
exit(1);
}
rc = ukeyset_add_key(&keyset1W, key1, UK_WRITE); /* WRITE */
if (rc) {
perror("ukeyset_add_key 1W");
exit(1);
}
keyset1RW = keyset1W;
rc = ukeyset_add_key(&keyset1RW, key1, UK_READ); /* R/W */
if (rc) {
perror("ukeyset_add_key 1R");
exit(1);
}
/*
* Restrict access to the private data by applying a private key
* to the page(s) containing it.
*/
rc = ukey_protect(p1, padded_protsize_1, key1);
if (rc) {
perror("ukey_protect");
exit(1);
}
/*
* Allow our general code to reference the private data R/W.
*/
oldset = ukeyset_activate(keyset1RW, UKA_ADD_KEYS);
if (oldset == UKSET_INVALID) {
printf("ukeyset_activate failed\n");
exit(1);
}
/*
* Set up a signal handler for SIGSEGV, to catch the deliberate
* key violation in the untrusted code.
*/
sa.sa_sigaction = handler;
SIGINITSET(sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
rc = sigaction(SIGSEGV, &sa, 0);
if (rc) {
perror("sigaction");
exit(1);
}
/*
* Program's main processing loop.
*/
while (count < 2) {
/*
* When we need to run "untrusted" code, change access
* to the private data to R/O by removing write access.
*/
(void)ukeyset_activate(keyset1W, UKA_REMOVE_KEYS);
/*
* Call untrusted subroutine here. It can only read
* the protected data passed to it.
*/
count = untrusted(p1);
/*
* Restore our full access to private data.
*/
(void)ukeyset_activate(keyset1W, UKA_ADD_KEYS);
p1->some_data = count;
}
ukey_protect(p1, padded_protsize_1, UKEY_PUBLIC);
free(p1);
exit(0);
}