|
26 | 26 | ) |
27 | 27 |
|
28 | 28 |
|
| 29 | +@pytest.mark.parametrize('recheck,exited', ((0, True), (0x102, False), |
| 30 | + (0xffffffff, False))) |
| 31 | +@pytest.mark.parametrize('terminate', (False, True)) |
| 32 | +def test_identity_query_race_requires_same_handle_exit(monkeypatch, recheck, exited, |
| 33 | + terminate): |
| 34 | + class Kernel: |
| 35 | + def __init__(self): |
| 36 | + self.waits = [0x102, recheck] |
| 37 | + self.closed = [] |
| 38 | + |
| 39 | + def OpenProcess(self, access, inherit, pid): |
| 40 | + assert access == 0x101000 and not inherit and pid == 17 |
| 41 | + return 123 |
| 42 | + |
| 43 | + def WaitForSingleObject(self, handle, timeout): |
| 44 | + assert handle == 123 and timeout == (0 if len(self.waits) == 2 else 100) |
| 45 | + return self.waits.pop(0) |
| 46 | + |
| 47 | + def CloseHandle(self, handle): |
| 48 | + self.closed.append(handle) |
| 49 | + |
| 50 | + def OpenJobObjectW(self, *args): |
| 51 | + pytest.fail('uncertain identity must not reach job ownership') |
| 52 | + |
| 53 | + kernel = Kernel() |
| 54 | + monkeypatch.setattr(native_session, '_kernel', lambda: kernel) |
| 55 | + |
| 56 | + def unavailable(handle, actual_kernel): |
| 57 | + assert handle == 123 and actual_kernel is kernel |
| 58 | + raise InputError('windows_session_process_identity_unavailable') |
| 59 | + |
| 60 | + monkeypatch.setattr(native_session, '_identity', unavailable) |
| 61 | + identity = {'pid': 17, 'created_filetime': 1, 'image': 'python.exe', |
| 62 | + 'job': 'Local\\owned'} |
| 63 | + if exited: |
| 64 | + assert native_session._owned_process(identity, terminate=terminate) == (False, None, None) |
| 65 | + else: |
| 66 | + with pytest.raises(InputError, match='windows_session_process_identity_unavailable'): |
| 67 | + native_session._owned_process(identity, terminate=terminate) |
| 68 | + assert kernel.waits == [] |
| 69 | + assert kernel.closed == [123] |
| 70 | + |
| 71 | + |
| 72 | +@pytest.mark.skipif(os.name != 'nt', reason='native Windows process handles only') |
| 73 | +def test_real_process_exit_during_image_query_is_rechecked_on_retained_handle(monkeypatch): |
| 74 | + target = subprocess.Popen([sys._base_executable, '-I', '-c', 'import time;time.sleep(30)'], |
| 75 | + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, |
| 76 | + stderr=subprocess.DEVNULL, |
| 77 | + creationflags=subprocess.CREATE_NO_WINDOW) |
| 78 | + kernel = _kernel() |
| 79 | + unrelated = None |
| 80 | + job = None |
| 81 | + try: |
| 82 | + unrelated = subprocess.Popen([sys._base_executable, '-I', '-c', 'import time;time.sleep(30)'], |
| 83 | + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, |
| 84 | + stderr=subprocess.DEVNULL, |
| 85 | + creationflags=subprocess.CREATE_NO_WINDOW) |
| 86 | + job_name = 'Local\\jev-template-race-' + str(uuid.uuid4()) |
| 87 | + job = kernel.CreateJobObjectW(None, job_name) |
| 88 | + assert job and kernel.AssignProcessToJobObject(job, int(target._handle)) |
| 89 | + created, image = _identity(int(target._handle), kernel) |
| 90 | + identity = {'pid': target.pid, 'created_filetime': created, 'image': image, |
| 91 | + 'job': 'unused-before-identity-recheck'} |
| 92 | + |
| 93 | + class ExitDuringQuery: |
| 94 | + def __init__(self): |
| 95 | + self.query_attempted = False |
| 96 | + self.opened = None |
| 97 | + self.closed = [] |
| 98 | + |
| 99 | + def __getattr__(self, name): |
| 100 | + return getattr(kernel, name) |
| 101 | + |
| 102 | + def OpenProcess(self, *args): |
| 103 | + self.opened = kernel.OpenProcess(*args) |
| 104 | + return self.opened |
| 105 | + |
| 106 | + def QueryFullProcessImageNameW(self, *args): |
| 107 | + target.terminate() |
| 108 | + target.wait(timeout=5) |
| 109 | + # Invoke the actual API on the retained Win32 handle after |
| 110 | + # owned process exit. Force a query failure regardless of |
| 111 | + # OS-specific post-exit behavior so the recheck is deterministic. |
| 112 | + kernel.QueryFullProcessImageNameW(*args) |
| 113 | + self.query_attempted = True |
| 114 | + ctypes.set_last_error(5) |
| 115 | + return 0 |
| 116 | + |
| 117 | + def CloseHandle(self, handle): |
| 118 | + self.closed.append(handle) |
| 119 | + return kernel.CloseHandle(handle) |
| 120 | + |
| 121 | + raced = ExitDuringQuery() |
| 122 | + monkeypatch.setattr(native_session, '_kernel', lambda: raced) |
| 123 | + assert native_session._owned_process(identity) == (False, None, None) |
| 124 | + assert raced.query_attempted and raced.opened in raced.closed |
| 125 | + assert unrelated.poll() is None |
| 126 | + finally: |
| 127 | + if job: |
| 128 | + kernel.CloseHandle(job) |
| 129 | + if target.poll() is None: |
| 130 | + target.terminate() |
| 131 | + target.wait(timeout=5) |
| 132 | + if unrelated is not None and unrelated.poll() is None: |
| 133 | + unrelated.terminate() |
| 134 | + unrelated.wait(timeout=5) |
| 135 | + |
| 136 | + |
29 | 137 | @pytest.mark.skipif(os.name != 'nt', reason='native Windows Job Object only') |
30 | 138 | def test_owned_job_stop_does_not_touch_unrelated_process(tmp_path, monkeypatch): |
31 | 139 | owned = create_private_directory(tmp_path / 'session') |
|
0 commit comments