Repository: rwth-i6/returnn

Issue:
fork+exec should close unrelated file descriptors
From [here](https://stackoverflow.com/questions/72335904/simple-way-to-restart-application), I realized that our fork+exec code (e.g. `ExternSprintDataset`, `SprintSubprocessInstance` for `ExternSprintLoss`, `FastBaumWelchLayer`, `FastBaumWelchLoss`) does not properly closes unrelated file descriptors (fds). We should use `os.closerange` for 3 up to `os.sysconf("SC_OPEN_MAX")` but excluding maybe other used fds (`pipe_c2p`, `pipe_p2c`).



Discussion:

Resolving patch:
diff --git a/returnn/datasets/sprint.py b/returnn/datasets/sprint.py
--- a/returnn/datasets/sprint.py
+++ b/returnn/datasets/sprint.py
@@ -24,7 +24,7 @@
 from .cached2 import CachedDataset2
 from returnn.log import log
 from returnn.util.task_system import Unpickler, numpy_copy_and_set_unused
-from returnn.util.basic import eval_shell_str, interrupt_main, unicode, PY3, BytesIO
+from returnn.util.basic import eval_shell_str, interrupt_main, unicode, PY3, BytesIO, close_all_fds_except
 
 
 class SprintDatasetBase(Dataset):
@@ -725,6 +725,7 @@ def _start_child(self, epoch, get_dim_only=False):
         sys.stdin.close()  # Force no tty stdin.
         self.pipe_c2p[0].close()
         self.pipe_p2c[1].close()
+        close_all_fds_except([0, 1, 2, self.pipe_c2p[1].fileno(), self.pipe_p2c[0].fileno()])
         os.execv(args[0], args)  # Does not return if successful.
         print("%s child exec failed." % self)
       except BaseException:
diff --git a/returnn/sprint/error_signals.py b/returnn/sprint/error_signals.py
--- a/returnn/sprint/error_signals.py
+++ b/returnn/sprint/error_signals.py
@@ -19,7 +19,7 @@
 from threading import RLock, Thread
 import returnn.util.task_system as task_system
 from returnn.util.task_system import Pickler, Unpickler, numpy_set_unused
-from returnn.util.basic import eval_shell_str, make_hashable, BackendEngine
+from returnn.util.basic import eval_shell_str, make_hashable, BackendEngine, close_all_fds_except
 from returnn.log import log
 
 
@@ -137,6 +137,7 @@ def _start_child(self):
         sys.stdin.close()  # Force no tty stdin.
         self.pipe_c2p[0].close()
         self.pipe_p2c[1].close()
+        close_all_fds_except([0, 1, 2, self.pipe_c2p[1].fileno(), self.pipe_p2c[0].fileno()])
         os.execv(args[0], args)  # Does not return if successful.
       except BaseException:
         print("SprintSubprocessInstance: Error when starting Sprint %r." % args, file=log.v1)
diff --git a/returnn/util/basic.py b/returnn/util/basic.py
--- a/returnn/util/basic.py
+++ b/returnn/util/basic.py
@@ -3731,6 +3731,27 @@ def maybe_restart_returnn_with_atfork_patch():
   print("execvpe did not work?")
 
 
+def close_all_fds_except(except_fds):
+  """
+  Calls os.closerange except for the given fds.
+  Code adopted and extended from multiprocessing.util.close_all_fds_except.
+
+  :param typing.Collection[int] except_fds: usually at least {0,1,2}
+  """
+  # noinspection PyBroadException
+  try:
+    max_fd = os.sysconf("SC_OPEN_MAX")
+  except Exception:
+    max_fd = 256
+
+  except_fds = sorted(list(except_fds) + [-1, max_fd])
+  assert except_fds[0] == -1 and except_fds[-1] == max_fd, 'fd invalid'
+
+  for i in range(len(except_fds) - 1):
+    if except_fds[i] + 1 < except_fds[i + 1]:
+      os.closerange(except_fds[i] + 1, except_fds[i + 1])
+
+
 class Stats:
   """
   Collects mean and variance, running average.


Test patch: