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