|
1 | 1 | import contextlib |
| 2 | +import threading |
2 | 3 | from contextlib import contextmanager |
3 | 4 | from copy import deepcopy |
4 | 5 | from functools import lru_cache |
|
13 | 14 | from .util.importlib import import_file_as_module |
14 | 15 |
|
15 | 16 |
|
| 17 | +# Lock to protect transient_settings from concurrent access. |
| 18 | +# On macOS, checkov's ParallelRunner uses ThreadPoolExecutor, |
| 19 | +# so all scanners share the same process and global state. Without this lock, |
| 20 | +# concurrent calls to transient_settings() can corrupt the LRU-cached singletons |
| 21 | +# (get_settings, get_plugins, get_mapping_from_secret_type_to_class), causing |
| 22 | +# the secrets scanner to silently produce 0 findings. |
| 23 | +_settings_lock = threading.Lock() |
| 24 | + |
| 25 | + |
16 | 26 | @lru_cache(maxsize=1) |
17 | 27 | def get_settings() -> 'Settings': |
18 | 28 | """ |
@@ -77,21 +87,33 @@ def default_settings() -> Generator['Settings', None, None]: |
77 | 87 |
|
78 | 88 | @contextmanager |
79 | 89 | def transient_settings(config: Dict[str, Any]) -> Generator['Settings', None, None]: |
80 | | - """Allows the customizability of non-global settings per invocation.""" |
81 | | - original_settings = get_settings().json() |
| 90 | + """Allows the customizability of non-global settings per invocation. |
| 91 | +
|
| 92 | + Protected by _settings_lock to prevent race conditions when |
| 93 | + multiple threads call this concurrently (e.g., IAC + SECRETS scanners |
| 94 | + running in parallel via ThreadPoolExecutor on macOS). |
| 95 | + """ |
| 96 | + with _settings_lock: |
| 97 | + original_settings = get_settings().json() |
82 | 98 |
|
83 | | - cache_bust() |
84 | | - try: |
85 | | - yield configure_settings_from_baseline(config) |
86 | | - finally: |
87 | 99 | cache_bust() |
88 | | - configure_settings_from_baseline(original_settings) |
| 100 | + try: |
| 101 | + yield configure_settings_from_baseline(config) |
| 102 | + finally: |
| 103 | + cache_bust() |
| 104 | + configure_settings_from_baseline(original_settings) |
89 | 105 |
|
90 | 106 |
|
91 | 107 | def cache_bust() -> None: |
92 | 108 | get_plugins.cache_clear() |
93 | 109 |
|
94 | 110 | get_filters.cache_clear() |
| 111 | + |
| 112 | + # BCE-56937: Clear the plugin-type mapping cache to prevent stale mappings |
| 113 | + # built from empty settings during a race window. |
| 114 | + from .core.plugins.util import get_mapping_from_secret_type_to_class |
| 115 | + get_mapping_from_secret_type_to_class.cache_clear() |
| 116 | + |
95 | 117 | for path in get_settings().filters: |
96 | 118 | # Need to also clear the individual caches (e.g. cached regex patterns). |
97 | 119 | parts = urlparse(path) |
|
0 commit comments