1 /*
   2  * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /* @test
  25    @bug 4843136 4763384
  26    @summary Various race conditions caused exec'ed processes to have
  27    extra unused file descriptors, which caused hard-to-reproduce hangs.
  28    @author Martin Buchholz
  29 */
  30 
  31 import java.util.Timer;
  32 import java.util.TimerTask;
  33 import java.io.IOException;
  34 
  35 public class SleepyCat {
  36 
  37     private static void destroy (Process[] deathRow) {
  38         for (int i = 0; i < deathRow.length; ++i)
  39             if (deathRow[i] != null)
  40                 deathRow[i].destroy();
  41     }
  42 
  43     static class TimeoutTask extends TimerTask {
  44         private Process[] deathRow;
  45         private boolean timedOut;
  46 
  47         TimeoutTask (Process[] deathRow) {
  48             this.deathRow = deathRow;
  49             this.timedOut = false;
  50         }
  51 
  52         public void run() {
  53             timedOut = true;
  54             destroy(deathRow);
  55         }
  56 
  57         public boolean timedOut() {
  58             return timedOut;
  59         }
  60     }
  61 
  62     private static boolean hang1() throws IOException, InterruptedException {
  63         // Time out was reproducible on Solaris 50% of the time;
  64         // on Linux 80% of the time.
  65         //
  66         // Scenario: After fork(), parent executes and closes write end of child's stdin.
  67         // This causes child to retain a write end of the same pipe.
  68         // Thus the child will never see an EOF on its stdin, and will hang.
  69         Runtime rt = Runtime.getRuntime();
  70         // Increasing the iteration count makes the bug more
  71         // reproducible not only for the obvious reason, but also for
  72         // the subtle reason that it makes reading /proc/getppid()/fd
  73         // slower, making the child more likely to win the race!
  74         int iterations = 20;
  75         int timeout = 30;
  76         String[] catArgs   = new String[] {"/bin/cat"};
  77         String[] sleepArgs = new String[] {"/bin/sleep",
  78                                             String.valueOf(timeout+1)};
  79         Process[] cats   = new Process[iterations];
  80         Process[] sleeps = new Process[iterations];
  81         Timer timer = new Timer(true);
  82         TimeoutTask catExecutioner = new TimeoutTask(cats);
  83         timer.schedule(catExecutioner, timeout * 1000);
  84 
  85         for (int i = 0; i < cats.length; ++i) {
  86             cats[i] = rt.exec(catArgs);
  87             java.io.OutputStream s = cats[i].getOutputStream();
  88             Process sleep = rt.exec(sleepArgs);
  89             s.close(); // race condition here
  90             sleeps[i] = sleep;
  91         }
  92 
  93         for (int i = 0; i < cats.length; ++i)
  94             cats[i].waitFor(); // hangs?
  95 
  96         timer.cancel();
  97 
  98         destroy(sleeps);
  99 
 100         if (catExecutioner.timedOut())
 101             System.out.println("Child process has a hidden writable pipe fd for its stdin.");
 102         return catExecutioner.timedOut();
 103     }
 104 
 105     private static boolean hang2() throws Exception {
 106         // Inspired by the imaginative test case for
 107         // 4850368 (process) getInputStream() attaches to forked background processes (Linux)
 108 
 109         // Time out was reproducible on Linux 80% of the time;
 110         // never on Solaris because of explicit close in Solaris-specific code.
 111 
 112         // Scenario: After fork(), the parent naturally closes the
 113         // child's stdout write end.  The child dup2's the write end
 114         // of its stdout onto fd 1.  On Linux, it fails to explicitly
 115         // close the original fd, and because of the parent's close()
 116         // of the fd, the child retains it.  The child thus ends up
 117         // with two copies of its stdout.  Thus closing one of those
 118         // write fds does not have the desired effect of causing an
 119         // EOF on the parent's read end of that pipe.
 120         Runtime rt = Runtime.getRuntime();
 121         int iterations = 10;
 122         Timer timer = new Timer(true);
 123         int timeout = 30;
 124         Process[] backgroundSleepers = new Process[iterations];
 125         TimeoutTask sleeperExecutioner = new TimeoutTask(backgroundSleepers);
 126         timer.schedule(sleeperExecutioner, timeout * 1000);
 127         byte[] buffer = new byte[10];
 128         String[] args =
 129             new String[] {"/bin/sh", "-c",
 130                           "exec sleep " + (timeout+1) + " >/dev/null"};
 131 
 132         for (int i = 0;
 133              i < backgroundSleepers.length && !sleeperExecutioner.timedOut();
 134              ++i) {
 135             backgroundSleepers[i] = rt.exec(args); // race condition here
 136             try {
 137                 // should get immediate EOF, but might hang
 138                 if (backgroundSleepers[i].getInputStream().read() != -1)
 139                     throw new Exception("Expected EOF, got a byte");
 140             } catch (IOException e) {
 141                 // Stream closed by sleeperExecutioner
 142                 break;
 143             }
 144         }
 145 
 146         timer.cancel();
 147 
 148         destroy(backgroundSleepers);
 149 
 150         if (sleeperExecutioner.timedOut())
 151             System.out.println("Child process has two (should be one) writable pipe fds for its stdout.");
 152         return sleeperExecutioner.timedOut();
 153     }
 154 
 155     public static void main (String[] args) throws Exception {
 156         try {
 157             if (hang1() | hang2())
 158                 throw new Exception("Read from closed pipe hangs");
 159         } catch (IOException e) {
 160             // We will get here on non-Posix systems,
 161             // which don't have cat and sleep and sh.
 162         }
 163     }
 164 }