From 172b64dcb8b69166e081b7053e872d9f08622650 Mon Sep 17 00:00:00 2001 From: edanhub Date: Thu, 18 Sep 2025 13:47:48 +0200 Subject: [PATCH 1/4] CWE-366: Race Condition within a Thread Signed-off-by: edanhub --- .../CWE-691/CWE-366/README.md | 283 ++++++++++++++++++ .../CWE-691/CWE-366/compliant01.py | 57 ++++ .../CWE-691/CWE-366/example01.py | 32 ++ .../CWE-691/CWE-366/noncompliant01.py | 50 ++++ .../readme.md | 1 + docs/Secure-Coding-Guide-for-Python/readme.md | 4 +- 6 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md create mode 100644 docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/compliant01.py create mode 100644 docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/example01.py create mode 100644 docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/noncompliant01.py diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md new file mode 100644 index 00000000..fe9cda63 --- /dev/null +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md @@ -0,0 +1,283 @@ +# CWE-366: Race Condition within a Thread + +In multithreaded programming, use synchronization mechanisms, such as locks, to avoid race conditions, which occur when multiple threads access shared resources simultaneously and lead to unpredictable results. + +> [!NOTE] +> Prerequisite to understand this page: +> [Intro to multiprocessing and multithreading](../../Intro_to_multiprocessing_and_multithreading/readme.md) + +Before Python 3.10, both `direct_add` and `method_calling_add` were at risk of race conditions. After Python 3.10 changed how eval breaking operations are handled ([GH-18334](https://github.com/python/cpython/pull/18334)), `direct_add` should not require additional locks while `method_calling_add` might give unpredictable results without them. The `example01.py` code example is demonstrating the issue. Its output will differ depending on the version of Python: + +_[example01.py:](example01.py)_ + +```py +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Code Example """ +import dis + + +class Number(): + """ + Example of a class where a method calls another method + """ + amount = 100 + + def direct_add(self): + """Simulating hard work""" + a = 0 + a += self.amount + + def method_calling_add(self): + """Simulating hard work""" + a = 0 + a += self.read_amount() + + def read_amount(self): + """Simulating data fetching""" + return self.amount + + +num = Number() +print("direct_add():") +dis.dis(num.direct_add) +print("method_calling_add():") +dis.dis(num.method_calling_add) + +``` + +When run on Python 3.10.13, output shows that `CALL_METHOD` doesn't appear when calling `direct_add` but it does when `method_calling_add` is called instead: + + __Output of example01.py:__ + +```bash +direct_add(): + 14 0 LOAD_CONST 1 (0) + 2 STORE_FAST 1 (a) + + 15 4 LOAD_FAST 1 (a) + 6 LOAD_FAST 0 (self) + 8 LOAD_ATTR 0 (amount) + 10 INPLACE_ADD + 12 STORE_FAST 1 (a) + 14 LOAD_CONST 2 (None) + 16 RETURN_VALUE +method_calling_add(): + 19 0 LOAD_CONST 1 (0) + 2 STORE_FAST 1 (a) + + 20 4 LOAD_FAST 1 (a) + 6 LOAD_FAST 0 (self) + 8 LOAD_METHOD 0 (read_amount) + 10 CALL_METHOD 0 + 12 INPLACE_ADD + 14 STORE_FAST 1 (a) + 16 LOAD_CONST 2 (None) + 18 RETURN_VALUE +``` + +An update to Python 3.10 has introduced the change that prevents such issues from occurring under specific condition. The [GH-18334](https://github.com/python/cpython/pull/18334) change has made it so that the GIL is released and re-aquired only after specific operations as opposed to a certain number of any of them. These operations, called "eval breaking", can be found in the `Python/ceval.c` file and call CHECK_EVAL_BREAKER() to check if the interpreter should process pending events, such as releasing GIL to switch threads. They don't include inplace operations, such as `INPLACE_ADD` (called when using the `+=` operator) but they do include `CALL_METHOD`. The `dis` library provides a disassembler for analyzing bytecode operations in specific functions [[Python docs 2025 - dis](https://docs.python.org/3/library/dis.html)]. + +While both methods might cause race conditions on older versions of Python, only the latter method is risky since Python 3.10. Since Python 3.11, `CALL_FUNCTION` and `CALL_METHOD` have been replaced by a singular `CALL` operation, which is eval breaking as well. [[Python docs 2025 - dis](https://docs.python.org/3/library/dis.html)]. + +## Non-Compliant Code Example - Unsynchronized Addition/Subtraction + +The `noncompliant01.py` code example modifies the value of `amount` by adding and subtracting numerous times. Each of the arithmetic operations is performed by an independent thread [[Python docs 2025 - launching parallel tasks](https://docs.python.org/3.9/library/concurrent.futures.html)]. The expected value once both threads finish their calculations should be `0`. + +_[noncompliant01.py](noncompliant01.py):_ + +```python +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Non-compliant Code Example """ +import logging +import sys +from threading import Thread + +logging.basicConfig(level=logging.INFO) + + +class Number(): + """ + Multithreading incompatible class missing locks. + Issue only occures with more than 1 million repetitions. + """ + value = 0 + repeats = 1000000 + + def add(self): + """Simulating hard work""" + for _ in range(self.repeats): + logging.debug("Number.add: id=%i int=%s size=%s", id(self.value), self.value, sys.getsizeof(self.value)) + self.value += self.read_amount() + + def remove(self): + """Simulating hard work""" + for _ in range(self.repeats): + self.value -= self.read_amount() + + def read_amount(self): + """ Simulating reading amount from an external source, i.e. a file, a database, etc. """ + return 100 + + +if __name__ == "__main__": + ##################### + # exploiting above code example + ##################### + number = Number() + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + add = Thread(target=number.add) + substract = Thread(target=number.remove) + add.start() + substract.start() + + logging.info('Waiting for threads to finish...') + add.join() + substract.join() + + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + +``` + +Due to a race condition occurring, the value is never what we expect e.g. `0`. In this example it is `-2609100`. + + __Example noncompliant01.py output should show int=0:__ + + ```bash +INFO:root:id=2084074055952 int=0 size=24 +INFO:root:Waiting for threads to finish... +INFO:root:id=2084083567824 int=-2609100 size=28 +``` + +## Compliant Solution - Using a Lock + +This compliant solution uses a lock to ensure atomicity and visibility. It ensure only one thread at a time has access to and can modify `self.value` [[Python docs 2025 - lock](https://docs.python.org/3.9/library/concurrent.futures.html)]: + +_[compliant01.py](compliant01.py):_ + +```python +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Non-compliant Code Example """ +import logging +import sys +import threading +from threading import Thread + +logging.basicConfig(level=logging.INFO) + + +class Number(): + """ + Multithreading compatible class with locks. + """ + value = 0 + repeats = 1000000 + + def __init__(self): + self.lock = threading.Lock() + + def add(self): + """Simulating hard work""" + for _ in range(self.repeats): + logging.debug("Number.add: id=%i int=%s size=%s", id(self.value), self.value, sys.getsizeof(self.value)) + self.lock.acquire() + self.value += self.read_amount() + self.lock.release() + + def remove(self): + """Simulating hard work""" + for _ in range(self.repeats): + self.lock.acquire() + self.value -= self.read_amount() + self.lock.release() + + def read_amount(self): + """ Simulating reading amount from an external source, i.e. a file, a database, etc. """ + return 100 + + +if __name__ == "__main__": + ##################### + # exploiting above code example + ##################### + number = Number() + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + add = Thread(target=number.add) + substract = Thread(target=number.remove) + add.start() + substract.start() + + logging.info('Waiting for threads to finish...') + add.join() + substract.join() + + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + +``` + + __Example compliant01.py output provides the expected output of int=0:__ + + ```bash +INFO:root:id=2799840487696 int=0 size=24 +INFO:root:Waiting for threads to finish... +INFO:root:id=2799840487696 int=0 size=24 +``` + +## Automated Detection + + +
+ + + + + + + + + + + + + + + + + +
ToolVersionCheckerDescription
Bandit1.7.4 on Python 3.10.13Not Available
Flake88-4.0.1 on Python 3.10.13Not Available
+ +## Related Guidelines + + + + + + + + + + + + + + +
MITRE CWEPillar: [CWE-691: Insufficient Control Flow Management]
MITRE CWEBase: [CWE-366: Race Condition within a Thread (4.18)]
SEI CERT Oracle Coding Standard for Java[VNA02-J. Ensure that compound operations on shared variables are atomic]
+ +## Bibliography + + + + + + + + + + + + + + +
[Python docs 2025 - launching parallel tasks]Python Software Foundation. (2024). concurrent.futures — Launching parallel tasks [online]. Available from: https://docs.python.org/3.10/library/concurrent.futures.html, [Accessed 18 September 2025]
[Python docs 2025 - lock]Python Software Foundation. (2024). Lock Objects [online]. Available from: https://docs.python.org/3.10/library/threading.html#lock-objects, [Accessed 18 September 2025]
[Python docs 2025 - dis]Python Software Foundation. (2024). dis — Disassembler for Python bytecode [online]. Available from: https://docs.python.org/3/library/dis.html, [Accessed 18 September 2025]
\ No newline at end of file diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/compliant01.py b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/compliant01.py new file mode 100644 index 00000000..eeedb9d7 --- /dev/null +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/compliant01.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Non-compliant Code Example """ +import logging +import sys +import threading +from threading import Thread + +logging.basicConfig(level=logging.INFO) + + +class Number(): + """ + Multithreading compatible class with locks. + """ + value = 0 + repeats = 1000000 + + def __init__(self): + self.lock = threading.Lock() + + def add(self): + """Simulating hard work""" + for _ in range(self.repeats): + logging.debug("Number.add: id=%i int=%s size=%s", id(self.value), self.value, sys.getsizeof(self.value)) + self.lock.acquire() + self.value += self.read_amount() + self.lock.release() + + def remove(self): + """Simulating hard work""" + for _ in range(self.repeats): + self.lock.acquire() + self.value -= self.read_amount() + self.lock.release() + + def read_amount(self): + """ Simulating reading amount from an external source, i.e. a file, a database, etc. """ + return 100 + + +if __name__ == "__main__": + ##################### + # exploiting above code example + ##################### + number = Number() + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + add = Thread(target=number.add) + substract = Thread(target=number.remove) + add.start() + substract.start() + + logging.info('Waiting for threads to finish...') + add.join() + substract.join() + + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) \ No newline at end of file diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/example01.py b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/example01.py new file mode 100644 index 00000000..258ea868 --- /dev/null +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/example01.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Code Example """ +import dis + + +class Number(): + """ + Example of a class where a method calls another method + """ + amount = 100 + + def direct_add(self): + """Simulating hard work""" + a = 0 + a += self.amount + + def method_calling_add(self): + """Simulating hard work""" + a = 0 + a += self.read_amount() + + def read_amount(self): + """Simulating data fetching""" + return self.amount + + +num = Number() +print("direct_add():") +dis.dis(num.direct_add) +print("method_calling_add():") +dis.dis(num.method_calling_add) diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/noncompliant01.py b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/noncompliant01.py new file mode 100644 index 00000000..a56c6e48 --- /dev/null +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/noncompliant01.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: OpenSSF project contributors +# SPDX-License-Identifier: MIT +""" Non-compliant Code Example """ +import logging +import sys +from threading import Thread + +logging.basicConfig(level=logging.INFO) + + +class Number(): + """ + Multithreading incompatible class missing locks. + Issue only occures with more than 1 million repetitions. + """ + value = 0 + repeats = 1000000 + + def add(self): + """Simulating hard work""" + for _ in range(self.repeats): + logging.debug("Number.add: id=%i int=%s size=%s", id(self.value), self.value, sys.getsizeof(self.value)) + self.value += self.read_amount() + + def remove(self): + """Simulating hard work""" + for _ in range(self.repeats): + self.value -= self.read_amount() + + def read_amount(self): + """ Simulating reading amount from an external source, i.e. a file, a database, etc. """ + return 100 + + +if __name__ == "__main__": + ##################### + # exploiting above code example + ##################### + number = Number() + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) + add = Thread(target=number.add) + substract = Thread(target=number.remove) + add.start() + substract.start() + + logging.info('Waiting for threads to finish...') + add.join() + substract.join() + + logging.info("id=%i int=%s size=%s", id(number.value), number.value, sys.getsizeof(number.value)) diff --git a/docs/Secure-Coding-Guide-for-Python/Intro_to_multiprocessing_and_multithreading/readme.md b/docs/Secure-Coding-Guide-for-Python/Intro_to_multiprocessing_and_multithreading/readme.md index 72eeee4a..e0e721fc 100644 --- a/docs/Secure-Coding-Guide-for-Python/Intro_to_multiprocessing_and_multithreading/readme.md +++ b/docs/Secure-Coding-Guide-for-Python/Intro_to_multiprocessing_and_multithreading/readme.md @@ -7,6 +7,7 @@ This page aims to explain the concepts that could be found in the following rule - [CWE-400: Uncontrolled Resource Consumption](../CWE-664/CWE-400/README.md) - [CWE-392: Missing Report of Error Condition](../CWE-703/CWE-392/README.md) - [CWE-665: Improper Initialization](../CWE-664/CWE-665/README.md) +- [CWE-366: Race Condition within a Thread](../CWE-691/CWE-366/README.md) ## What is Multithreading in Python - Multithreading vs Multiprocessing diff --git a/docs/Secure-Coding-Guide-for-Python/readme.md b/docs/Secure-Coding-Guide-for-Python/readme.md index d28818c3..498ea643 100644 --- a/docs/Secure-Coding-Guide-for-Python/readme.md +++ b/docs/Secure-Coding-Guide-for-Python/readme.md @@ -71,12 +71,13 @@ It is __not production code__ and requires code-style or python best practices t |[CWE-682: Incorrect Calculation](https://cwe.mitre.org/data/definitions/682.html)|Prominent CVE| |:---------------------------------------------------------------------------------------------------------------|:----| |[CWE-191: Integer Underflow (Wrap or Wraparound)](CWE-682/CWE-191/README.md)|| -|[# CWE-1335: Incorrect Bitwise Shift of Integer](CWE-682/CWE-1335/README.md)|| +|[CWE-1335: Incorrect Bitwise Shift of Integer](CWE-682/CWE-1335/README.md)|| |[CWE-1335: Promote readability and compatibility by using mathematical written code with arithmetic operations instead of bit-wise operations](CWE-682/CWE-1335/01/README.md)|| |[CWE-1339: Insufficient Precision or Accuracy of a Real Number](CWE-682/CWE-1339/.) || |[CWE-691: Insufficient Control Flow Management](https://cwe.mitre.org/data/definitions/691.html)|Prominent CVE| |:---------------------------------------------------------------------------------------------------------------|:----| +|[CWE-366: Race Condition within a Thread](CWE-691/CWE-366/README.md)|| |[CWE-362: Concurrent Execution Using Shared Resource with Improper Synchronization ("Race Condition")](CWE-691/CWE-362/README.md)|| |[CWE-617: Reachable Assertion](CWE-691/CWE-617/README.md)|| @@ -85,6 +86,7 @@ It is __not production code__ and requires code-style or python best practices t |[CWE-182: Collapse of Data into Unsafe Value](CWE-693/CWE-182/README.md)|| |[CWE-184: Incomplete List of Disallowed Input](CWE-693/CWE-184/README.md)|| |[CWE-330: Use of Insufficiently Random Values](CWE-693/CWE-330/README.md)|[CVE-2020-7548](https://www.cvedetails.com/cve/CVE-2020-7548),
CVSSv3.1: __9.8__,
EPSS: __0.22__ (12.12.2024)| +|[CWE-472: External Control of Assumed-Immutable Web Parameter](CWE-693/CWE-472/README.md)|| |[CWE-778: Insufficient Logging](CWE-693/CWE-778/README.md)|| |[CWE-798: Use of hardcoded credentials](CWE-693/CWE-798/README.md)|| From 518aa23f1c2da66e2a73f56d2d28d9f31b54650b Mon Sep 17 00:00:00 2001 From: edanhub Date: Thu, 18 Sep 2025 14:00:44 +0200 Subject: [PATCH 2/4] Addressed markdownlint errors --- docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md index fe9cda63..8106a081 100644 --- a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md @@ -280,4 +280,4 @@ INFO:root:id=2799840487696 int=0 size=24 [Python docs 2025 - dis] Python Software Foundation. (2024). dis — Disassembler for Python bytecode [online]. Available from: https://docs.python.org/3/library/dis.html, [Accessed 18 September 2025] - \ No newline at end of file + From c816dec81c13bb1fba99b80c4b9fc5e51424db21 Mon Sep 17 00:00:00 2001 From: edanhub Date: Thu, 18 Sep 2025 14:12:07 +0200 Subject: [PATCH 3/4] Adding signed-off Signed-off-by: edanhub From 07518c808e461954b6a61149d2548fa0de812f06 Mon Sep 17 00:00:00 2001 From: Helge Wehder Date: Thu, 18 Sep 2025 13:33:26 +0100 Subject: [PATCH 4/4] fixed cosmetics and added patch to reference section Signed-off-by: Helge Wehder --- .../CWE-691/CWE-366/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md index 8106a081..aae493f7 100644 --- a/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md +++ b/docs/Secure-Coding-Guide-for-Python/CWE-691/CWE-366/README.md @@ -6,7 +6,7 @@ In multithreaded programming, use synchronization mechanisms, such as locks, to > Prerequisite to understand this page: > [Intro to multiprocessing and multithreading](../../Intro_to_multiprocessing_and_multithreading/readme.md) -Before Python 3.10, both `direct_add` and `method_calling_add` were at risk of race conditions. After Python 3.10 changed how eval breaking operations are handled ([GH-18334](https://github.com/python/cpython/pull/18334)), `direct_add` should not require additional locks while `method_calling_add` might give unpredictable results without them. The `example01.py` code example is demonstrating the issue. Its output will differ depending on the version of Python: +Before Python 3.10, both `direct_add` and `method_calling_add` were at risk of race conditions. After Python 3.10 changed how eval breaking operations are handled [[GH-18334 (2021)](https://github.com/python/cpython/pull/18334)], `direct_add` should not require additional locks while `method_calling_add` might give unpredictable results without them. The `example01.py` code example is demonstrating the issue. Its output will differ depending on the version of Python: _[example01.py:](example01.py)_ @@ -76,7 +76,7 @@ method_calling_add(): 18 RETURN_VALUE ``` -An update to Python 3.10 has introduced the change that prevents such issues from occurring under specific condition. The [GH-18334](https://github.com/python/cpython/pull/18334) change has made it so that the GIL is released and re-aquired only after specific operations as opposed to a certain number of any of them. These operations, called "eval breaking", can be found in the `Python/ceval.c` file and call CHECK_EVAL_BREAKER() to check if the interpreter should process pending events, such as releasing GIL to switch threads. They don't include inplace operations, such as `INPLACE_ADD` (called when using the `+=` operator) but they do include `CALL_METHOD`. The `dis` library provides a disassembler for analyzing bytecode operations in specific functions [[Python docs 2025 - dis](https://docs.python.org/3/library/dis.html)]. +An update to Python 3.10 has introduced the change that prevents such issues from occurring under specific condition. The [[GH-18334 (2021)](https://github.com/python/cpython/pull/18334)] change has made it so that the GIL is released and re-aquired only after specific operations as opposed to a certain number of any of them. These operations, called "eval breaking", can be found in the `Python/ceval.c` file and call `CHECK_EVAL_BREAKER()` to check if the interpreter should process pending events, such as releasing GIL to switch threads. They don't include inplace operations, such as `INPLACE_ADD` (called when using the `+=` operator) but they do include `CALL_METHOD`. The `dis` library provides a disassembler for analyzing bytecode operations in specific functions [[Python docs 2025 - dis](https://docs.python.org/3/library/dis.html)]. While both methods might cause race conditions on older versions of Python, only the latter method is risky since Python 3.10. Since Python 3.11, `CALL_FUNCTION` and `CALL_METHOD` have been replaced by a singular `CALL` operation, which is eval breaking as well. [[Python docs 2025 - dis](https://docs.python.org/3/library/dis.html)]. @@ -280,4 +280,8 @@ INFO:root:id=2799840487696 int=0 size=24 [Python docs 2025 - dis] Python Software Foundation. (2024). dis — Disassembler for Python bytecode [online]. Available from: https://docs.python.org/3/library/dis.html, [Accessed 18 September 2025] + + [GH-18334 (2021)] + GitHub CPython bpo-29988: Only check evalbreaker after calls and on backwards egdes. #18334 [online]. Available from: https://github.com/python/cpython/pull/18334, [Accessed 18 September 2025] +