Feedback ticket ID: FB21797397
Summary
When using posix_spawn() with posix_spawnattr_set_uid_np() to spawn a child process with a different UID, the eslogger incorrectly reports a setuid event as an event originating from the parent process instead of the child process.
Steps to Reproduce
- Create a binary that do the following:
- Configure posix_spawnattr_t that set the process UIDs to some other user ID (I'll use 501 in this example).
- Uses
posix_spawn()to spawn a child process
- Run eslogger with the event types
setuid,fork,exec - Execute the binary as root process using sudo or from root owned shell
- Terminate the launched eslogger
- Observe the
processfield in thesetuidevent
Expected behavior
- The eslogger will report events indicating a process launch and uid changes so the child process is set to 501. i.e.:
forksetuid- Done by child processexec
Actual behavior
The process field in the setuid event is reported as the parent process (that called posix_spawn) - indicating UID change to the parent process.
Attachments
I'm attaching source code for a small project with a 2 binaries:
I'll add the source code for the project at the end of the file + attach filtered eslogger JSONs
- One that runs the descirbed
posix_spawnflow - One that produces the exact same sequence of events by doing different operation and reaching a different process state:
- Parent calls
fork() - Parent process calls
setuid(501) - Child process calls
exec()
- Parent calls
Why this is problematic
Both binaries in my attachment do different operations, achieving different process state (1 is parent with UID=0 and child with UID=501 while the other is parent UID=501 and child UID=0), but report the same sequence of events.
Code
#include <cstdio>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
// environ contains the current environment variables
extern char **environ;
extern "C" {
int posix_spawnattr_set_uid_np(posix_spawnattr_t *attr, uid_t uid);
int posix_spawnattr_set_gid_np(posix_spawnattr_t *attr, gid_t gid);
}
int main() {
pid_t pid;
int status;
posix_spawnattr_t attr;
// 1. Define the executable path and arguments
const char *path = "/bin/sleep";
char *const argv[] = {(char *)"sleep", (char *)"1", NULL};
// 2. Initialize spawn attributes
if ((status = posix_spawnattr_init(&attr)) != 0) {
fprintf(stderr, "posix_spawnattr_init: %s\n", strerror(status));
return EXIT_FAILURE;
}
// 3. Set the UID for the child process (e.g., UID 501)
// Note: Parent must be root to change to a different user
uid_t target_uid = 501;
if ((status = posix_spawnattr_set_uid_np(&attr, target_uid)) != 0) {
fprintf(stderr, "posix_spawnattr_set_uid_np: %s\n", strerror(status));
posix_spawnattr_destroy(&attr);
return EXIT_FAILURE;
}
// 4. Spawn the process
printf("Spawning /bin/sleep 1 as UID %d...\n", target_uid);
status = posix_spawn(&pid, path, NULL, &attr, argv, environ);
if (status == 0) {
printf("Successfully spawned child with PID: %d\n", pid);
// Wait for the child to finish (will take 63 seconds)
if (waitpid(pid, &status, 0) != -1) {
printf("Child process exited with status %d\n", WEXITSTATUS(status));
} else {
perror("waitpid");
}
} else {
fprintf(stderr, "posix_spawn: %s\n", strerror(status));
}
// 5. Clean up
posix_spawnattr_destroy(&attr);
return (status == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
// This program demonstrates fork + setuid + exec behavior for ES framework bug report
// 1. Parent forks
// 2. Parent does setuid(501)
// 3. Child waits with sleep syscall
// 4. Child performs exec
int main() {
printf("Parent PID: %d, UID: %d, EUID: %d\n", getpid(), getuid(), geteuid());
pid_t pid = fork();
if (pid < 0) {
// Fork failed
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
// Child process
printf("Child PID: %d, UID: %d, EUID: %d\n", getpid(), getuid(), geteuid());
// Child waits for a bit with sleep syscall
printf("Child sleeping for 2 seconds...\n");
sleep(2);
// Child performs exec
printf("Child executing child_exec...\n");
// Get the path to child_exec (same directory as this executable)
char *const argv[] = {(char *)"/bin/sleep", (char *)"2", NULL};
// Try to exec child_exec from current directory first
execv("/bin/sleep", argv);
// If exec fails
perror("execv");
return EXIT_FAILURE;
} else {
// Parent process
printf("Parent forked child with PID: %d\n", pid);
// Parent does setuid(501)
printf("Parent calling setuid(501)...\n");
if (setuid(501) != 0) {
perror("setuid");
// Continue anyway to observe behavior
}
printf("Parent after setuid - UID: %d, EUID: %d\n", getuid(), geteuid());
// Wait for child to finish
int status;
if (waitpid(pid, &status, 0) != -1) {
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child killed by signal %d\n", WTERMSIG(status));
}
} else {
perror("waitpid");
}
}
return EXIT_SUCCESS;
}
I was curious about this and, with a bit of looking, it's actually fairly easy to explain what's going on. So, getting into things...
When using posix_spawn() with posix_spawnattr_set_uid_np() to spawn a child process with a different UID, the eslogger incorrectly reports a setuid event as an event originating from the parent process instead of the child process.
...the answer here is basically "yes, that's exactly what's going on". That is, ES is saying the parent called setuid() because the parent IS in fact who called setuid. More specifically, ES_EVENT_TYPE_NOTIFY_SETUID is triggered by this MAC check inside the kernel's setuid() function. Critically, the kernel setuid() function is a general-purpose function which allows the setting of an "arbitrary" process ID. The user-space setuid() function works by passing in its own process as the target. However, posix_spawn's implementation is calling it here, inside the kernel as part of the posix_spawn syscall, to set the UID of the process it's creating.
Looking through the MAC check chain, it looks like this (what posix_spawn is doing) is an edge case that simply wasn't considered, as the proc_t (target process) is dropped by the MAC callback which the EndpointSecurity system hooks onto. So, the ES system is giving you the only information it has any access to.
That leads to the final issue, which is that I think this may be a bit of a "which came first” situation (chicken or egg?). All of this is happening inside posix_spawn before it's actually returned... which is, in theory at least, is happening somewhat before the process actually exists. Notably, exec() has NOT yet occurred on the new process, so you'd end receiving a setuid() call from a process you haven't yet approved the creation of. That assumes you get an exec all, as I think it's possible (though probably unlikely) for posix_spawn to fail after it's called setuid() but before it exec's.
Finally, I haven't tested this but I think it might be possible to at least partially differentiate these two cases, at least for the common case. Looking at your two cases, if you could use audit_token_to_ruid on es_message_t.process.audit_token to retrieve the UID of the audit token. Looking at the two cases:
(1)
posix_spawn: "parent with UID=0 and child with UID=501"
audit_token_to_ruid-> 0
es_event_setuid_t.uid-> 501
(2)
fork: "parent UID=501 and child UID=0"
audit_token_to_ruid-> 501
es_event_setuid_t.uid-> 501
One other point here— one thing to note here is that in both cases child's UID never actually changes. That's obvious for #2 (since it never called setuid) but in the case of #1 the child will ALREADY have UID 501 when you get the auth exec call.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware