From ff66ed3a965e8d3872dbd26e5b2e41244e60aad3 Mon Sep 17 00:00:00 2001 From: Ariel Abreu Date: Wed, 20 Sep 2023 21:39:36 -0400 Subject: [PATCH] Emulate LoginWindow behavior in shellspawn LoginWindow is responsible for setting up the user session with launchd. For us, shellspawn serves the same purpose as LoginWindow, so we initialize the session there. This does require some additional dancing around with processes and sockets since launchd only expects a single process to register a session for a given user; this means that we couldn't just keep the old behavior with each shellspawn connection getting a new process and simply add-on launchd session initialization. In the new approach, shellspawn forks-off an instance for a user session and that instance has its own socket; this new instance is the one that receives shellspawn commands like the daemon previously did. The main socket (`/var/run/shellspawn.sock`) simply serves to start up new user sessions or connect to existing ones. I'm not quite sure I'm happy with the hacking around I did with shellspawn forks and sockets to get this to work; frankly, it introduces too much complexity into the shellspawn daemon. This is currently just a work-in-progress, so I might redesign that in a future commit. I'm currently leaning towards having a separate session manager daemon that the `darling` executable connects to and requests a session from; essentially, it would be what I added to shellspawn right now, but as its own separate daemon instead. --- src/shellspawn/shellspawn.c | 325 ++++++++++++++++++++++++++++++++++-- src/shellspawn/shellspawn.h | 6 + src/startup/darling.c | 67 ++++++++ 3 files changed, 380 insertions(+), 18 deletions(-) diff --git a/src/shellspawn/shellspawn.c b/src/shellspawn/shellspawn.c index 2f99825bc..5db31dd4c 100644 --- a/src/shellspawn/shellspawn.c +++ b/src/shellspawn/shellspawn.c @@ -35,16 +35,28 @@ along with Darling. If not, see . #include #include "shellspawn.h" #include "duct_signals.h" +#include + +#include +#include #define DBG 0 int g_serverSocket = -1; +uid_t g_sessionUID = -1; +gid_t g_sessionGID = -1; +struct sockaddr_un g_serverAddr; -void setupSocket(void); +void setupSocketAddr(uid_t sessionUID, struct sockaddr_un* addr); +int setupSocket(const struct sockaddr_un* outAddr); void listenForConnections(void); +void spawnSession(int fd); void spawnShell(int fd); void setupSigchild(void); void reapAll(void); +void sendClientSocket(int sockfd, int socketToSend); +int tryConnectSession(const struct sockaddr_un* addr); +void runSessionManager(void); int main(int argc, const char** argv) { @@ -52,7 +64,13 @@ int main(int argc, const char** argv) // we have to allow it to become a zombie, which is prevented // when we set the SIGCHLD signal to SA_NOCLDWAIT //setupSigchild(); - setupSocket(); + + setupSocketAddr(g_sessionUID, &g_serverAddr); + + g_serverSocket = setupSocket(&g_serverAddr); + if (g_serverSocket < 0) + exit(EXIT_FAILURE); + listenForConnections(); if (g_serverSocket != -1) @@ -60,36 +78,49 @@ int main(int argc, const char** argv) return 0; } -void setupSocket(void) +void setupSocketAddr(uid_t sessionUID, struct sockaddr_un* addr) { - struct sockaddr_un addr = { - .sun_family = AF_UNIX, - .sun_path = SHELLSPAWN_SOCKPATH - }; + memset(addr, 0, sizeof(*addr)); - g_serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); - if (g_serverSocket == -1) + addr->sun_family = AF_UNIX; + + if (sessionUID != -1) + snprintf(addr->sun_path, sizeof(addr->sun_path), "%s.%d", SHELLSPAWN_SOCKPATH, sessionUID); + else + snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", SHELLSPAWN_SOCKPATH); +} + +int setupSocket(const struct sockaddr_un* addr) +{ + int serverSocket = -1; + + serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); + if (serverSocket == -1) { perror("Creating unix socket"); - exit(EXIT_FAILURE); + return -1; } - fcntl(g_serverSocket, F_SETFD, FD_CLOEXEC); - unlink(SHELLSPAWN_SOCKPATH); + fcntl(serverSocket, F_SETFD, FD_CLOEXEC); + unlink(addr->sun_path); - if (bind(g_serverSocket, (struct sockaddr*) &addr, sizeof(addr)) == -1) + if (bind(serverSocket, (struct sockaddr*) addr, sizeof(*addr)) == -1) { perror("Binding the unix socket"); - exit(EXIT_FAILURE); + close(serverSocket); + return -1; } - chmod(addr.sun_path, 0600); + chmod(addr->sun_path, 0600); - if (listen(g_serverSocket, 1) == -1) + if (listen(serverSocket, 1) == -1) { perror("Listening on unix socket"); - exit(EXIT_FAILURE); + close(serverSocket); + return -1; } + + return serverSocket; } void listenForConnections(void) @@ -106,8 +137,16 @@ void listenForConnections(void) if (fork() == 0) { + // we don't need the server socket + close(g_serverSocket); + g_serverSocket = -1; + fcntl(sock, F_SETFD, FD_CLOEXEC); - spawnShell(sock); + if (g_sessionUID != -1) + spawnShell(sock); + else + spawnSession(sock); + exit(EXIT_SUCCESS); } else @@ -117,6 +156,153 @@ void listenForConnections(void) } } +void sendClientSocket(int sockfd, int socketToSend) +{ + struct shellspawn_cmd cmd; + struct msghdr msg; + struct iovec iov; + char cmsgbuf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmptr; + + memset(&cmd, 0, sizeof(cmd)); + memset(&iov, 0, sizeof(iov)); + memset(&msg, 0, sizeof(msg)); + + cmd.cmd = SHELLSPAWN_SESSION; + cmd.data_length = 0; + + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + + iov.iov_base = &cmd; + iov.iov_len = sizeof(cmd); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + cmptr = CMSG_FIRSTHDR(&msg); + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + memcpy(CMSG_DATA(cmptr), &socketToSend, sizeof(socketToSend)); + + if (sendmsg(sockfd, &msg, 0) < 0) + { + fprintf(stderr, "Error sending reply: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } +} + +int tryConnectSession(const struct sockaddr_un* addr) +{ + int clientSocket = -1; + + clientSocket = socket(AF_UNIX, SOCK_STREAM, 0); + if (clientSocket == -1) + return -1; + + if (connect(clientSocket, (struct sockaddr*) addr, sizeof(*addr)) == -1) + { + close(clientSocket); + return -1; + } + + return clientSocket; +} + +void spawnSession(int sockfd) +{ + struct shellspawn_cmd cmd; + struct msghdr msg; + struct iovec iov; + int uidgid[2]; + struct sockaddr_un sessionAddr; + int sessionSocket = -1; + int clientSocket = -1; + + memset(&msg, 0, sizeof(msg)); + + iov.iov_base = &cmd; + iov.iov_len = sizeof(cmd); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + if (recvmsg(sockfd, &msg, 0) != sizeof(cmd)) + { + if (DBG) puts("bad recvmsg"); + goto err; + } + + if (cmd.cmd != SHELLSPAWN_SESSION) + { + if (DBG) puts("bad command type"); + goto err; + } + + if (cmd.data_length != sizeof(uidgid)) + { + if (DBG) puts("bad command data"); + goto err; + } + + if (read(sockfd, uidgid, cmd.data_length) != cmd.data_length) + goto err; + + setupSocketAddr(uidgid[0], &sessionAddr); + + clientSocket = tryConnectSession(&sessionAddr); + if (clientSocket >= 0) + { + // we already had a session set up; avoid setting up a new one. + + // now reply to the client with the session client socket + sendClientSocket(sockfd, clientSocket); + close(sockfd); + return; + } + + // we didn't have a session set up already, so let's create one. + + sessionSocket = setupSocket(&sessionAddr); + if (sessionSocket < 0) + { + if (DBG) puts("failed to setup session socket"); + goto err; + } + + if (fork() == 0) + { + // this is the new session manager process + g_serverSocket = sessionSocket; + g_sessionUID = uidgid[0]; + g_sessionGID = uidgid[1]; + g_serverAddr = sessionAddr; + close(sockfd); + runSessionManager(); + } + else + { + // we don't care about the listening side of the session socket + close(sessionSocket); + sessionSocket = -1; + + clientSocket = tryConnectSession(&sessionAddr); + if (clientSocket == -1) + { + fprintf(stderr, "Error connecting to shellspawn session\n"); + exit(EXIT_FAILURE); + } + + // now reply to the client with the session client socket + sendClientSocket(sockfd, clientSocket); + close(sockfd); + } + + return; + +err: + close(sockfd); +} + void spawnShell(int fd) { pid_t shell_pid = -1; @@ -177,6 +363,7 @@ void spawnShell(int fd) argv = (char**) realloc(argv, sizeof(char*) * (argc + 1)); argv[argc] = param; if (DBG) printf("add arg: %s\n", param); + param = NULL; // `argv` assumes ownership of the string argc++; } break; @@ -187,6 +374,7 @@ void spawnShell(int fd) { if (DBG) printf("set env: %s\n", param); putenv(param); + param = NULL; // `putenv()` assumes ownership of the string } break; } @@ -249,6 +437,7 @@ void spawnShell(int fd) argv = realloc(argv, 0); alloc_exec = param; if (DBG) printf("setexec: %s\n", param); + param = NULL; // `alloc_exec` assumes ownership of the string break; } } @@ -439,3 +628,103 @@ void reapAll(void) { while (waitpid((pid_t)(-1), 0, WNOHANG) > 0); } + +void runSessionManager(void) +{ + const char* login = NULL; + struct passwd* pw = NULL; + char* homedirTmp = NULL; + mach_port_t subset = MACH_PORT_NULL; + mach_port_t dummyService = MACH_PORT_NULL; + kern_return_t kr = KERN_SUCCESS; + + // we are the shellspawn instance for the user's session. this means we're in charge + // of the user's session and are responsible for setting it up as macOS would. + // we are essentially going to be doing the same kind of setup that LoginWindow + // would do on macOS. + + // switch ourselves to run under the user's UID and GID (but under Darling, this is all fake anyways) + setgid(g_sessionGID); + setuid(g_sessionUID); + + // fix up env vars to what they should be + pw = getpwuid(g_sessionUID); + if (pw != NULL) + login = pw->pw_name; + + if (!login) + login = getlogin(); + + setenv("PATH", "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin", 1); + setenv("TMPDIR", "/private/tmp", 1); + + asprintf(&homedirTmp, "HOME=/Users/%s", login); + putenv(homedirTmp); + homedirTmp = NULL; // `putenv()` assumes ownership of the string + + // + // IMPORTANT NOTE + // + // okay, so i have no clue how this whole session switching is supposed to work. + // or rather, i do have *some* idea: we need to call `create_and_switch_to_per_session_launchd` + // to switch to a per-user launchd (which will also trigger LaunchAgents for the user). + // the problem is that the way this function works internally is that it calls `_vprocmgr_move_subset_to_user`. + // this function, in turn, moves the current subset into the new per-user launchd session. + // obviously, this means we must already have a subset prior to calling `create_and_switch_to_per_session_launchd`. + // + // the thing is, i can't see how (the modern version of) LoginWindow does this. maybe the API has changed since then + // and it no longer moves a subset into the new session, but i can't see it creating a new subset at any point during + // the session setup. but clearly, *we* need to do this, so let's go ahead and do so. + // + + if ((kr = bootstrap_subset(bootstrap_port, mach_task_self(), &subset)) != KERN_SUCCESS) + { + fprintf(stderr, "Failed to create bootstrap subset: %d\n", kr); + exit(EXIT_FAILURE); + } + + // replace the bootstrap port with our subset port + if ((kr = task_set_bootstrap_port(mach_task_self(), subset)) != KERN_SUCCESS) + { + fprintf(stderr, "Failed to replace bootstrap port: %d\n", kr); + exit(EXIT_FAILURE); + } + + // we no longer need the old bootstrap port + mach_port_deallocate(mach_task_self(), bootstrap_port); + + bootstrap_port = subset; + + // + // ANOTHER IMPORTANT NOTE + // + // launchd requires the new subset to have at least one Mach service registered before switching to the per-user + // session. to that end, we simply create a bogus service here. + // + if ((kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &dummyService)) != KERN_SUCCESS) + { + fprintf(stderr, "Failed to allocate dummy service port: %d\n", kr); + exit(EXIT_FAILURE); + } + + if ((kr = mach_port_insert_right(mach_task_self(), dummyService, dummyService, MACH_MSG_TYPE_MAKE_SEND)) != KERN_SUCCESS) + { + fprintf(stderr, "Failed to insert send right into dummy service port: %d\n", kr); + exit(EXIT_FAILURE); + } + + if ((kr = bootstrap_register(bootstrap_port, "org.darlinghq.shellspawn.dummy-service", dummyService)) != KERN_SUCCESS) + { + fprintf(stderr, "Failed to register dummy service: %d\n", kr); + exit(EXIT_FAILURE); + } + + // set up a launchd session + if (create_and_switch_to_per_session_launchd("shellspawn", LAUNCH_GLOBAL_ON_DEMAND) < 0) + { + fprintf(stderr, "Failed to set up launchd user session.\n"); + exit(EXIT_FAILURE); + } + + listenForConnections(); +} diff --git a/src/shellspawn/shellspawn.h b/src/shellspawn/shellspawn.h index c9eb396e6..50504ed05 100644 --- a/src/shellspawn/shellspawn.h +++ b/src/shellspawn/shellspawn.h @@ -36,6 +36,12 @@ enum { SHELLSPAWN_SIGNAL, // pass a signal from client SHELLSPAWN_SETUIDGID, // set virtual uid and gid SHELLSPAWN_SETEXEC, // set the executable to spawn (instead of a shell). must be given before any ADDARG commands. + + // start a session for a user with the given UID and GID (same format as SETUIDGID). + // this will either start or connect to a shellspawn session agent with a socket at `${SHELLSPAWN_SOCKPATH}.${UID}`. + // + // shellspawn will reply to this command with a socket FD for the agent that can be used to send it commands. + SHELLSPAWN_SESSION, }; struct __attribute__((packed)) shellspawn_cmd diff --git a/src/startup/darling.c b/src/startup/darling.c index 6014f46e8..db45699e4 100644 --- a/src/startup/darling.c +++ b/src/startup/darling.c @@ -577,6 +577,70 @@ int connectToShellspawn(void) return sockfd; } +static int startShellspawnSession(int sockfd) +{ + int ids[2] = { g_originalUid, g_originalGid }; + pushShellspawnCommandData(sockfd, SHELLSPAWN_SESSION, ids, sizeof(ids)); + + struct shellspawn_cmd cmd; + char cmsgbuf[CMSG_SPACE(sizeof(int))]; + struct msghdr msg; + struct iovec iov; + struct cmsghdr *cmptr; + int sessionFD; + + memset(&msg, 0, sizeof(msg)); + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + + iov.iov_base = &cmd; + iov.iov_len = sizeof(cmd); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + if (recvmsg(sockfd, &msg, 0) != sizeof(cmd)) + { + fprintf(stderr, "Error receiving reply from shellspawn: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + if (cmd.cmd != SHELLSPAWN_SESSION) + { + fprintf(stderr, "Invalid reply from shellspawn: invalid reply type %d\n", cmd.cmd); + exit(EXIT_FAILURE); + } + + if (cmd.data_length != 0) + { + fprintf(stderr, "Invalid reply from shellspawn: invalid reply data length %u (expected 0)\n", cmd.data_length); + exit(EXIT_FAILURE); + } + + cmptr = CMSG_FIRSTHDR(&msg); + + if (cmptr == NULL || cmptr->cmsg_level != SOL_SOCKET || cmptr->cmsg_type != SCM_RIGHTS) + { + fprintf(stderr, "Invalid reply from shellspawn: no attached FD\n"); + exit(EXIT_FAILURE); + } + + if (cmptr->cmsg_len != CMSG_LEN(sizeof(int))) + { + fprintf(stderr, "Invalid reply from shellspawn: invalid CMSG length %lu (expected %lu)\n", cmptr->cmsg_len, sizeof(int)); + exit(EXIT_FAILURE); + } + + memcpy(&sessionFD, CMSG_DATA(cmptr), sizeof(int)); + + if (sessionFD < 0) + { + fprintf(stderr, "Invalid reply from shellspawn: invalid FD %d\n", sessionFD); + exit(EXIT_FAILURE); + } + + return sessionFD; +} + void setupShellspawnEnv(int sockfd) { static const char* skip_vars[] = { @@ -703,6 +767,7 @@ void spawnShell(const char** argv) buffer = NULL; sockfd = connectToShellspawn(); + sockfd = startShellspawnSession(sockfd); setupShellspawnEnv(sockfd); @@ -727,6 +792,8 @@ void spawnBinary(const char* binary, const char** argv) int sockfd; sockfd = connectToShellspawn(); + sockfd = startShellspawnSession(sockfd); + setupShellspawnEnv(sockfd); pushShellspawnCommand(sockfd, SHELLSPAWN_SETEXEC, binary);