-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathstress_test_threaded.py
More file actions
executable file
·337 lines (281 loc) · 10.6 KB
/
Copy pathstress_test_threaded.py
File metadata and controls
executable file
·337 lines (281 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env python3
"""
Standalone stress test for PAM authentication with concurrent threads.
This script creates 100 concurrent authentication attempts to stress test
the handle validation and thread safety of the python-pam library.
Usage:
python stress_test_threaded.py [--username USERNAME] [--password PASSWORD] [--threads N]
If username/password are not provided, the script will use mock authentication
to avoid requiring real system credentials.
"""
import argparse
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, List, Optional
try:
from pam import authenticate, PamAuthenticator
from pam.__internals import PAM_SUCCESS
except ImportError:
print("Error: Could not import pam module. Make sure python-pam is installed.")
print("Try: poetry install")
sys.exit(1)
class StressTestResult:
"""Container for stress test results."""
def __init__(self):
self.successful = 0
self.failed = 0
self.errors: List[Dict] = []
self.lock = threading.Lock()
self.start_time: Optional[float] = None
self.end_time: Optional[float] = None
def add_success(self):
with self.lock:
self.successful += 1
def add_failure(self, thread_id: int, attempt: int, code: int, reason: str):
with self.lock:
self.failed += 1
self.errors.append({
'thread_id': thread_id,
'attempt': attempt,
'code': code,
'reason': reason
})
def add_error(self, thread_id: int, attempt: int, error: Exception):
with self.lock:
self.failed += 1
self.errors.append({
'thread_id': thread_id,
'attempt': attempt,
'error': str(error),
'type': type(error).__name__
})
def get_duration(self) -> float:
if self.start_time and self.end_time:
return self.end_time - self.start_time
return 0.0
def authenticate_worker(
thread_id: int,
num_attempts: int,
username: str,
password: str,
use_separate_instances: bool,
result: StressTestResult
):
"""Worker function for each thread."""
for attempt in range(num_attempts):
try:
if use_separate_instances:
# Use separate PamAuthenticator instance for each authentication
pam_obj = PamAuthenticator()
auth_result = pam_obj.authenticate(username, password)
code = pam_obj.code
reason = pam_obj.reason
else:
# Use the module authenticate function (fresh instance per call)
auth_result = authenticate(username, password)
# Note: With shared instance, we can't easily get code/reason
code = PAM_SUCCESS if auth_result else -1
reason = "Success" if auth_result else "Failed"
if auth_result:
result.add_success()
else:
result.add_failure(thread_id, attempt, code, str(reason))
except Exception as e:
result.add_error(thread_id, attempt, e)
def run_stress_test(
num_threads: int = 10,
attempts_per_thread: int = 10,
username: Optional[str] = None,
password: Optional[str] = None,
use_separate_instances: bool = True,
use_mock: bool = False
) -> StressTestResult:
"""
Run stress test with concurrent authentication attempts.
Args:
num_threads: Number of concurrent threads
attempts_per_thread: Number of authentication attempts per thread
username: Username to authenticate (if None, uses mock)
password: Password to authenticate (if None, uses mock)
use_separate_instances: If True, each thread uses separate instance
use_mock: If True, use mock authentication (for testing without real PAM)
Returns:
StressTestResult with test results
"""
result = StressTestResult()
# If no credentials provided, use mock mode
if username is None or password is None:
use_mock = True
username = 'test_user'
password = 'test_password'
print("Note: No credentials provided, using mock mode")
print(" (This will test the code path but won't hit real PAM)")
if use_mock:
print("\n⚠️ MOCK MODE: Using mock authentication")
print(" To test with real PAM, provide --username and --password\n")
total_attempts = num_threads * attempts_per_thread
print(f"Starting stress test:")
print(f" Threads: {num_threads}")
print(f" Attempts per thread: {attempts_per_thread}")
print(f" Total attempts: {total_attempts}")
print(f" Mode: {'Separate instances' if use_separate_instances else 'Shared instance'}")
print(f" Username: {username}")
print()
result.start_time = time.time()
# Run concurrent authentications
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [
executor.submit(
authenticate_worker,
i,
attempts_per_thread,
username,
password,
use_separate_instances,
result
)
for i in range(num_threads)
]
# Wait for all threads to complete
for future in as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Unexpected error in thread: {e}")
result.end_time = time.time()
return result
def print_results(result: StressTestResult, total_attempts: int):
"""Print stress test results."""
duration = result.get_duration()
print("\n" + "=" * 70)
print("STRESS TEST RESULTS")
print("=" * 70)
print(f"Total attempts: {total_attempts}")
print(f"Successful: {result.successful} ({100 * result.successful / total_attempts:.1f}%)")
print(f"Failed: {result.failed} ({100 * result.failed / total_attempts:.1f}%)")
print(f"Duration: {duration:.2f} seconds")
print(f"Throughput: {total_attempts / duration:.2f} attempts/second")
print()
if result.errors:
print(f"Errors encountered: {len(result.errors)}")
print("\nFirst 10 errors:")
for i, error in enumerate(result.errors[:10], 1):
if 'error' in error:
print(f" {i}. Thread {error['thread_id']}, Attempt {error['attempt']}: "
f"{error['type']}: {error['error']}")
else:
print(f" {i}. Thread {error['thread_id']}, Attempt {error['attempt']}: "
f"Code {error['code']}, Reason: {error['reason']}")
if len(result.errors) > 10:
print(f" ... and {len(result.errors) - 10} more errors")
print()
# Check for specific error types
ctypes_errors = [
e for e in result.errors
if 'ctypes.ArgumentError' in e.get('error', '') or
'ctypes' in e.get('type', '').lower() or
'ArgumentError' in e.get('error', '')
]
handle_errors = [
e for e in result.errors
if 'handle' in e.get('error', '').lower() or
'handle' in e.get('reason', '').lower()
]
if ctypes_errors:
print("❌ CRITICAL: ctypes.ArgumentError detected!")
print(" This indicates handle validation issues - the bug is present!")
print(f" Count: {len(ctypes_errors)}")
print()
elif handle_errors:
print("⚠️ Handle-related errors detected (but not ctypes.ArgumentError)")
print(f" Count: {len(handle_errors)}")
print()
else:
print("✅ No ctypes.ArgumentError detected - handle validation working correctly!")
print("✅ All handle checks passed - no crashes or ArgumentErrors!")
print()
# Summary
if result.failed == 0:
print("✅ All authentications completed successfully!")
elif result.successful > 0:
print(f"⚠️ Some authentications failed ({result.failed}/{total_attempts})")
else:
print("❌ All authentications failed!")
print("=" * 70)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Stress test PAM authentication with concurrent threads',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Test with 100 concurrent attempts (10 threads × 10 attempts)
python stress_test_threaded.py
# Test with real credentials
python stress_test_threaded.py --username myuser --password mypass
# Custom thread/attempt configuration
python stress_test_threaded.py --threads 20 --attempts 5
# Test via pam.authenticate() (one PamAuthenticator per call)
python stress_test_threaded.py --shared
"""
)
parser.add_argument(
'--threads',
type=int,
default=10,
help='Number of concurrent threads (default: 10)'
)
parser.add_argument(
'--attempts',
type=int,
default=10,
help='Number of authentication attempts per thread (default: 10)'
)
parser.add_argument(
'--username',
type=str,
default=None,
help='Username for authentication (if not provided, uses mock)'
)
parser.add_argument(
'--password',
type=str,
default=None,
help='Password for authentication (if not provided, uses mock)'
)
parser.add_argument(
'--shared',
action='store_true',
help='Use pam.authenticate() instead of an explicit PamAuthenticator per attempt'
)
parser.add_argument(
'--mock',
action='store_true',
help='Force mock mode even if credentials are provided'
)
args = parser.parse_args()
total_attempts = args.threads * args.attempts
print("PAM Authentication Stress Test")
print("=" * 70)
# Run the stress test
result = run_stress_test(
num_threads=args.threads,
attempts_per_thread=args.attempts,
username=args.username,
password=args.password,
use_separate_instances=not args.shared,
use_mock=args.mock
)
# Print results
print_results(result, total_attempts)
# Exit with appropriate code
if result.failed == 0:
sys.exit(0)
elif result.successful > 0:
sys.exit(1) # Partial success
else:
sys.exit(2) # Complete failure
if __name__ == '__main__':
main()