Repository: giampaolo/psutil
Pull request #2543 (2025-04-07 18:00:27)

Make `children()` and `parent()` aware of system clock updates
## Summary

* OS: all
* Bug fix: yes
* Type: core
* Fixes: #2542

## Description
Fixes https://github.com/giampaolo/psutil/issues/2542.


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index aa0a7edf..cece7af6 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -17,6 +17,8 @@ XXXX-XX-XX
 - 2528_, [Linux]: `Process.children()`_ may raise ``PermissionError``. It will
   now raise `AccessDenied`_ instead.
 - 2540_, [macOS]: `boot_time()`_ is off by 45 seconds (C precision issue).
+- 2542_: if system clock is updated `Process.children()`_ and
+  `Process.parent()`_ may not be able to return the right information.
 
 7.0.0
 =====
diff --git a/psutil/__init__.py b/psutil/__init__.py
index 80c5c225..06cbac0a 100644
--- a/psutil/__init__.py
+++ b/psutil/__init__.py
@@ -598,10 +598,13 @@ class Process:
             return None
         ppid = self.ppid()
         if ppid is not None:
-            ctime = self.create_time()
+            # Get a fresh (non-cached) ctime in case the system clock
+            # was updated. TODO: use a monotonic ctime on platforms
+            # where it's supported.
+            proc_ctime = Process(self.pid).create_time()
             try:
                 parent = Process(ppid)
-                if parent.create_time() <= ctime:
+                if parent.create_time() <= proc_ctime:
                     return parent
                 # ...else ppid has been reused by another process
             except NoSuchProcess:
@@ -969,6 +972,10 @@ class Process:
         """
         self._raise_if_pid_reused()
         ppid_map = _ppid_map()
+        # Get a fresh (non-cached) ctime in case the system clock was
+        # updated. TODO: use a monotonic ctime on platforms where it's
+        # supported.
+        proc_ctime = Process(self.pid).create_time()
         ret = []
         if not recursive:
             for pid, ppid in ppid_map.items():
@@ -977,7 +984,7 @@ class Process:
                         child = Process(pid)
                         # if child happens to be older than its parent
                         # (self) it means child's PID has been reused
-                        if self.create_time() <= child.create_time():
+                        if proc_ctime <= child.create_time():
                             ret.append(child)
                     except (NoSuchProcess, ZombieProcess):
                         pass
@@ -1003,7 +1010,7 @@ class Process:
                         child = Process(child_pid)
                         # if child happens to be older than its parent
                         # (self) it means child's PID has been reused
-                        intime = self.create_time() <= child.create_time()
+                        intime = proc_ctime <= child.create_time()
                         if intime:
                             ret.append(child)
                             stack.append(child_pid)


Test patch:
diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py
index eb104ace..7df07e3d 100755
--- a/psutil/tests/test_process.py
+++ b/psutil/tests/test_process.py
@@ -1123,6 +1123,18 @@ class TestProcess(PsutilTestCase):
         lowest_pid = psutil.pids()[0]
         assert psutil.Process(lowest_pid).parent() is None
 
+    def test_parent_mocked_ctime(self):
+        # Make sure we get a fresh copy of the ctime before processing
+        # parent().We make the assumption that the parent pid MUST have
+        # a creation time < than the child. If system clock is updated
+        # this assumption was broken.
+        # https://github.com/giampaolo/psutil/issues/2542
+        p = self.spawn_psproc()
+        p.create_time()  # trigger cache
+        assert p._create_time
+        p._create_time = 1
+        assert p.parent().pid == os.getpid()
+
     def test_parent_multi(self):
         parent = psutil.Process()
         child, grandchild = self.spawn_children_pair()
@@ -1153,6 +1165,30 @@ class TestProcess(PsutilTestCase):
             assert children[0].pid == child.pid
             assert children[0].ppid() == parent.pid
 
+    def test_children_mocked_ctime(self):
+        # Make sure we get a fresh copy of the ctime before processing
+        # children(). We make the assumption that process children MUST
+        # have a creation time > than the parent. If system clock is
+        # updated this assumption was broken.
+        # https://github.com/giampaolo/psutil/issues/2542
+        parent = psutil.Process()
+        parent.create_time()  # trigger cache
+        assert parent._create_time
+        parent._create_time += 100000
+
+        assert not parent.children()
+        assert not parent.children(recursive=True)
+        # On Windows we set the flag to 0 in order to cancel out the
+        # CREATE_NO_WINDOW flag (enabled by default) which creates
+        # an extra "conhost.exe" child.
+        child = self.spawn_psproc(creationflags=0)
+        children1 = parent.children()
+        children2 = parent.children(recursive=True)
+        for children in (children1, children2):
+            assert len(children) == 1
+            assert children[0].pid == child.pid
+            assert children[0].ppid() == parent.pid
+
     def test_children_recursive(self):
         # Test children() against two sub processes, p1 and p2, where
         # p1 (our child) spawned p2 (our grandchild).