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;
}
Feedback ticket ID: FB21797397
Thanks for that. Honestly, I think a bug report is the best path forward for this, and I took quick look and it seems that it’s landed in the right place.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"