From 890969433f313c2704cbe8fd58130d880e19c991 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Wed, 17 Jun 2026 12:19:03 +0100 Subject: [PATCH] Add test for FP for py/file-not-closed --- .../FileNotAlwaysClosed.expected | 19 ++++++------ .../FileNotAlwaysClosed/resources_test.py | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/FileNotAlwaysClosed.expected b/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/FileNotAlwaysClosed.expected index 7f48feb72eb..6f17eab01aa 100644 --- a/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/FileNotAlwaysClosed.expected +++ b/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/FileNotAlwaysClosed.expected @@ -1,9 +1,10 @@ -| resources_test.py:4:10:4:25 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:5:5:5:33 | ControlFlowNode for Attribute() | this operation | -| resources_test.py:9:10:9:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | -| resources_test.py:108:11:108:20 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | -| resources_test.py:112:11:112:28 | ControlFlowNode for opener_func2() | File may not be closed if $@ raises an exception. | resources_test.py:113:5:113:22 | ControlFlowNode for Attribute() | this operation | -| resources_test.py:123:11:123:24 | ControlFlowNode for opener_func2() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | -| resources_test.py:129:15:129:24 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:130:9:130:26 | ControlFlowNode for Attribute() | this operation | -| resources_test.py:248:11:248:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | -| resources_test.py:269:10:269:27 | ControlFlowNode for Attribute() | File may not be closed if $@ raises an exception. | resources_test.py:271:5:271:19 | ControlFlowNode for Attribute() | this operation | -| resources_test.py:285:11:285:20 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:287:5:287:31 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:6:10:6:25 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:7:5:7:33 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:11:10:11:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | +| resources_test.py:110:11:110:20 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | +| resources_test.py:114:11:114:28 | ControlFlowNode for opener_func2() | File may not be closed if $@ raises an exception. | resources_test.py:115:5:115:22 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:125:11:125:24 | ControlFlowNode for opener_func2() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | +| resources_test.py:131:15:131:24 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:132:9:132:26 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:250:11:250:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | +| resources_test.py:271:10:271:27 | ControlFlowNode for Attribute() | File may not be closed if $@ raises an exception. | resources_test.py:273:5:273:19 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:287:11:287:20 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:289:5:289:31 | ControlFlowNode for Attribute() | this operation | +| resources_test.py:361:13:361:27 | ControlFlowNode for Attribute() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation | diff --git a/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/resources_test.py b/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/resources_test.py index f4bd33eb12c..4f9b067d773 100644 --- a/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/resources_test.py +++ b/python/ql/test/query-tests/Resources/FileNotAlwaysClosed/resources_test.py @@ -1,5 +1,7 @@ #File not always closed +import os + def not_close1(): f1 = open("filename") # $ Alert # not closed on exception f1.write("Error could occur") @@ -332,3 +334,30 @@ def closed32(path): # due to a check that an operation is lexically contained within a `with` block (with `expr.getParent*()`) # not detecting this case. return list(wrap.read()) + + +class FdHolder33(): + # Mirrors CPython's `_pyio.FileIO`: it opens a file descriptor with `os.open`, + # stores it in an instance attribute, and exposes it again via `fileno()`. + def __init__(self, path): + fd = os.open(path, os.O_RDONLY) + self._fd = fd + + def fileno(self): + return self._fd + + def close(self): + os.close(self._fd) + +def closed33(path): + # False positive mirroring CPython's `_pyio.open`. + # `holder.fileno()` merely returns the existing file descriptor; it does not + # open a new resource. With instance-attribute type tracking, the `os.open` + # source flows through `self._fd` and back out of `fileno()`, so the call + # `holder.fileno()` is wrongly treated as a fresh file-open whose result is + # never closed. The descriptor is in fact owned and closed by `holder.close()`. + holder = FdHolder33(path) + try: + n = holder.fileno() # $ SPURIOUS: Alert + finally: + holder.close()