From f3fd72a2cdf84b3f425aa2a0641cf596a89bb96f Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Thu, 23 Jul 2026 13:13:54 -0700 Subject: [PATCH 1/8] Validate that process is actually a patching operation before terminating There are times when the previous patching operation will not be running (reboot, etc), where its PID will be reused before the next patching operation starts. When ProcessHandler checks for running previous patching operations, it will identify this new process which is not the patching extension as a process that needs to be terminated. If this process is a required process for the service, it causes that service to effectively crash. This change checks the process's cmdline by reading the pid's cmdline file in the proc filesystem. If it contains the patching extension Core filename, then we can be more confident that the process we are about to end is the correct one. If it does not find the core filename in the cmdline, we can guarantee that the process is not a patching operation, and we will not end that process. --- src/extension/src/ProcessHandler.py | 16 +++++++++++++++- src/extension/tests/Test_ProcessHandler.py | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/extension/src/ProcessHandler.py b/src/extension/src/ProcessHandler.py index b3ea00ca..f051d165 100644 --- a/src/extension/src/ProcessHandler.py +++ b/src/extension/src/ProcessHandler.py @@ -199,9 +199,23 @@ def is_process_running(self, pid): # According to "man 2 kill" possible error values are (EINVAL, EPERM, ESRCH) Thus considering this as an error return False + def is_process_patching_operation(self, pid): + try: + with self.env_layer.file_system.open("/proc/{0}/cmdline".format(str(pid)), mode="r") as cmdline_file: + cmdline = cmdline_file.read() + if Constants.CORE_CODE_FILE_NAME in cmdline: + self.logger.log_debug("Process is a patching operation. [PID={0}]".format(str(pid))) + return True + else: + self.logger.log_debug("Process is not a patching operation. [PID={0}]".format(str(pid))) + return False + except Exception as error: + self.logger.log_debug("Error checking if process is a patching operation. [PID={0}] [Error={1}]".format(str(pid), repr(error))) + return True # If we cannot determine, assume it is a patching operation to be safe + def kill_process(self, pid): try: - if self.is_process_running(pid): + if self.is_process_running(pid) and self.is_process_patching_operation(pid): self.logger.log("Terminating process: [PID={0}]".format(str(pid))) os.kill(pid, signal.SIGTERM) except OSError as error: diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index 99e19250..2e75558c 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -49,6 +49,9 @@ def tearDown(self): def mock_is_process_running_to_return_true(self, pid): return True + def mock_is_process_running_to_return_true(self, pid): + return True + def mock_os_kill_to_raise_exception(self, pid, sig): raise OSError @@ -133,6 +136,8 @@ def test_kill_process(self): # setting mocks is_process_running_backup = ProcessHandler.is_process_running ProcessHandler.is_process_running = self.mock_is_process_running_to_return_true + is_process_patching_operation_backup = ProcessHandler.is_process_patching_operation + ProcessHandler.is_process_patching_operation = self.mock_is_process_patching_operation_to_return_true os_kill_backup = os.kill os.kill = self.mock_os_kill_to_raise_exception @@ -143,6 +148,7 @@ def test_kill_process(self): # reseting mocks ProcessHandler.is_process_running = is_process_running_backup + ProcessHandler.is_process_patching_operation = is_process_patching_operation_backup os.kill = os_kill_backup def test_get_python_cmd(self): From 7ca6ffdaabf02a00fb31039d47d9b45f6792b5c1 Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Thu, 23 Jul 2026 13:55:55 -0700 Subject: [PATCH 2/8] fix test --- src/extension/tests/Test_ProcessHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index 2e75558c..b13ad870 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -49,7 +49,7 @@ def tearDown(self): def mock_is_process_running_to_return_true(self, pid): return True - def mock_is_process_running_to_return_true(self, pid): + def mock_is_process_patching_operation_to_return_true(self, pid): return True def mock_os_kill_to_raise_exception(self, pid, sig): From 0761c2fc7f277462cf427f1059fd5d863c29077b Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 09:20:34 -0700 Subject: [PATCH 3/8] Address feedback --- src/extension/src/ProcessHandler.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/extension/src/ProcessHandler.py b/src/extension/src/ProcessHandler.py index f051d165..71030572 100644 --- a/src/extension/src/ProcessHandler.py +++ b/src/extension/src/ProcessHandler.py @@ -201,13 +201,17 @@ def is_process_running(self, pid): def is_process_patching_operation(self, pid): try: - with self.env_layer.file_system.open("/proc/{0}/cmdline".format(str(pid)), mode="r") as cmdline_file: + # Patching operation cmdline will look like: + # /usr/bin/python3.10 /var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/MsftLinuxPatchCore.py + with self.env_layer.file_system.open("/proc/{0}/cmdline".format(str(pid)), mode="rb") as cmdline_file: cmdline = cmdline_file.read() + cmdline = cmdline.replace(b'\x00', b' ').decode("utf-8") # cmdline is null-separated, so replacing nulls with spaces for easier parsing and logging + if Constants.CORE_CODE_FILE_NAME in cmdline: - self.logger.log_debug("Process is a patching operation. [PID={0}]".format(str(pid))) + self.logger.log_verbose("Process is a patching operation. [PID={0}]".format(str(pid))) return True else: - self.logger.log_debug("Process is not a patching operation. [PID={0}]".format(str(pid))) + self.logger.log_debug("Process is not a patching operation. [PID={0}] [CmdLine={1}]".format(str(pid), cmdline)) return False except Exception as error: self.logger.log_debug("Error checking if process is a patching operation. [PID={0}] [Error={1}]".format(str(pid), repr(error))) From 8a4474e8277888838547ed57e541bd370a8559d4 Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 10:31:33 -0700 Subject: [PATCH 4/8] Add tests for is_process_patching_operation --- src/extension/tests/Test_ProcessHandler.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index b13ad870..0c2a18ae 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -39,6 +39,7 @@ def setUp(self): self.json_file_handler = runtime.json_file_handler self.env_layer = runtime.env_layer dir_path = os.path.join(os.path.pardir, "tests", "helpers") + self.proc_cmdline_path = os.path.join(dir_path, "proc_cmdline") self.ext_output_status_handler = ExtOutputStatusHandler(self.logger, self.utility, self.json_file_handler, dir_path) self.process = subprocess.Popen(["echo", "Hello World!"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -67,6 +68,9 @@ def mock_run_command_output_for_python_not_found(self, cmd, no_output=False, chk def mock_get_python_cmd(self): return "python" + def mock_file_system_open_to_return_proc_cmdline(self, path, mode): + return open(self.proc_cmdline_path, mode=mode) + def mock_run_command_to_set_auto_assess_shell_file_permission(self, cmd, no_output=False, chk_err=False): return 0, "permissions set" @@ -211,6 +215,34 @@ def test_start_daemon(self): subprocess.Popen = subprocess_popen_backup process_handler.env_layer.run_command_output = run_command_output_backup ExtEnvHandler.get_temp_folder = ext_env_handler_get_temp_folder_backup + + def test_is_process_patching_operation(self): + # setting mocks + backup_file_system_open = self.env_layer.file_system.open + self.env_layer.file_system.open = self.mock_file_system_open_to_return_proc_cmdline + + # process is patching operation + is_patching_operation_content = b'/usr/bin/python3.10\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/MsftLinuxPatchCore.py\x00--seqno\x001234\x00--configfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/config/1234/config.json\x00--envfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/env/1234/env.json\x00--outputfile\x00/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-1.6.69/output/1234/output.json' + with open(self.proc_cmdline_path, "wb") as f: + f.write(is_patching_operation_content) + + process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler) + pid = 1234 + + self.assertTrue(process_handler.is_process_patching_operation(pid)) + + # process is not patching operation + is_not_patching_operation_content = b'/usr/bin/python3.10\x00/someother/path/some_other_script.py\x00--somearg\x00somevalue' + with open(self.proc_cmdline_path, "wb") as f: + f.write(is_not_patching_operation_content) + + process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler) + pid = 1234 + + self.assertFalse(process_handler.is_process_patching_operation(pid)) + + # resetting mocks + self.env_layer.file_system.open = backup_file_system_open if __name__ == '__main__': From 278150475b99d92961c043196114e842708e5642 Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 10:34:11 -0700 Subject: [PATCH 5/8] Use temp location --- src/extension/tests/Test_ProcessHandler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index 0c2a18ae..0509d996 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -17,6 +17,7 @@ import os import subprocess import sys +import tempfile import unittest from extension.src.Constants import Constants from extension.src.file_handlers.ExtOutputStatusHandler import ExtOutputStatusHandler @@ -39,7 +40,8 @@ def setUp(self): self.json_file_handler = runtime.json_file_handler self.env_layer = runtime.env_layer dir_path = os.path.join(os.path.pardir, "tests", "helpers") - self.proc_cmdline_path = os.path.join(dir_path, "proc_cmdline") + self.test_dir = tempfile.mkdtemp() + self.proc_cmdline_path = os.path.join(self.test_dir, "proc_cmdline") self.ext_output_status_handler = ExtOutputStatusHandler(self.logger, self.utility, self.json_file_handler, dir_path) self.process = subprocess.Popen(["echo", "Hello World!"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) From defbdf83bbe9c49ac60078e1ad68b34ef7e5cbdf Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 10:59:18 -0700 Subject: [PATCH 6/8] add final test for except path --- src/extension/tests/Test_ProcessHandler.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index 0509d996..c925c1c0 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -73,6 +73,9 @@ def mock_get_python_cmd(self): def mock_file_system_open_to_return_proc_cmdline(self, path, mode): return open(self.proc_cmdline_path, mode=mode) + def mock_file_system_open_raises_exception(self, path, mode): + raise FileNotFoundError + def mock_run_command_to_set_auto_assess_shell_file_permission(self, cmd, no_output=False, chk_err=False): return 0, "permissions set" @@ -243,6 +246,13 @@ def test_is_process_patching_operation(self): self.assertFalse(process_handler.is_process_patching_operation(pid)) + self.env_layer.file_system.open = self.mock_file_system_open_raises_exception + + process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler) + pid = 1234 + + self.assertTrue(process_handler.is_process_patching_operation(pid)) + # resetting mocks self.env_layer.file_system.open = backup_file_system_open From a35d0a152c606b7d220523299b440d8c20c7c62f Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 11:00:43 -0700 Subject: [PATCH 7/8] add comment --- src/extension/tests/Test_ProcessHandler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index c925c1c0..cbb44f76 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -246,6 +246,7 @@ def test_is_process_patching_operation(self): self.assertFalse(process_handler.is_process_patching_operation(pid)) + # process cmdline exception self.env_layer.file_system.open = self.mock_file_system_open_raises_exception process_handler = ProcessHandler(self.logger, self.env_layer, self.ext_output_status_handler) From 211de45d0ec8c9e8e9fd47c8638c9ea1688321e5 Mon Sep 17 00:00:00 2001 From: Michelle McDaniel Date: Fri, 24 Jul 2026 11:09:14 -0700 Subject: [PATCH 8/8] cleanup test files --- src/extension/tests/Test_ProcessHandler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/extension/tests/Test_ProcessHandler.py b/src/extension/tests/Test_ProcessHandler.py index cbb44f76..72283a4b 100644 --- a/src/extension/tests/Test_ProcessHandler.py +++ b/src/extension/tests/Test_ProcessHandler.py @@ -15,6 +15,7 @@ # Requires Python 2.7+ import os +import shutil import subprocess import sys import tempfile @@ -47,6 +48,7 @@ def setUp(self): def tearDown(self): VirtualTerminal().print_lowlight("\n----------------- tear down test runner -----------------") + shutil.rmtree(self.test_dir) self.process.kill() def mock_is_process_running_to_return_true(self, pid):