From aea98a045be85dcff48be9441944d946bf17d75e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 May 2024 16:00:57 +0900 Subject: [PATCH 1/5] Enable open protection in the event loop --- homeassistant/block_async_io.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index a2c187fc537d66..2ca15a1495ec26 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,5 +1,6 @@ """Block blocking calls being done in asyncio.""" +import builtins from contextlib import suppress from http.client import HTTPConnection import importlib @@ -45,11 +46,11 @@ def enable() -> None: time.sleep, strict=False, check_allowed=_check_sleep_call_allowed ) - # Currently disabled. pytz doing I/O when getting timezone. - # Prevent files being opened inside the event loop - # builtins.open = protect_loop(builtins.open) - if not _IN_TESTS: + # Prevent files being opened inside the event loop + builtins.open = protect_loop( # type: ignore[assignment] + builtins.open, strict_core=False, strict=False + ) # unittest uses `importlib.import_module` to do mocking # so we cannot protect it if we are running tests importlib.import_module = protect_loop( From e123c672bd7e138e581ad9061a5abf7a58652ccd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 May 2024 16:18:04 +0900 Subject: [PATCH 2/5] tweaks --- homeassistant/block_async_io.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 2ca15a1495ec26..4865d01d9c9d97 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -13,12 +13,21 @@ _IN_TESTS = "unittest" in sys.modules +ALLOWED_FILE_PREFIXES = ("/proc",) + def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool: # If the module is already imported, we can ignore it. return bool((args := mapped_args.get("args")) and args[0] in sys.modules) +def _check_file_allowed(mapped_args: dict[str, Any]) -> bool: + # If the file is in /proc we can ignore it. + return bool( + (args := mapped_args.get("args")) and args[0].startswith(ALLOWED_FILE_PREFIXES) + ) + + def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: # # Avoid extracting the stack unless we need to since it @@ -49,7 +58,10 @@ def enable() -> None: if not _IN_TESTS: # Prevent files being opened inside the event loop builtins.open = protect_loop( # type: ignore[assignment] - builtins.open, strict_core=False, strict=False + builtins.open, + strict_core=False, + strict=False, + check_allowed=_check_file_allowed, ) # unittest uses `importlib.import_module` to do mocking # so we cannot protect it if we are running tests From 2045758f2b25808464d2d414027f67c4c50a601d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 May 2024 16:47:42 +0900 Subject: [PATCH 3/5] tests --- tests/test_block_async_io.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 688852ecf55c2a..753a8f7ed7f064 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -1,5 +1,6 @@ """Tests for async util methods from Python source.""" +import contextlib import importlib import time from unittest.mock import Mock, patch @@ -198,3 +199,20 @@ async def test_protect_loop_importlib_import_module_in_integration( "Detected blocking call to import_module inside the event loop by " "integration 'hue' at homeassistant/components/hue/light.py, line 23" ) in caplog.text + + +async def test_protect_loop_open(caplog: pytest.LogCaptureFixture) -> None: + """Test open of a file in /proc is not reported.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + open("/proc/does_not_exist").close() + assert "Detected blocking call to open with args" not in caplog.text + + +async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: + """Test opening a file in the event loop logs.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + open("/config/data_not_exist").close() + + assert "Detected blocking call to open with args" in caplog.text From 5c81b3ade18e87b86e1838f0e4f01269007f8888 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 May 2024 07:02:48 +0900 Subject: [PATCH 4/5] fix merge --- homeassistant/block_async_io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 8862ac0362151d..0fb3e5f37fccd8 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -67,6 +67,7 @@ def enable() -> None: strict_core=False, strict=False, check_allowed=_check_file_allowed, + loop_thread_id=loop_thread_id, ) # unittest uses `importlib.import_module` to do mocking # so we cannot protect it if we are running tests From 9f4df74956f2c51d3e75a3218987f0c828bc228f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 May 2024 08:25:37 +0900 Subject: [PATCH 5/5] handle posixpath --- homeassistant/block_async_io.py | 6 +++--- tests/test_block_async_io.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 0fb3e5f37fccd8..1e47e30876c260 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -24,9 +24,9 @@ def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool: def _check_file_allowed(mapped_args: dict[str, Any]) -> bool: # If the file is in /proc we can ignore it. - return bool( - (args := mapped_args.get("args")) and args[0].startswith(ALLOWED_FILE_PREFIXES) - ) + args = mapped_args["args"] + path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721 + return path.startswith(ALLOWED_FILE_PREFIXES) def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool: diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index 753a8f7ed7f064..11b83bdcd3a6a5 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -2,7 +2,9 @@ import contextlib import importlib +from pathlib import Path, PurePosixPath import time +from typing import Any from unittest.mock import Mock, patch import pytest @@ -216,3 +218,20 @@ async def test_protect_open(caplog: pytest.LogCaptureFixture) -> None: open("/config/data_not_exist").close() assert "Detected blocking call to open with args" in caplog.text + + +@pytest.mark.parametrize( + "path", + [ + "/config/data_not_exist", + Path("/config/data_not_exist"), + PurePosixPath("/config/data_not_exist"), + ], +) +async def test_protect_open_path(path: Any, caplog: pytest.LogCaptureFixture) -> None: + """Test opening a file by path in the event loop logs.""" + block_async_io.enable() + with contextlib.suppress(FileNotFoundError): + open(path).close() + + assert "Detected blocking call to open with args" in caplog.text