/* * This file is part of Darling. * * Copyright (C) 2023 Darling Developers * * Darling is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Darling is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Darling. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shellsession.h" #include "shellspawn.h" #define MAX_ACCESS_TRIES 10 static void setup_socket_addr(struct sockaddr_un* addr); static void setup_session_socket_addr(unsigned int uid, struct sockaddr_un* addr); static int setup_socket(const struct sockaddr_un* addr); static void listen_for_connections(int server_socket); static void spawn_session(int sockfd); static void send_client_socket(int sockfd, int socket_to_send); static int try_connect_session(const struct sockaddr_un* addr); static void run_session_manager(unsigned int uid, unsigned int gid); int main(int argc, const char* const* argv) { struct sockaddr_un server_addr; int server_socket = -1; setup_socket_addr(&server_addr); server_socket = setup_socket(&server_addr); if (server_socket < 0) { exit(EXIT_FAILURE); } listen_for_connections(server_socket); if (server_socket >= 0) { close(server_socket); } return 0; }; static void setup_socket_addr(struct sockaddr_un* addr) { memset(addr, 0, sizeof(*addr)); addr->sun_family = AF_UNIX; snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", SHELLSESSION_SOCKPATH); }; static void setup_session_socket_addr(unsigned int uid, struct sockaddr_un* addr) { memset(addr, 0, sizeof(*addr)); addr->sun_family = AF_UNIX; snprintf(addr->sun_path, sizeof(addr->sun_path), "%s.%d", SHELLSPAWN_SOCKPATH, uid); }; static int setup_socket(const struct sockaddr_un* addr) { int server_socket = -1; server_socket = socket(AF_UNIX, SOCK_STREAM, 0); if (server_socket == -1) { perror("Creating unix socket"); goto err; } fcntl(server_socket, F_SETFD, FD_CLOEXEC); unlink(addr->sun_path); if (bind(server_socket, (struct sockaddr*)addr, sizeof(*addr)) == -1) { perror("Binding the unix socket"); goto err; } chmod(addr->sun_path, S_IRUSR | S_IWUSR); if (listen(server_socket, 1) == -1) { perror("Listening on unix socket"); goto err; } return server_socket; err: if (server_socket >= 0) { close(server_socket); } return -1; }; static void listen_for_connections(int server_socket) { int sock; struct sockaddr_un client_addr; socklen_t len = sizeof(client_addr); while (true) { sock = accept(server_socket, (struct sockaddr*)&client_addr, &len); if (sock == -1) { break; } if (fork() == 0) { // we don't need the server socket close(server_socket); server_socket = -1; fcntl(sock, F_SETFD, FD_CLOEXEC); spawn_session(sock); exit(EXIT_SUCCESS); } else { close(sock); } } }; void spawn_session(int sockfd) { shellsession_cmd_t cmd; struct sockaddr_un session_addr; int client_socket = -1; char session_started_path[PATH_MAX]; int started_fd = -1; if (read(sockfd, &cmd, sizeof(cmd)) != sizeof(cmd)) { goto err; } setup_session_socket_addr(cmd.uid, &session_addr); snprintf(session_started_path, sizeof(session_started_path), "%s.%d", SHELLSESSION_STARTED_PATH, cmd.uid); started_fd = open(session_started_path, O_RDONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (started_fd < 0 && errno == EEXIST) { // we already had a session set up; avoid setting up a new one. // wait until shellspawn starts for (int i = 0; i < MAX_ACCESS_TRIES; i++) { if (access(session_addr.sun_path, F_OK) == 0) { break; } sleep(1); } client_socket = try_connect_session(&session_addr); if (client_socket < 0) { goto err; } // now reply to the client with the session client socket send_client_socket(sockfd, client_socket); close(sockfd); return; } else if (started_fd < 0) { perror("Failed to create session indicator file"); goto err; } // we didn't have a session set up already, so let's create one. // we only need to create the session-running file; we don't need to write anything to it close(started_fd); if (fork() == 0) { // this is the new session manager process close(sockfd); run_session_manager(cmd.uid, cmd.gid); } else { // wait until shellspawn starts for (int i = 0; i < MAX_ACCESS_TRIES; i++) { if (access(session_addr.sun_path, F_OK) == 0) { break; } sleep(1); } client_socket = try_connect_session(&session_addr); if (client_socket < 0) { fprintf(stderr, "Error connecting to shellspawn session\n"); exit(EXIT_FAILURE); } // now reply to the client with the session client socket send_client_socket(sockfd, client_socket); close(sockfd); } return; err: close(sockfd); }; static void send_client_socket(int sockfd, int socket_to_send) { char dummy = '\0'; struct msghdr msg; char cmsgbuf[CMSG_SPACE(sizeof(int))]; struct cmsghdr *cmptr; struct iovec iov; memset(&msg, 0, sizeof(msg)); memset(&iov, 0, sizeof(iov)); iov.iov_base = &dummy; iov.iov_len = sizeof(dummy); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); 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), &socket_to_send, sizeof(socket_to_send)); if (sendmsg(sockfd, &msg, 0) != sizeof(dummy)) { fprintf(stderr, "Error sending reply: %s\n", strerror(errno)); exit(EXIT_FAILURE); } }; static int try_connect_session(const struct sockaddr_un* addr) { int client_socket = -1; client_socket = socket(AF_UNIX, SOCK_STREAM, 0); if (client_socket < 0) { goto err; } if (connect(client_socket, (struct sockaddr*)addr, sizeof(*addr)) != 0) { goto err; } return client_socket; err: if (client_socket >= 0) { close(client_socket); } return -1; }; static void run_session_manager(unsigned int uid, unsigned int gid) { 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(gid); setuid(uid); // fix up env vars to what they should be pw = getpwuid(uid); 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.shellsession.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); } // unset the global-on-demand flag to kickstart jobs that need to run at load if (!_vproc_set_global_on_demand(false)) { fprintf(stderr, "Failed to unset global-on-demand flag.\n"); exit(EXIT_FAILURE); } // now we just need to stay alive for launchd to keep our session alive // (launchd will start shellspawn as a LaunchAgent, and that's what the `darling` command will actually send commands to) while (true) { pause(); } };