jenkins-bot has submitted this change and it was merged.

Change subject: Improve logging for wfShellExec() and ignore missing cgroup
......................................................................


Improve logging for wfShellExec() and ignore missing cgroup

Allow limit.sh to log its errors to an MW debug log channel, by opening
a separate FD for private communication.

Ensure that the log FD is always closed (3>&-) before executing a
subprocess, so that MW will not hang waiting for background processes
to close the log FD. This means using a fixed FD number, since the bash
syntax for closing a file requires a literal FD number.

The "exec" debug channel is now intended for production monitoring. In
addition to errors from limit.sh, it also records when a subprocess is
terminated by a signal.

The case where stream_select() returns false was tested by patching PHP
to inject EINTR or EBADF into errno.

When wfShellExec() is used with a memory cgroup, and the cgroup is missing, log 
and continue with no cgroup instead of immediately exiting.

Bug: 55709
Change-Id: Ie40befe9c0d00c9a0ddb01077df4afb774d17e15
---
M includes/GlobalFunctions.php
M includes/limit.sh
2 files changed, 199 insertions(+), 74 deletions(-)

Approvals:
  Aaron Schulz: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index d8e6b36..de1ebcd 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -2714,9 +2714,9 @@
                        $functions = explode( ',', ini_get( 'disable_functions' 
) );
                        $functions = array_map( 'trim', $functions );
                        $functions = array_map( 'strtolower', $functions );
-                       if ( in_array( 'passthru', $functions ) ) {
-                               wfDebug( "passthru is in disabled_functions\n" 
);
-                               $disabled = 'passthru';
+                       if ( in_array( 'proc_open', $functions ) ) {
+                               wfDebug( "proc_open is in disabled_functions\n" 
);
+                               $disabled = 'disabled';
                        }
                }
        }
@@ -2728,13 +2728,16 @@
  * configuration if supported.
  * @param string $cmd Command line, properly escaped for shell.
  * @param &$retval null|Mixed optional, will receive the program's exit code.
- *                 (non-zero is usually failure)
+ *                 (non-zero is usually failure). If there is an error from
+ *                 read, select, or proc_open(), this will be set to -1.
  * @param array $environ optional environment variables which should be
  *                 added to the executed command environment.
  * @param array $limits optional array with limits(filesize, memory, time, 
walltime)
  *                 this overwrites the global wgShellMax* limits.
- * @param array $options Array of options. Only one is "duplicateStderr" => 
true, which
- *                 Which duplicates stderr to stdout, including errors from 
limit.sh
+ * @param array $options Array of options:
+ *    - duplicateStderr: Set this to true to duplicate stderr to stdout, 
+ *      including errors from limit.sh
+ *      
  * @return string collected stdout as a string
  */
 function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = 
array(), $options = array() ) {
@@ -2746,7 +2749,7 @@
                $retval = 1;
                return $disabled == 'safemode' ?
                        'Unable to run external programs in safe mode.' :
-                       'Unable to run external programs, passthru() is 
disabled.';
+                       'Unable to run external programs, proc_open() is 
disabled.';
        }
 
        $includeStderr = isset( $options['duplicateStderr'] ) && 
$options['duplicateStderr'];
@@ -2772,6 +2775,7 @@
        }
        $cmd = $envcmd . $cmd;
 
+       $useLogPipe = false;
        if ( php_uname( 's' ) == 'Linux' ) {
                $time = intval ( isset( $limits['time'] ) ? $limits['time'] : 
$wgMaxShellTime );
                if ( isset( $limits['walltime'] ) ) {
@@ -2793,8 +2797,10 @@
                                        'MW_CGROUP=' . escapeshellarg( 
$wgShellCgroup ) . '; ' .
                                        "MW_MEM_LIMIT=$mem; " .
                                        "MW_FILE_SIZE_LIMIT=$filesize; " .
-                                       "MW_WALL_CLOCK_LIMIT=$wallTime"
+                                       "MW_WALL_CLOCK_LIMIT=$wallTime; " .
+                                       "MW_USE_LOG_PIPE=yes"
                                );
+                       $useLogPipe = true;
                } elseif ( $includeStderr ) {
                        $cmd .= ' 2>&1';
                }
@@ -2803,19 +2809,126 @@
        }
        wfDebug( "wfShellExec: $cmd\n" );
 
-       // Default to an unusual value that shouldn't happen naturally,
-       // so in the unlikely event of a weird php bug, it would be
-       // more obvious what happened.
-       $retval = 200;
-       ob_start();
-       passthru( $cmd, $retval );
-       $output = ob_get_contents();
-       ob_end_clean();
-
-       if ( $retval == 127 ) {
-               wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" 
);
+       $desc = array(
+               0 => array( 'file', 'php://stdin', 'r' ),
+               1 => array( 'pipe', 'w' ),
+               2 => array( 'file', 'php://stderr', 'w' ) );
+       if ( $useLogPipe ) {
+               $desc[3] = array( 'pipe', 'w' );
        }
-       return $output;
+       $pipes = null;
+       $proc = proc_open( $cmd, $desc, $pipes );
+       if ( !$proc ) {
+               wfDebugLog( 'exec', "proc_open() failed: $cmd\n" );
+               $retval = -1;
+               return '';
+       }
+       $outBuffer = $logBuffer = '';
+       $emptyArray = array();
+       $status = false;
+       $logMsg = false;
+
+       // According to the documentation, it is possible for stream_select()
+       // to fail due to EINTR. I haven't managed to induce this in testing 
+       // despite sending various signals. If it did happen, the error 
+       // message would take the form: 
+       //
+       // stream_select(): unable to select [4]: Interrupted system call 
(max_fd=5)
+       //
+       // where [4] is the value of the macro EINTR and "Interrupted system
+       // call" is string which according to the Linux manual is "possibly"
+       // localised according to LC_MESSAGES.
+       $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
+       $eintrMessage = "stream_select(): unable to select [$eintr]";
+
+       while ( true ) {
+               $status = proc_get_status( $proc );
+               if ( !$status['running'] ) {
+                       break;
+               }
+               $status = false;
+
+               $readyPipes = $pipes;
+
+               // Clear last error
+               @trigger_error( '' );
+               if ( @stream_select( $readyPipes, $emptyArray, $emptyArray, 
null ) === false ) {
+                       $error = error_get_last();
+                       if ( strncmp( $error['message'], $eintrMessage, strlen( 
$eintrMessage ) ) == 0 ) {
+                               continue;
+                       } else {
+                               trigger_error( $error['message'], 
E_USER_WARNING );
+                               $logMsg = $error['message'];
+                               break;
+                       }
+               }
+               foreach ( $readyPipes as $fd => $pipe ) {
+                       $block = fread( $pipe, 65536 );
+                       if ( $block === '' ) {
+                               // End of file
+                               fclose( $pipes[$fd] );
+                               unset( $pipes[$fd] );
+                               if ( !$pipes ) {
+                                       break 2;
+                               }
+                       } elseif ( $block === false ) {
+                               // Read error
+                               $logMsg = "Error reading from pipe";
+                               break 2;
+                       } elseif ( $fd == 1 ) {
+                               // From stdout
+                               $outBuffer .= $block;
+                       } elseif ( $fd == 3 ) {
+                               // From log FD
+                               $logBuffer .= $block;
+                               if ( strpos( $block, "\n" ) !== false ) {
+                                       $lines = explode( "\n", $logBuffer );
+                                       $logBuffer = array_pop( $lines );
+                                       foreach ( $lines as $line ) {
+                                               wfDebugLog( 'exec', $line );
+                                       }
+                               }
+                       }
+               }
+       }
+
+       foreach ( $pipes as $pipe ) {
+               fclose( $pipe );
+       }
+
+       // Use the status previously collected if possible, since 
proc_get_status()
+       // just calls waitpid() which will not return anything useful the 
second time.
+       if ( $status === false ) {
+               $status = proc_get_status( $proc );
+       }
+
+       if ( $logMsg !== false ) {
+               // Read/select error
+               $retval = -1;
+               proc_close( $proc );
+       } elseif ( $status['signaled'] ) {
+               $logMsg = "Exited with signal {$status['termsig']}";
+               $retval = 128 + $status['termsig'];
+               proc_close( $proc );
+       } else {
+               if ( $status['running'] ) {
+                       $retval = proc_close( $proc );
+               } else {
+                       $retval = $status['exitcode'];
+                       proc_close( $proc );
+               }
+               if ( $retval == 127 ) {
+                       $logMsg = "Possibly missing executable file";
+               } elseif ( $retval >= 129 && $retval <= 192 ) {
+                       $logMsg = "Probably exited with signal " . ( $retval - 
128 );
+               }
+       }
+
+       if ( $logMsg !== false ) {
+               wfDebugLog( 'exec', "$logMsg: $cmd\n" );
+       }
+
+       return $outBuffer;
 }
 
 /**
diff --git a/includes/limit.sh b/includes/limit.sh
index 2a1545b..82197b5 100644
--- a/includes/limit.sh
+++ b/includes/limit.sh
@@ -6,58 +6,6 @@
 # and is available on most Linux systems. If Perl was distributed with
 # BSD::Resource included, we would happily use that instead, but it isn't.
 
-MW_INCLUDE_STDERR=
-MW_CPU_LIMIT=0
-MW_CGROUP=
-MW_MEM_LIMIT=0
-MW_FILE_SIZE_LIMIT=0
-MW_WALL_CLOCK_LIMIT=0
-
-# Override settings
-eval "$2"
-
-if [ -n "$MW_INCLUDE_STDERR" ]; then
-       exec 2>&1
-fi
-
-if [ "$MW_CPU_LIMIT" -gt 0 ]; then
-       ulimit -t "$MW_CPU_LIMIT"
-fi
-if [ "$MW_MEM_LIMIT" -gt 0 ]; then
-       if [ -n "$MW_CGROUP" ]; then
-               # Create cgroup
-               if ! mkdir -m 0700 "$MW_CGROUP"/$$; then
-                       echo "limit.sh: failed to create the cgroup." 1>&2
-                       exit 1
-               fi
-               echo $$ > "$MW_CGROUP"/$$/tasks
-               if [ -n "$MW_CGROUP_NOTIFY" ]; then
-                       echo "1" > "$MW_CGROUP"/$$/notify_on_release
-               fi
-               # Memory
-               echo $(($MW_MEM_LIMIT*1024)) > 
"$MW_CGROUP"/$$/memory.limit_in_bytes
-               # Memory+swap
-               echo $(($MW_MEM_LIMIT*1024)) > 
"$MW_CGROUP"/$$/memory.memsw.limit_in_bytes
-       else
-               ulimit -v "$MW_MEM_LIMIT"
-       fi
-else
-       MW_CGROUP=""
-fi
-if [ "$MW_FILE_SIZE_LIMIT" -gt 0 ]; then
-       ulimit -f "$MW_FILE_SIZE_LIMIT"
-fi
-if [ "$MW_WALL_CLOCK_LIMIT" -gt 0 -a -x "/usr/bin/timeout" ]; then
-       /usr/bin/timeout $MW_WALL_CLOCK_LIMIT /bin/bash -c "$1"
-       STATUS="$?"
-       if [ "$STATUS" == 124 ]; then
-               echo "limit.sh: timed out." 1>&2
-       fi
-else
-       eval "$1"
-       STATUS="$?"
-fi
-
 # Clean up cgroup
 cleanup() {
        # First we have to move the current task into a "garbage" group, 
otherwise
@@ -66,7 +14,7 @@
        if [ -w "$MW_CGROUP"/tasks ]; then
                GARBAGE="$MW_CGROUP"
        else
-               GARBAGE="$MW_CGROUP"/garbage-"$USER"
+               GARBAGE="$MW_CGROUP"/garbage-`id -un`
                if [ ! -e "$GARBAGE" ]; then
                        mkdir -m 0700 "$GARBAGE"
                fi
@@ -85,6 +33,70 @@
        NUM_TASKS=${#MAPFILE[*]}
 }
 
+log() {
+       echo limit.sh: "$*" >&3
+       echo limit.sh: "$*" >&2
+}
+
+MW_INCLUDE_STDERR=
+MW_USE_LOG_PIPE=
+MW_CPU_LIMIT=0
+MW_CGROUP=
+MW_MEM_LIMIT=0
+MW_FILE_SIZE_LIMIT=0
+MW_WALL_CLOCK_LIMIT=0
+
+# Override settings
+eval "$2"
+
+if [ -n "$MW_INCLUDE_STDERR" ]; then
+       exec 2>&1
+fi
+if [ -z "$MW_USE_LOG_PIPE" ]; then
+       # Open a dummy log FD
+       exec 3>/dev/null
+fi
+
+if [ "$MW_CPU_LIMIT" -gt 0 ]; then
+       ulimit -t "$MW_CPU_LIMIT"
+fi
+if [ "$MW_MEM_LIMIT" -gt 0 ]; then
+       if [ -n "$MW_CGROUP" ]; then
+               # Create cgroup
+               if ! mkdir -m 0700 "$MW_CGROUP"/$$; then
+                       log "failed to create the cgroup."
+                       MW_CGROUP=""
+               fi
+       fi
+       if [ -n "$MW_CGROUP" ]; then
+               echo $$ > "$MW_CGROUP"/$$/tasks
+               if [ -n "$MW_CGROUP_NOTIFY" ]; then
+                       echo "1" > "$MW_CGROUP"/$$/notify_on_release
+               fi
+               # Memory
+               echo $(($MW_MEM_LIMIT*1024)) > 
"$MW_CGROUP"/$$/memory.limit_in_bytes
+               # Memory+swap
+               echo $(($MW_MEM_LIMIT*1024)) > 
"$MW_CGROUP"/$$/memory.memsw.limit_in_bytes
+       else
+               ulimit -v "$MW_MEM_LIMIT"
+       fi
+else
+       MW_CGROUP=""
+fi
+if [ "$MW_FILE_SIZE_LIMIT" -gt 0 ]; then
+       ulimit -f "$MW_FILE_SIZE_LIMIT"
+fi
+if [ "$MW_WALL_CLOCK_LIMIT" -gt 0 -a -x "/usr/bin/timeout" ]; then
+       /usr/bin/timeout $MW_WALL_CLOCK_LIMIT /bin/bash -c "$1" 3>&-
+       STATUS="$?"
+       if [ "$STATUS" == 124 ]; then
+               log "timed out executing command \"$1\""
+       fi
+else
+       eval "$1" 3>&-
+       STATUS="$?"
+fi
+
 if [ -n "$MW_CGROUP" ]; then
        updateTaskCount
 
@@ -97,7 +109,7 @@
                                updateTaskCount
                        done
                        cleanup
-               ) >&/dev/null < /dev/null &
+               ) >&/dev/null < /dev/null 3>&- &
                disown -a
        else
                cleanup

-- 
To view, visit https://gerrit.wikimedia.org/r/91115
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie40befe9c0d00c9a0ddb01077df4afb774d17e15
Gerrit-PatchSet: 3
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Tim Starling <[email protected]>
Gerrit-Reviewer: Aaron Schulz <[email protected]>
Gerrit-Reviewer: Faidon Liambotis <[email protected]>
Gerrit-Reviewer: Parent5446 <[email protected]>
Gerrit-Reviewer: PleaseStand <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to