classification
Title: [patch] Fix file object leak in `aifc.open` when given invalid AIFF file.
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.7, Python 3.6, Python 3.5, Python 2.7
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: inada.naoki Nosy List: azhang, inada.naoki, serhiy.storchaka
Priority: normal Keywords: patch

Created on 2016-12-30 00:55 by azhang, last changed 2017-03-31 16:36 by dstufft. This issue is now closed.

Files
File name Uploaded Description Edit
fix_aifc_leak.patch azhang, 2016-12-30 00:55 Fix for first issue described in issue. review
fix_aifc_leak_and_file_object_close.patch azhang, 2016-12-30 00:56 Fix for second issue described in issue. review
Pull Requests
URL Status Linked Edit
PR 162 closed azhang, 2017-02-18 23:42
PR 293 merged inada.naoki, 2017-02-25 18:21
PR 310 merged inada.naoki, 2017-02-26 12:42
PR 311 merged inada.naoki, 2017-02-26 12:43
PR 356 merged inada.naoki, 2017-02-28 10:50
PR 703 larry, 2017-03-17 21:00
PR 552 closed dstufft, 2017-03-31 16:36
Messages (10)
msg284299 - (view) Author: Anthony Zhang (azhang) * Date: 2016-12-30 00:55
Summary
-------

This shows up as two closely-related issues:

* ``aifc.open`` leaks file object when invalid AIFF file encountered. This is probably a bug.
* ``aifc.close`` closes file object even when it didn't open the file object to begin with. While this is technically documented behaviour [1], it's inconsistent with how similar modules like ``wave`` work [2].

I have confirmed the issue is present in all the selected Python versions.

Steps to reproduce
------------------

For the first issue, run this code:

    #!/usr/bin/env python3

    # to simplify this example, use this script itself as a random non-AIFF file (though any non-AIFF file will work fine)
    FILE = __file__

    # print warnings on stderr
    import warnings
    warnings.resetwarnings()

    # try to open the non-AIFF file as an AIFF file, which should fail, but shouldn't give any ResourceWarnings
    import aifc
    try: aifc.open(FILE, "rb")
    except: pass

For the second issue, run this code:

    #!/usr/bin/env python3

    from os import path
    FILE = path.expanduser("~/cpython/Lib/test/audiodata/pluck-pcm8.aiff")

    # open the file as a file-like object, then open it again with ``aifc.open``
    import aifc
    with open(FILE, "rb") as f:
        assert not f.closed, "Before opening with AIFC"
        with aifc.open(f, "rb"):
            pass
        assert not f.closed, "After opening with AIFC"

Expected result
---------------

For the first code sample, code should give no output - ``aifc.open`` should throw an exception due to the passed filename being an invalid AIFF file, but that exception should be caught and suppressed. by the ``try``/``except`` statements.

For the second code sample, all assertions should pass - the file should be opened with ``open``, "opened" again with ``aifc.open``, and should not be closed when the inner context manager exits.

Actual result
-------------

For the first code sample:

    $ python3 test.py
    /home/anthony/Desktop/test.py:13: ResourceWarning: unclosed file <_io.BufferedReader name='/home/anthony/Desktop/test.py'>
      except: pass

In other words, code executes as described in "Expected result", but also prints a warning to stderr about the file object not being closed.

For the second code sample:

    $ python3 "/home/anthony/Desktop/test.py"
    Traceback (most recent call last):
      File "/home/anthony/Desktop/test.py", line 11, in <module>
        assert not f.closed, "After opening with AIFC"
    AssertionError: After opening with AIFC

In other words, code runs normally until the inner context manager exits, and the file object gets closed, even though the file object wasn't opened by ``aifc``.

Solution
--------

Attached are patches that fix each issue separately - the first patch fixes the first mentioned issue, while the second patch fixes both at once.

Backwards compatibility:

* The first patch, "fix_aifc_leak.patch", makes no functionality changes, so there shouldn't be any backwards compatibility concerns.
* The second patch, "fix_aifc_leak_and_file_object_close.patch", slightly changes the behaviour of `aifc_instance.close()` so that it closes in only a subset of cases it would originally. While it is possible for this to lead to leaks in certain cases (example below), the impact shoul be low, as existing codebases seem to use context managers and similar constructs that clean everything up properly.

    #!/usr/bin/env python3

    from os import path
    FILE = path.expanduser("~/cpython/Lib/test/audiodata/pluck-pcm8.aiff")
    import aifc
    f = open(FILE, "rb")
    with aifc.open(f, "rb"):
        pass
    # with the patch applied, `f` is not closed at this point anymore

[1]: https://docs.python.org/3/library/aifc.html#aifc.aifc.close
[2]: https://docs.python.org/3/library/wave.html#wave.Wave_read.close
msg284371 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2016-12-31 07:12
Thank you for your patches Anthony. Could you please create a unittest for the first issue?

The second issue is well known. Since the behavior is documented, I think it can be changed only in Python 3.7. The patch needs the "versionchanged" directives in the documentation and documenting the change in What's New (in the section "Porting to Python 3.7" or like). This change can cause reference leaks in existing code.

This is not the only inconsistency between aifc and similar modules.
msg288340 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-02-22 07:23
New changeset 03f68b60e17b57f6f13729ff73245dbb37b30a4c by INADA Naoki in branch 'master':
bpo-29110: Fix file object leak in `aifc.open` when given invalid AIFF file. (GH-162)
https://github.com/python/cpython/commit/03f68b60e17b57f6f13729ff73245dbb37b30a4c
msg288611 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-02-26 19:50
Anyone mind Python 2.7?

Since Python 2.7 doesn't have mock and check_no_resource_warning context manager, I can't cherry-pick easily.

Backporting is bother to me, and the issue doesn't seem so critical.
Can I just close this issue?
msg288622 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2017-02-27 08:11
test.support.swap_attr() provides the functionality similar to mock. check_no_resource_warning() can be backported to 2.7.

But if there are problems with testing in 2.7 you can backport without tests.
msg288705 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-02-28 11:14
OK, since Aifc_write is harder to test, and the exception is very unlikely happens, I fixed only Aifc_read.
msg290358 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-03-24 23:34
New changeset 02eb4b0bd4a7d86cf5e40567aaa8710b00e079a4 by INADA Naoki in branch '2.7':
bpo-29110: Fix file object leak in aifc.open (GH-356)
https://github.com/python/cpython/commit/02eb4b0bd4a7d86cf5e40567aaa8710b00e079a4
msg290375 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-03-24 23:39
New changeset c9131b61fa060a51ec181053cade9f0a7ee91e4f by INADA Naoki in branch '3.6':
[3.6] bpo-29110: Fix file object leak in `aifc.open` (#310)
https://github.com/python/cpython/commit/c9131b61fa060a51ec181053cade9f0a7ee91e4f
msg290376 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-03-24 23:39
New changeset b7fb1e25c89a9eb85b95027f4167bc0977687c43 by INADA Naoki in branch '3.5':
bpo-29110: Fix file object leak in `aifc.open` (GH-311)
https://github.com/python/cpython/commit/b7fb1e25c89a9eb85b95027f4167bc0977687c43
msg290394 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-03-24 23:42
New changeset 5dc33eea538361f8a218255f83db2e9298dd8c53 by INADA Naoki in branch 'master':
bpo-29110: add test for Aifc_write. (GH-293)
https://github.com/python/cpython/commit/5dc33eea538361f8a218255f83db2e9298dd8c53
History
Date User Action Args
2017-03-31 16:36:22dstufftsetpull_requests: + pull_request959
2017-03-24 23:42:56inada.naokisetmessages: + msg290394
2017-03-24 23:39:42inada.naokisetmessages: + msg290376
2017-03-24 23:39:36inada.naokisetmessages: + msg290375
2017-03-24 23:34:29inada.naokisetmessages: + msg290358
2017-03-17 21:00:33larrysetpull_requests: + pull_request597
2017-02-28 12:03:44inada.naokisetstatus: open -> closed
resolution: fixed
stage: patch review -> resolved
2017-02-28 11:14:34inada.naokisetmessages: + msg288705
2017-02-28 10:50:25inada.naokisetpull_requests: + pull_request303
2017-02-27 08:11:06serhiy.storchakasetmessages: + msg288622
2017-02-26 19:50:39inada.naokisetmessages: + msg288611
2017-02-26 12:43:13inada.naokisetpull_requests: + pull_request272
2017-02-26 12:42:22inada.naokisetpull_requests: + pull_request271
2017-02-26 10:10:30serhiy.storchakasetassignee: serhiy.storchaka -> inada.naoki
2017-02-25 18:21:28inada.naokisetpull_requests: + pull_request263
2017-02-22 07:23:33inada.naokisetnosy: + inada.naoki
messages: + msg288340
2017-02-18 23:42:13azhangsetpull_requests: + pull_request127
2016-12-31 07:12:13serhiy.storchakasetmessages: + msg284371
2016-12-30 06:00:20serhiy.storchakasetassignee: serhiy.storchaka
stage: patch review

nosy: + serhiy.storchaka
versions: - Python 3.3, Python 3.4
2016-12-30 00:56:05azhangsetfiles: + fix_aifc_leak_and_file_object_close.patch
2016-12-30 00:55:17azhangcreate