Skip to content

Commit 6329dc9

Browse files
committed
Add PTY support to proc_open (again after 16 long years)
Back in 2004, a feature was added to proc_open which allowed it to open a PTY, connecting specific FDs in the child process to the slave end of the PTY and returning the master end of the PTY (wrapped as a PHP stream) in the `$pipes` array. However, this feature was disabled just about a month later. Little information is available about why this was done, but from talking to the original implementer, it seems there were portability problems with some rare flavors of Unix. Re-enable this feature with a simplified implementation which uses openpty(). No attempt is made to support PTYs if the platform does not have openpty(). The configure script checks if linking with -lutil is necessary to use openpty(), but if anything else is required, like including some special header or linking with some other library, PTY support will be disabled. The original PTY support for proc_open automatically daemonized the child process (disassociating it from the TTY session and process group of the parent). However, I don't think this is a good idea. Just because a user opens a child process in a PTY, it doesn't mean they want it to continue running even when the parent process is killed. Of course, if the child process is some kind of server, it will likely daemonize itself; but we have no reason to preempt that decision. It turns out that since 2015, there has been one test case for PTY support in proc_open() in the test suite. This test was added in GitHub PR #1588 (#1588). That PR mentioned that the PHP binary in the Debian/Ubuntu repositories is patched to *enable* PTY support. Checking the Debian PHP repository (https://salsa.debian.org/php-team/php.git) shows that this is still true. Debian's patch does not modify the implementation from 2004 in any way; it just removes the #if 0 line which disables it. Naturally, the test case is skipped if PTY support is not enabled. This means that ever since it was added, every test run against the 'vanilla' PHP codebase has skipped it. Interestingly, the test case which was added in 2015 fails on my Linux Mint PC... both with this simplified implementation *and* when enabling the original implementation. Investigation reveals the reason: when the child process using the slave end of the PTY exits and its FDs are all closed, and all buffered data is read from the master end of the PTY, any further attempt to read from the master end fails with EIO. The test case seems to expect that reading from the master end will always return an empty string if no data is available. Could this mean that on the platform where this test case was originally developed, the PHP parent process ran *faster* than the child process, so that all the calls to fread() complete before the child process exits and closes its FDs? Or could something else about the platform have been different, such that read() in the parent does not fail with EIO even after the child process exits? Another question: Are the Debian packagers even running this test case every time they build PHP binaries? Intriguing questions, these! There may be another way out of this sticky dilemma: IF at least one FD referring to the slave end of the PTY is kept open *in the parent process*, the failure with EIO will not occur even after the child process exits. This might be a good approach, though we would need a way to ensure the FD will be closed eventually in long-running programs. Perhaps associate it with the proc resource and close it when the proc is freed or explicitly closed with proc_close? And the rabbit hole goes deeper. Although (for now) the test case from 2015 has been updated so it does not attempt to fread() multiple times from the PTY, it still fails intermittently. The reason? When the child process writes "foo\n" to the PTY, the parent sometimes receives "foo" (3 bytes) and sometimes "foo\r\n" (5 bytes). The "\r" is obviously from the TTY line discipline converting "\n" to "\r\n", but why on earth would the parent sometimes read the newline and sometimes not? What is more, strace clearly shows that this is happening *in the kernel*. The child process always executes a single write("foo\n") syscall, but when the blocking read() syscall issued by the parent process returns, sometimes it returns "foo" and sometimes "foo\r\n". This doesn't happen when I extract the test code and run it directly from the PHP CLI, only when the test case is run by run-tests.php. Thanks to Nikita Popov for suggesting that we should just use openpty() rather than grantpt(), unlockpt(), etc.
1 parent 2dc4481 commit 6329dc9

File tree

3 files changed

+58
-87
lines changed

3 files changed

+58
-87
lines changed

configure.ac

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,6 @@ getgrnam_r \
566566
getpwuid_r \
567567
getwd \
568568
glob \
569-
grantpt \
570569
inet_ntoa \
571570
inet_ntop \
572571
inet_pton \
@@ -578,7 +577,6 @@ mmap \
578577
nice \
579578
nl_langinfo \
580579
poll \
581-
ptsname \
582580
putenv \
583581
scandir \
584582
setitimer \
@@ -594,7 +592,6 @@ strptime \
594592
strtok_r \
595593
symlink \
596594
tzset \
597-
unlockpt \
598595
unsetenv \
599596
usleep \
600597
utime \
@@ -713,6 +710,9 @@ if test "$PHP_VALGRIND" != "no"; then
713710
fi
714711
fi
715712

713+
dnl Check for openpty. It may require linking against libutil.
714+
PHP_CHECK_FUNC(openpty, util)
715+
716716
dnl General settings.
717717
dnl ----------------------------------------------------------------------------
718718
PHP_CONFIGURE_PART(General settings)

ext/standard/proc_open.c

Lines changed: 42 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,8 @@
4343
* */
4444
#ifdef PHP_CAN_SUPPORT_PROC_OPEN
4545

46-
#if 0 && HAVE_PTSNAME && HAVE_GRANTPT && HAVE_UNLOCKPT && HAVE_SYS_IOCTL_H && HAVE_TERMIOS_H
47-
# include <sys/ioctl.h>
48-
# include <termios.h>
49-
# define PHP_CAN_DO_PTS 1
46+
#if HAVE_OPENPTY
47+
#include <pty.h>
5048
#endif
5149

5250
#include "proc_open.h"
@@ -592,6 +590,27 @@ static int set_proc_descriptor_to_blackhole(struct php_proc_open_descriptor_item
592590
return SUCCESS;
593591
}
594592

593+
static int set_proc_descriptor_to_pty(struct php_proc_open_descriptor_item *desc, int *master_fd, int *slave_fd)
594+
{
595+
#if HAVE_OPENPTY
596+
if (*master_fd == -1) {
597+
if (openpty(master_fd, slave_fd, NULL, NULL, NULL)) {
598+
php_error_docref(NULL, E_WARNING, "Could not open PTY (pseudoterminal) - %s", strerror(errno));
599+
return FAILURE;
600+
}
601+
}
602+
603+
desc->is_pipe = 1;
604+
desc->childend = dup(*slave_fd);
605+
desc->parentend = dup(*master_fd);
606+
desc->mode_flags = O_RDWR;
607+
return SUCCESS;
608+
#else
609+
php_error_docref(NULL, E_WARNING, "PTY (pseudoterminal) not supported on this system");
610+
return FAILURE;
611+
#endif
612+
}
613+
595614
static int set_proc_descriptor_to_pipe(struct php_proc_open_descriptor_item *desc, zend_string *zmode)
596615
{
597616
php_file_descriptor_t newpipe[2];
@@ -708,9 +727,9 @@ static int redirect_proc_descriptor(struct php_proc_open_descriptor_item *desc,
708727
return dup_proc_descriptor(redirect_to, &desc->childend, nindex);
709728
}
710729

711-
712730
int set_proc_descriptor_from_array(
713-
zval *descitem, struct php_proc_open_descriptor_item *descriptors, int ndesc, int nindex) {
731+
zval *descitem, struct php_proc_open_descriptor_item *descriptors, int ndesc,
732+
int nindex, int *pty_master_fd, int *pty_slave_fd) {
714733
zend_string *ztype = get_string_parameter(descitem, 0, "handle qualifier");
715734
if (!ztype) {
716735
return FAILURE;
@@ -749,32 +768,7 @@ int set_proc_descriptor_from_array(
749768
} else if (zend_string_equals_literal(ztype, "null")) {
750769
retval = set_proc_descriptor_to_blackhole(&descriptors[ndesc]);
751770
} else if (zend_string_equals_literal(ztype, "pty")) {
752-
#if PHP_CAN_DO_PTS
753-
if (dev_ptmx == -1) {
754-
/* open things up */
755-
dev_ptmx = open("/dev/ptmx", O_RDWR);
756-
if (dev_ptmx == -1) {
757-
php_error_docref(NULL, E_WARNING, "Failed to open /dev/ptmx, errno %d", errno);
758-
goto finish;
759-
}
760-
grantpt(dev_ptmx);
761-
unlockpt(dev_ptmx);
762-
slave_pty = open(ptsname(dev_ptmx), O_RDWR);
763-
764-
if (slave_pty == -1) {
765-
php_error_docref(NULL, E_WARNING, "Failed to open slave pty, errno %d", errno);
766-
goto finish;
767-
}
768-
}
769-
descriptors[ndesc].is_pipe = 1;
770-
descriptors[ndesc].childend = dup(slave_pty);
771-
descriptors[ndesc].parentend = dup(dev_ptmx);
772-
descriptors[ndesc].mode_flags = O_RDWR;
773-
retval = SUCCESS;
774-
#else
775-
php_error_docref(NULL, E_WARNING, "PTY pseudo terminal not supported on this system");
776-
goto finish;
777-
#endif
771+
retval = set_proc_descriptor_to_pty(&descriptors[ndesc], pty_master_fd, pty_slave_fd);
778772
} else {
779773
php_error_docref(NULL, E_WARNING, "%s is not a valid descriptor spec/mode", ZSTR_VAL(ztype));
780774
goto finish;
@@ -867,14 +861,10 @@ PHP_FUNCTION(proc_open)
867861
#else
868862
char **argv = NULL;
869863
#endif
864+
int pty_master_fd = -1, pty_slave_fd = -1;
870865
php_process_id_t child;
871866
struct php_process_handle *proc;
872867

873-
#if PHP_CAN_DO_PTS
874-
php_file_descriptor_t dev_ptmx = -1; /* master */
875-
php_file_descriptor_t slave_pty = -1;
876-
#endif
877-
878868
ZEND_PARSE_PARAMETERS_START(3, 6)
879869
Z_PARAM_ZVAL(command_zv)
880870
Z_PARAM_ARRAY(descriptorspec)
@@ -957,7 +947,8 @@ PHP_FUNCTION(proc_open)
957947
goto exit_fail;
958948
}
959949
} else if (Z_TYPE_P(descitem) == IS_ARRAY) {
960-
if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, nindex) == FAILURE) {
950+
if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, nindex,
951+
&pty_master_fd, &pty_slave_fd) == FAILURE) {
961952
goto exit_fail;
962953
}
963954
} else {
@@ -1044,27 +1035,6 @@ PHP_FUNCTION(proc_open)
10441035
if (child == 0) {
10451036
/* this is the child process */
10461037

1047-
#if PHP_CAN_DO_PTS
1048-
if (dev_ptmx >= 0) {
1049-
int my_pid = getpid();
1050-
1051-
#ifdef TIOCNOTTY
1052-
/* detach from original tty. Might only need this if isatty(0) is true */
1053-
ioctl(0,TIOCNOTTY,NULL);
1054-
#else
1055-
setsid();
1056-
#endif
1057-
/* become process group leader */
1058-
setpgid(my_pid, my_pid);
1059-
tcsetpgrp(0, my_pid);
1060-
}
1061-
1062-
if (dev_ptmx >= 0) {
1063-
close(dev_ptmx);
1064-
close(slave_pty);
1065-
}
1066-
#endif
1067-
10681038
if (close_parent_ends_of_pipes_in_child(descriptors, ndesc) == FAILURE) {
10691039
/* We are already in child process and can't do anything to make
10701040
* proc_open() return an error in the parent
@@ -1121,13 +1091,6 @@ PHP_FUNCTION(proc_open)
11211091
#endif
11221092
proc->env = env;
11231093

1124-
#if PHP_CAN_DO_PTS
1125-
if (dev_ptmx >= 0) {
1126-
close(dev_ptmx);
1127-
close(slave_pty);
1128-
}
1129-
#endif
1130-
11311094
/* clean up all the child ends and then open streams on the parent
11321095
* ends, where appropriate */
11331096
for (i = 0; i < ndesc; i++) {
@@ -1191,7 +1154,14 @@ PHP_FUNCTION(proc_open)
11911154
#else
11921155
efree_argv(argv);
11931156
#endif
1194-
1157+
#if HAVE_OPENPTY
1158+
if (pty_master_fd != -1) {
1159+
close(pty_master_fd);
1160+
}
1161+
if (pty_slave_fd != -1) {
1162+
close(pty_slave_fd);
1163+
}
1164+
#endif
11951165
efree(descriptors);
11961166
ZVAL_RES(return_value, zend_register_resource(proc, le_proc_open));
11971167
return;
@@ -1211,12 +1181,12 @@ PHP_FUNCTION(proc_open)
12111181
#else
12121182
efree_argv(argv);
12131183
#endif
1214-
#if PHP_CAN_DO_PTS
1215-
if (dev_ptmx >= 0) {
1216-
close(dev_ptmx);
1184+
#if HAVE_OPENPTY
1185+
if (pty_master_fd != -1) {
1186+
close(pty_master_fd);
12171187
}
1218-
if (slave_pty >= 0) {
1219-
close(slave_pty);
1188+
if (pty_slave_fd != -1) {
1189+
close(pty_slave_fd);
12201190
}
12211191
#endif
12221192
RETURN_FALSE;

ext/standard/tests/file/bug69442.phpt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ EOC;
1717
$output = join("\n", $output);
1818
unlink($tmpFile);
1919

20-
if (strstr($output, "PTY pseudo terminal not supported on this system") !== false) {
20+
if (strstr($output, "PTY (pseudoterminal) not supported on this system") !== false) {
2121
die("skip PTY pseudo terminals are not supported");
2222
}
2323
--FILE--
@@ -28,17 +28,18 @@ $pipes = array();
2828

2929
$process = proc_open($cmd, $descriptors, $pipes);
3030

31-
foreach ($pipes as $type => $pipe) {
32-
$data = fread($pipe, 999);
33-
echo 'type ' . $type . ' ';
34-
var_dump($data);
35-
fclose($pipe);
36-
}
31+
$data0 = fread($pipes[0], 100);
32+
echo 'read from pipe 0: ';
33+
var_dump($data0);
34+
fclose($pipes[0]);
35+
36+
$data3 = fread($pipes[3], 100);
37+
echo 'read from pipe 3: ';
38+
var_dump($data3);
39+
fclose($pipes[3]);
40+
3741
proc_close($process);
3842
--EXPECT--
39-
type 0 string(5) "foo
40-
"
41-
type 1 string(0) ""
42-
type 2 string(0) ""
43-
type 3 string(3) "42
43+
read from pipe 0: string(3) "foo"
44+
read from pipe 3: string(3) "42
4445
"

0 commit comments

Comments
 (0)