@@ -1005,3 +1005,47 @@ def test_zombies_reaped():
10051005 while ps_observes_pid (pid ):
10061006 tries += 1
10071007 assert tries < 100 , f"child #{ i } (PID { pid } ) never went away?"
1008+
1009+
1010+ def test_pipe_inheritance ():
1011+ """Spawn 300 child processes: 100 writers, 100 readers, and 100 waiters.
1012+ The goal is to see whether any of the waiters inherits a write end of one
1013+ of the pipes. If so, it will deadlock a reader and deadlock this test."""
1014+ waiters = []
1015+ threads = []
1016+
1017+ def reader_writer_fn ():
1018+ # Open a pipe for the child to read from. This can deadlock if a waiter
1019+ # inherits this pipe and keeps it open.
1020+ reader_fd , writer_fd = os .pipe ()
1021+ reader = os .fdopen (reader_fd , "rb" )
1022+ writer = os .fdopen (writer_fd , "wb" )
1023+ writer_child = echo_cmd ("foo" ).stdout_file (writer ).start ()
1024+ writer .close ()
1025+ output = cat_cmd ().stdin_file (reader ).read ()
1026+ reader .close ()
1027+ writer_child .wait ()
1028+ assert output == "foo"
1029+
1030+ def waiter_fn ():
1031+ # Spawn a child that won't exit until we kill it. This is intended to
1032+ # keep pipes open if they're inherited unintentionally. Without
1033+ # something like this, we'd need an inheritance *cycle* between
1034+ # readers, which might be unlikely.
1035+ waiters .append (sleep_cmd (365 * 24 * 60 * 60 ).unchecked ().start ())
1036+
1037+ for _ in range (100 ):
1038+ thread1 = threading .Thread (target = reader_writer_fn )
1039+ thread1 .start ()
1040+ threads .append (thread1 )
1041+
1042+ thread2 = threading .Thread (target = waiter_fn )
1043+ thread2 .start ()
1044+ threads .append (thread2 )
1045+
1046+ for thread in threads :
1047+ thread .join ()
1048+
1049+ for waiter in waiters :
1050+ waiter .kill ()
1051+ waiter .wait ()
0 commit comments