/* * Copyright (c) 2015-2018 Apple Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ #include <unistd.h> #include <sys/ioctl.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <util.h> #include <syslog.h> #include <termios.h> #include <errno.h> #include <fcntl.h> #include <sys/socket.h> #include <sys/stat.h> #include <darwintest.h> #include <darwintest_utils.h> #include <darwintest_multiprocess.h> #define TEST_TIMEOUT 10 /* * Receiving SIGTTIN (from the blocked read) is the passing condition, we just * catch it so that we don't get terminated when we receive this. */ void handle_sigttin(int signal) { return; } /* * Because of the way dt_fork_helpers work, we have to ensure any children * created by this function calls exit instead of getting the fork handlers exit * handling */ int get_new_session_and_terminal_and_fork_child_to_read(char *pty_name) { int sock_fd[2]; int pty_fd; pid_t pid; char buf[10]; /* * We use this to handshake certain actions between this process and its * child. */ T_ASSERT_POSIX_SUCCESS(socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fd), NULL); /* * New session, lose any existing controlling terminal and become * session leader. */ T_ASSERT_POSIX_SUCCESS(setsid(), NULL); /* now open pty, become controlling terminal of new session */ T_ASSERT_POSIX_SUCCESS(pty_fd = open(pty_name, O_RDWR), NULL); T_ASSERT_POSIX_SUCCESS(pid = fork(), NULL); if (pid == 0) { /* child */ int pty_fd_child; char buf[10]; T_ASSERT_POSIX_SUCCESS(close(sock_fd[0]), NULL); T_ASSERT_POSIX_SUCCESS(close(pty_fd), NULL); /* Make a new process group for ourselves */ T_ASSERT_POSIX_SUCCESS(setpgid(0, 0), NULL); T_ASSERT_POSIX_SUCCESS(pty_fd_child = open(pty_name, O_RDWR), NULL); /* now let parent know we've done open and setpgid */ write(sock_fd[1], "done", sizeof("done")); /* wait for parent to set us to the foreground process group */ read(sock_fd[1], buf, sizeof(buf)); /* * We are the foreground process group now so we can read * without getting a SIGTTIN. * * Once we are blocked though (we have a crude 1 second sleep on * the parent to "detect" this), our parent is going to change * us to be in the background. * * We'll be blocked until we get a signal and if that is signal * is SIGTTIN, then the test has passed otherwise the test has * failed. */ signal(SIGTTIN, handle_sigttin); (void)read(pty_fd_child, buf, sizeof(buf)); /* * If we get here, we passed, if we get any other signal than * SIGTTIN, we will not reach here. */ exit(0); } T_ASSERT_POSIX_SUCCESS(close(sock_fd[1]), NULL); /* wait for child to open slave side and set its pgid to its pid */ T_ASSERT_POSIX_SUCCESS(read(sock_fd[0], buf, sizeof(buf)), NULL); /* * We need this to happen and in the order shown * * parent (pgid = pid) child (child_pgid = child_pid) * * 1 - tcsetpgrp(child_pgid) * 2 - block in read() * 3 - tcsetpgrp(pgid) * * making sure 2 happens after 1 is easy, we use a sleep(1) in the * parent to try and ensure 3 happens after 2. */ T_ASSERT_POSIX_SUCCESS(tcsetpgrp(pty_fd, pid), NULL); /* let child know you have set it to be the foreground process group */ T_ASSERT_POSIX_SUCCESS(write(sock_fd[0], "done", sizeof("done")), NULL); /* * give it a second to do the read of the terminal in response. * * XXX : Find a way to detect that the child is blocked in read(2). */ sleep(1); /* * now change the foreground process group to ourselves - * Note we are now in the background process group and we need to ignore * SIGTTOU for this call to succeed. * * Hopefully the child has gotten to run and blocked for read on the * terminal in the 1 second we slept. */ signal(SIGTTOU, SIG_IGN); T_ASSERT_POSIX_SUCCESS(tcsetpgrp(pty_fd, getpid()), NULL); return 0; } /* * We're running in a "fork helper", we can't do a waitpid on the child because * the fork helper unhelpfully hides the pid of the child and in it kills itself. * We will instead fork first and wait on the child. If it is * able to emerge from the read of the terminal, the test passes and if it * doesn't, the test fails. * Since the test is testing for a deadlock in proc_exit of the child (caused * by a background read in the "grandchild". */ void run_test(int do_revoke) { int master_fd; char *slave_pty; pid_t pid; T_WITH_ERRNO; T_QUIET; T_SETUPBEGIN; slave_pty = NULL; T_ASSERT_POSIX_SUCCESS(master_fd = posix_openpt(O_RDWR | O_NOCTTY), NULL); (void)fcntl(master_fd, F_SETFL, O_NONBLOCK); T_ASSERT_POSIX_SUCCESS(grantpt(master_fd), NULL); T_ASSERT_POSIX_SUCCESS(unlockpt(master_fd), NULL); slave_pty = ptsname(master_fd); T_ASSERT_NOTNULL(slave_pty, NULL); T_LOG("slave pty is %s\n", slave_pty); T_SETUPEND; /* * We get the stdin and stdout redirection but we don't have visibility * into the child (nor can we wait for it). To get around that, we fork * and only let the parent to the caller and the child exits before * returning to the caller. */ T_ASSERT_POSIX_SUCCESS(pid = fork(), NULL); if (pid == 0) { /* child */ T_ASSERT_POSIX_SUCCESS(close(master_fd), NULL); get_new_session_and_terminal_and_fork_child_to_read(slave_pty); /* * These tests are for testing revoke and read hangs. This * revoke can be explicit by a revoke(2) system call (test 2) * or as part of exit(2) of the session leader (test 1). * The exit hang is the common hang and can be fixed * independently but fixing the revoke(2) hang requires us make * changes in the tcsetpgrp path ( which also fixes the exit * hang). In essence, we have 2 fixes. One which only addresses * the exit hang and one which fixes both. */ if (do_revoke) { /* This should not hang for the test to pass .. */ T_ASSERT_POSIX_SUCCESS(revoke(slave_pty), NULL); } /* * This child has the same dt_helper variables as its parent * The way dt_fork_helpers work if we don't exit() from here, * we will be killing the parent. So we have to exit() and not * let the dt_fork_helpers continue. * If we didn't do the revoke(2), This test passes if this exit * doesn't hang waiting for its child to finish reading. */ exit(0); } int status; int sig; dt_waitpid(pid, &status, &sig, 0); if (sig) { T_FAIL("Test failed because child received signal %s\n", strsignal(sig)); } else if (status) { T_FAIL("Test failed because child exited with status %d\n", status); } else { T_PASS("test_passed\n"); } /* * we can let this process proceed with the regular darwintest process * termination and cleanup. */ } /*************************** TEST 1 ********************************/ T_HELPER_DECL(create_new_session_and_exit, "create_new_session_and_exit") { run_test(0); } T_DECL(tty_exit_bgread_hang_test, "test for background read hang on ttys with proc exit") { dt_helper_t helpers[1]; helpers[0] = dt_fork_helper("create_new_session_and_exit"); dt_run_helpers(helpers, 1, TEST_TIMEOUT); } /*********************** END TEST 1 ********************************/ /************************** TEST 2 ***********************************/ T_HELPER_DECL(create_new_session_and_revoke_terminal, "create_new_session_and_revoke_terminal") { run_test(1); } T_DECL(tty_revoke_bgread_hang_test, "test for background read hang on ttys with revoke") { dt_helper_t helpers[1]; helpers[0] = dt_fork_helper("create_new_session_and_revoke_terminal"); dt_run_helpers(helpers, 1, TEST_TIMEOUT); } /*********************** END TEST 2 *********************************/