this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Emulate LoginWindow behavior in when starting a shell

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, we introduce a new shellsession daemon that forks-off
instances for a user sessions; shellspawn now becomes a LaunchAgent that gets
started by launchd when the user session starts. We then connect to this
instance of the shellspawn daemon.

+505 -11
+4 -2
src/shellspawn/CMakeLists.txt
··· 3 3 add_definitions(-nostdinc) 4 4 5 5 add_darling_executable(shellspawn shellspawn.c duct_signals.c) 6 + add_darling_executable(shellsession shellsession.c) 6 7 7 - install(TARGETS shellspawn DESTINATION libexec/darling/usr/libexec) 8 - install(FILES org.darlinghq.shellspawn.plist DESTINATION libexec/darling/System/Library/LaunchDaemons) 8 + install(TARGETS shellspawn shellsession DESTINATION libexec/darling/usr/libexec) 9 + install(FILES org.darlinghq.shellsession.plist DESTINATION libexec/darling/System/Library/LaunchDaemons) 10 + install(FILES org.darlinghq.shellspawn.plist DESTINATION libexec/darling/System/Library/LaunchAgents) 9 11
+17
src/shellspawn/org.darlinghq.shellsession.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 3 + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 4 + <plist version="1.0"> 5 + <dict> 6 + <key>Label</key> 7 + <string>org.darlinghq.shellsession</string> 8 + <key>ProgramArguments</key> 9 + <array> 10 + <string>/usr/libexec/shellsession</string> 11 + </array> 12 + <key>RunAtLoad</key> 13 + <true/> 14 + <key>KeepAlive</key> 15 + <true/> 16 + </dict> 17 + </plist>
+373
src/shellspawn/shellsession.c
··· 1 + /* 2 + * This file is part of Darling. 3 + * 4 + * Copyright (C) 2023 Darling Developers 5 + * 6 + * Darling is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * Darling is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with Darling. If not, see <http://www.gnu.org/licenses/>. 18 + */ 19 + 20 + #include <stdlib.h> 21 + #include <string.h> 22 + #include <stdio.h> 23 + #include <stdbool.h> 24 + 25 + #include <unistd.h> 26 + #include <sys/socket.h> 27 + #include <sys/un.h> 28 + #include <sys/fcntl.h> 29 + #include <sys/stat.h> 30 + #include <sys/syslimits.h> 31 + #include <errno.h> 32 + #include <pwd.h> 33 + 34 + #include <liblaunch/launch_priv.h> 35 + #include <bootstrap_priv.h> 36 + #include <vproc_priv.h> 37 + 38 + #include "shellsession.h" 39 + #include "shellspawn.h" 40 + 41 + #define MAX_ACCESS_TRIES 10 42 + 43 + static void setup_socket_addr(struct sockaddr_un* addr); 44 + static void setup_session_socket_addr(unsigned int uid, struct sockaddr_un* addr); 45 + static int setup_socket(const struct sockaddr_un* addr); 46 + static void listen_for_connections(int server_socket); 47 + static void spawn_session(int sockfd); 48 + static void send_client_socket(int sockfd, int socket_to_send); 49 + static int try_connect_session(const struct sockaddr_un* addr); 50 + static void run_session_manager(unsigned int uid, unsigned int gid); 51 + 52 + int main(int argc, const char* const* argv) { 53 + struct sockaddr_un server_addr; 54 + int server_socket = -1; 55 + 56 + setup_socket_addr(&server_addr); 57 + 58 + server_socket = setup_socket(&server_addr); 59 + if (server_socket < 0) { 60 + exit(EXIT_FAILURE); 61 + } 62 + 63 + listen_for_connections(server_socket); 64 + 65 + if (server_socket >= 0) { 66 + close(server_socket); 67 + } 68 + 69 + return 0; 70 + }; 71 + 72 + static void setup_socket_addr(struct sockaddr_un* addr) { 73 + memset(addr, 0, sizeof(*addr)); 74 + addr->sun_family = AF_UNIX; 75 + snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", SHELLSESSION_SOCKPATH); 76 + }; 77 + 78 + static void setup_session_socket_addr(unsigned int uid, struct sockaddr_un* addr) { 79 + memset(addr, 0, sizeof(*addr)); 80 + addr->sun_family = AF_UNIX; 81 + snprintf(addr->sun_path, sizeof(addr->sun_path), "%s.%d", SHELLSPAWN_SOCKPATH, uid); 82 + }; 83 + 84 + static int setup_socket(const struct sockaddr_un* addr) { 85 + int server_socket = -1; 86 + 87 + server_socket = socket(AF_UNIX, SOCK_STREAM, 0); 88 + if (server_socket == -1) { 89 + perror("Creating unix socket"); 90 + goto err; 91 + } 92 + 93 + fcntl(server_socket, F_SETFD, FD_CLOEXEC); 94 + unlink(addr->sun_path); 95 + 96 + if (bind(server_socket, (struct sockaddr*)addr, sizeof(*addr)) == -1) { 97 + perror("Binding the unix socket"); 98 + goto err; 99 + } 100 + 101 + chmod(addr->sun_path, S_IRUSR | S_IWUSR); 102 + 103 + if (listen(server_socket, 1) == -1) { 104 + perror("Listening on unix socket"); 105 + goto err; 106 + } 107 + 108 + return server_socket; 109 + 110 + err: 111 + if (server_socket >= 0) { 112 + close(server_socket); 113 + } 114 + return -1; 115 + }; 116 + 117 + static void listen_for_connections(int server_socket) { 118 + int sock; 119 + struct sockaddr_un client_addr; 120 + socklen_t len = sizeof(client_addr); 121 + 122 + while (true) { 123 + sock = accept(server_socket, (struct sockaddr*)&client_addr, &len); 124 + if (sock == -1) { 125 + break; 126 + } 127 + 128 + if (fork() == 0) { 129 + // we don't need the server socket 130 + close(server_socket); 131 + server_socket = -1; 132 + 133 + fcntl(sock, F_SETFD, FD_CLOEXEC); 134 + spawn_session(sock); 135 + 136 + exit(EXIT_SUCCESS); 137 + } else { 138 + close(sock); 139 + } 140 + } 141 + }; 142 + 143 + void spawn_session(int sockfd) { 144 + shellsession_cmd_t cmd; 145 + struct sockaddr_un session_addr; 146 + int client_socket = -1; 147 + char session_started_path[PATH_MAX]; 148 + int started_fd = -1; 149 + 150 + if (read(sockfd, &cmd, sizeof(cmd)) != sizeof(cmd)) { 151 + goto err; 152 + } 153 + 154 + setup_session_socket_addr(cmd.uid, &session_addr); 155 + snprintf(session_started_path, sizeof(session_started_path), "%s.%d", SHELLSESSION_STARTED_PATH, cmd.uid); 156 + 157 + started_fd = open(session_started_path, O_RDONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 158 + 159 + if (started_fd < 0 && errno == EEXIST) { 160 + // we already had a session set up; avoid setting up a new one. 161 + 162 + // wait until shellspawn starts 163 + for (int i = 0; i < MAX_ACCESS_TRIES; i++) { 164 + if (access(session_addr.sun_path, F_OK) == 0) { 165 + break; 166 + } 167 + 168 + sleep(1); 169 + } 170 + 171 + client_socket = try_connect_session(&session_addr); 172 + if (client_socket < 0) { 173 + goto err; 174 + } 175 + 176 + // now reply to the client with the session client socket 177 + send_client_socket(sockfd, client_socket); 178 + close(sockfd); 179 + return; 180 + } else if (started_fd < 0) { 181 + perror("Failed to create session indicator file"); 182 + goto err; 183 + } 184 + 185 + // we didn't have a session set up already, so let's create one. 186 + 187 + // we only need to create the session-running file; we don't need to write anything to it 188 + close(started_fd); 189 + 190 + if (fork() == 0) { 191 + // this is the new session manager process 192 + close(sockfd); 193 + run_session_manager(cmd.uid, cmd.gid); 194 + } else { 195 + // wait until shellspawn starts 196 + for (int i = 0; i < MAX_ACCESS_TRIES; i++) { 197 + if (access(session_addr.sun_path, F_OK) == 0) { 198 + break; 199 + } 200 + 201 + sleep(1); 202 + } 203 + 204 + client_socket = try_connect_session(&session_addr); 205 + if (client_socket < 0) { 206 + fprintf(stderr, "Error connecting to shellspawn session\n"); 207 + exit(EXIT_FAILURE); 208 + } 209 + 210 + // now reply to the client with the session client socket 211 + send_client_socket(sockfd, client_socket); 212 + close(sockfd); 213 + } 214 + 215 + return; 216 + 217 + err: 218 + close(sockfd); 219 + }; 220 + 221 + static void send_client_socket(int sockfd, int socket_to_send) { 222 + char dummy = '\0'; 223 + struct msghdr msg; 224 + char cmsgbuf[CMSG_SPACE(sizeof(int))]; 225 + struct cmsghdr *cmptr; 226 + struct iovec iov; 227 + 228 + memset(&msg, 0, sizeof(msg)); 229 + memset(&iov, 0, sizeof(iov)); 230 + 231 + iov.iov_base = &dummy; 232 + iov.iov_len = sizeof(dummy); 233 + 234 + msg.msg_iov = &iov; 235 + msg.msg_iovlen = 1; 236 + msg.msg_control = cmsgbuf; 237 + msg.msg_controllen = sizeof(cmsgbuf); 238 + 239 + cmptr = CMSG_FIRSTHDR(&msg); 240 + cmptr->cmsg_len = CMSG_LEN(sizeof(int)); 241 + cmptr->cmsg_level = SOL_SOCKET; 242 + cmptr->cmsg_type = SCM_RIGHTS; 243 + memcpy(CMSG_DATA(cmptr), &socket_to_send, sizeof(socket_to_send)); 244 + 245 + if (sendmsg(sockfd, &msg, 0) != sizeof(dummy)) { 246 + fprintf(stderr, "Error sending reply: %s\n", strerror(errno)); 247 + exit(EXIT_FAILURE); 248 + } 249 + }; 250 + 251 + static int try_connect_session(const struct sockaddr_un* addr) { 252 + int client_socket = -1; 253 + 254 + client_socket = socket(AF_UNIX, SOCK_STREAM, 0); 255 + if (client_socket < 0) { 256 + goto err; 257 + } 258 + 259 + if (connect(client_socket, (struct sockaddr*)addr, sizeof(*addr)) != 0) { 260 + goto err; 261 + } 262 + 263 + return client_socket; 264 + 265 + err: 266 + if (client_socket >= 0) { 267 + close(client_socket); 268 + } 269 + return -1; 270 + }; 271 + 272 + static void run_session_manager(unsigned int uid, unsigned int gid) { 273 + const char* login = NULL; 274 + struct passwd* pw = NULL; 275 + char* homedirTmp = NULL; 276 + mach_port_t subset = MACH_PORT_NULL; 277 + mach_port_t dummyService = MACH_PORT_NULL; 278 + kern_return_t kr = KERN_SUCCESS; 279 + 280 + // we are the shellspawn instance for the user's session. this means we're in charge 281 + // of the user's session and are responsible for setting it up as macOS would. 282 + // we are essentially going to be doing the same kind of setup that LoginWindow 283 + // would do on macOS. 284 + 285 + // switch ourselves to run under the user's UID and GID (but under Darling, this is all fake anyways) 286 + setgid(gid); 287 + setuid(uid); 288 + 289 + // fix up env vars to what they should be 290 + pw = getpwuid(uid); 291 + if (pw != NULL) 292 + login = pw->pw_name; 293 + 294 + if (!login) 295 + login = getlogin(); 296 + 297 + setenv("PATH", "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin", 1); 298 + setenv("TMPDIR", "/private/tmp", 1); 299 + 300 + asprintf(&homedirTmp, "HOME=/Users/%s", login); 301 + putenv(homedirTmp); 302 + homedirTmp = NULL; // `putenv()` assumes ownership of the string 303 + 304 + // 305 + // IMPORTANT NOTE 306 + // 307 + // okay, so i have no clue how this whole session switching is supposed to work. 308 + // or rather, i do have *some* idea: we need to call `create_and_switch_to_per_session_launchd` 309 + // to switch to a per-user launchd (which will also trigger LaunchAgents for the user). 310 + // the problem is that the way this function works internally is that it calls `_vprocmgr_move_subset_to_user`. 311 + // this function, in turn, moves the current subset into the new per-user launchd session. 312 + // obviously, this means we must already have a subset prior to calling `create_and_switch_to_per_session_launchd`. 313 + // 314 + // the thing is, i can't see how (the modern version of) LoginWindow does this. maybe the API has changed since then 315 + // 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 316 + // the session setup. but clearly, *we* need to do this, so let's go ahead and do so. 317 + // 318 + 319 + if ((kr = bootstrap_subset(bootstrap_port, mach_task_self(), &subset)) != KERN_SUCCESS) { 320 + fprintf(stderr, "Failed to create bootstrap subset: %d\n", kr); 321 + exit(EXIT_FAILURE); 322 + } 323 + 324 + // replace the bootstrap port with our subset port 325 + if ((kr = task_set_bootstrap_port(mach_task_self(), subset)) != KERN_SUCCESS) { 326 + fprintf(stderr, "Failed to replace bootstrap port: %d\n", kr); 327 + exit(EXIT_FAILURE); 328 + } 329 + 330 + // we no longer need the old bootstrap port 331 + mach_port_deallocate(mach_task_self(), bootstrap_port); 332 + 333 + bootstrap_port = subset; 334 + 335 + // 336 + // ANOTHER IMPORTANT NOTE 337 + // 338 + // launchd requires the new subset to have at least one Mach service registered before switching to the per-user 339 + // session. to that end, we simply create a bogus service here. 340 + // 341 + if ((kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &dummyService)) != KERN_SUCCESS) { 342 + fprintf(stderr, "Failed to allocate dummy service port: %d\n", kr); 343 + exit(EXIT_FAILURE); 344 + } 345 + 346 + if ((kr = mach_port_insert_right(mach_task_self(), dummyService, dummyService, MACH_MSG_TYPE_MAKE_SEND)) != KERN_SUCCESS) { 347 + fprintf(stderr, "Failed to insert send right into dummy service port: %d\n", kr); 348 + exit(EXIT_FAILURE); 349 + } 350 + 351 + if ((kr = bootstrap_register(bootstrap_port, "org.darlinghq.shellsession.dummy-service", dummyService)) != KERN_SUCCESS) { 352 + fprintf(stderr, "Failed to register dummy service: %d\n", kr); 353 + exit(EXIT_FAILURE); 354 + } 355 + 356 + // set up a launchd session 357 + if (create_and_switch_to_per_session_launchd("shellspawn", LAUNCH_GLOBAL_ON_DEMAND) < 0) { 358 + fprintf(stderr, "Failed to set up launchd user session.\n"); 359 + exit(EXIT_FAILURE); 360 + } 361 + 362 + // unset the global-on-demand flag to kickstart jobs that need to run at load 363 + if (!_vproc_set_global_on_demand(false)) { 364 + fprintf(stderr, "Failed to unset global-on-demand flag.\n"); 365 + exit(EXIT_FAILURE); 366 + } 367 + 368 + // now we just need to stay alive for launchd to keep our session alive 369 + // (launchd will start shellspawn as a LaunchAgent, and that's what the `darling` command will actually send commands to) 370 + while (true) { 371 + pause(); 372 + } 373 + };
+37
src/shellspawn/shellsession.h
··· 1 + /* 2 + * This file is part of Darling. 3 + * 4 + * Copyright (C) 2023 Darling Developers 5 + * 6 + * Darling is free software: you can redistribute it and/or modify 7 + * it under the terms of the GNU General Public License as published by 8 + * the Free Software Foundation, either version 3 of the License, or 9 + * (at your option) any later version. 10 + * 11 + * Darling is distributed in the hope that it will be useful, 12 + * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 + * GNU General Public License for more details. 15 + * 16 + * You should have received a copy of the GNU General Public License 17 + * along with Darling. If not, see <http://www.gnu.org/licenses/>. 18 + */ 19 + 20 + #ifndef _SHELLSESSION_H_ 21 + #define _SHELLSESSION_H_ 22 + 23 + #ifdef TESTING 24 + #define SHELLSESSION_SOCKPATH "/tmp/shellsession.sock" 25 + #define SHELLSESSION_STARTED_PATH "/tmp/shellsession.running" 26 + #else 27 + #define SHELLSESSION_SOCKPATH "/var/run/shellsession.sock" 28 + #define SHELLSESSION_STARTED_PATH "/var/run/shellsession.running" 29 + #endif 30 + 31 + typedef struct shellsession_cmd { 32 + unsigned int uid; 33 + unsigned int gid; 34 + } shellsession_cmd_t; 35 + 36 + #endif // _SHELLSESSION_H_ 37 +
+3 -2
src/shellspawn/shellspawn.c
··· 64 64 { 65 65 struct sockaddr_un addr = { 66 66 .sun_family = AF_UNIX, 67 - .sun_path = SHELLSPAWN_SOCKPATH 68 67 }; 68 + 69 + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s.%d", SHELLSPAWN_SOCKPATH, getuid()); 69 70 70 71 g_serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); 71 72 if (g_serverSocket == -1) ··· 75 76 } 76 77 77 78 fcntl(g_serverSocket, F_SETFD, FD_CLOEXEC); 78 - unlink(SHELLSPAWN_SOCKPATH); 79 + unlink(addr.sun_path); 79 80 80 81 if (bind(g_serverSocket, (struct sockaddr*) &addr, sizeof(addr)) == -1) 81 82 {
+71 -7
src/startup/darling.c
··· 37 37 #include <pty.h> 38 38 #include <pwd.h> 39 39 #include "../shellspawn/shellspawn.h" 40 + #include "../shellspawn/shellsession.h" 40 41 #include "darling.h" 41 42 #include "darling-config.h" 42 43 ··· 169 170 { 170 171 char socketPath[4096]; 171 172 172 - snprintf(socketPath, sizeof(socketPath), "%s" SHELLSPAWN_SOCKPATH, prefix); 173 + snprintf(socketPath, sizeof(socketPath), "%s" SHELLSESSION_SOCKPATH, prefix); 173 174 174 175 unlink(socketPath); 175 176 ··· 545 546 return len; 546 547 } 547 548 548 - int connectToShellspawn(void) 549 + int connectToShellsession(void) 549 550 { 550 551 struct sockaddr_un addr; 551 552 int sockfd; 552 553 553 - // Connect to the shellspawn daemon in the container 554 + // Connect to the shellsession daemon in the container 554 555 addr.sun_family = AF_UNIX; 555 556 #if USE_LINUX_4_11_HACK 556 557 addr.sun_path[0] = '\0'; 557 558 558 559 strcpy(addr.sun_path, prefix); 559 - strcat(addr.sun_path, SHELLSPAWN_SOCKPATH); 560 + strcat(addr.sun_path, SHELLSESSION_SOCKPATH); 560 561 #else 561 - snprintf(addr.sun_path, sizeof(addr.sun_path), "%s" SHELLSPAWN_SOCKPATH, prefix); 562 + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s" SHELLSESSION_SOCKPATH, prefix); 562 563 #endif 563 564 564 565 sockfd = socket(AF_UNIX, SOCK_STREAM, 0); ··· 577 578 return sockfd; 578 579 } 579 580 581 + static int startShellspawnSession(int sockfd) 582 + { 583 + shellsession_cmd_t cmd; 584 + 585 + cmd.uid = g_originalGid; 586 + cmd.gid = g_originalGid; 587 + 588 + if (write(sockfd, &cmd, sizeof(cmd)) != sizeof(cmd)) 589 + { 590 + fprintf(stderr, "Error sending command to shellsession: %s\n", strerror(errno)); 591 + exit(EXIT_FAILURE); 592 + } 593 + 594 + char dummy = '\0'; 595 + char cmsgbuf[CMSG_SPACE(sizeof(int))]; 596 + struct msghdr msg; 597 + struct iovec iov; 598 + struct cmsghdr *cmptr; 599 + int sessionFD; 600 + 601 + memset(&msg, 0, sizeof(msg)); 602 + msg.msg_control = cmsgbuf; 603 + msg.msg_controllen = sizeof(cmsgbuf); 604 + 605 + iov.iov_base = &dummy; 606 + iov.iov_len = sizeof(dummy); 607 + msg.msg_iov = &iov; 608 + msg.msg_iovlen = 1; 609 + 610 + if (recvmsg(sockfd, &msg, 0) != sizeof(dummy)) 611 + { 612 + fprintf(stderr, "Error receiving reply from shellsession: %s\n", strerror(errno)); 613 + exit(EXIT_FAILURE); 614 + } 615 + 616 + cmptr = CMSG_FIRSTHDR(&msg); 617 + 618 + if (cmptr == NULL || cmptr->cmsg_level != SOL_SOCKET || cmptr->cmsg_type != SCM_RIGHTS) 619 + { 620 + fprintf(stderr, "Invalid reply from shellsession: no attached FD\n"); 621 + exit(EXIT_FAILURE); 622 + } 623 + 624 + if (cmptr->cmsg_len != CMSG_LEN(sizeof(int))) 625 + { 626 + fprintf(stderr, "Invalid reply from shellsession: invalid CMSG length %lu (expected %lu)\n", cmptr->cmsg_len, sizeof(int)); 627 + exit(EXIT_FAILURE); 628 + } 629 + 630 + memcpy(&sessionFD, CMSG_DATA(cmptr), sizeof(int)); 631 + 632 + if (sessionFD < 0) 633 + { 634 + fprintf(stderr, "Invalid reply from shellsession: invalid FD %d\n", sessionFD); 635 + exit(EXIT_FAILURE); 636 + } 637 + 638 + return sessionFD; 639 + } 640 + 580 641 void setupShellspawnEnv(int sockfd) 581 642 { 582 643 static const char* skip_vars[] = { ··· 702 763 else 703 764 buffer = NULL; 704 765 705 - sockfd = connectToShellspawn(); 766 + sockfd = connectToShellsession(); 767 + sockfd = startShellspawnSession(sockfd); 706 768 707 769 setupShellspawnEnv(sockfd); 708 770 ··· 726 788 int fds[3], master; 727 789 int sockfd; 728 790 729 - sockfd = connectToShellspawn(); 791 + sockfd = connectToShellsession(); 792 + sockfd = startShellspawnSession(sockfd); 793 + 730 794 setupShellspawnEnv(sockfd); 731 795 732 796 pushShellspawnCommand(sockfd, SHELLSPAWN_SETEXEC, binary);