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);