diff options
author | Florian Bruhin <me@the-compiler.org> | 2022-03-07 11:15:00 +0100 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2022-03-07 11:15:00 +0100 |
commit | a855831e24c895607848b15ca089f12cb190ad5b (patch) | |
tree | 3b1888c28791ffa605debe4c149727f380c8bdc9 | |
parent | 0924631cdcb1d058e96dac2a1b5ad14b427dd707 (diff) | |
parent | 0b412ffb740b4f752afb421a0a5d89d09f1965af (diff) | |
download | qutebrowser-a855831e24c895607848b15ca089f12cb190ad5b.tar.gz qutebrowser-a855831e24c895607848b15ca089f12cb190ad5b.zip |
Merge branch 'new-chromium-args'
-rw-r--r-- | doc/changelog.asciidoc | 4 | ||||
-rw-r--r-- | doc/help/settings.asciidoc | 114 | ||||
-rw-r--r-- | doc/qutebrowser.1.asciidoc | 2 | ||||
-rw-r--r-- | qutebrowser/config/configdata.yml | 39 | ||||
-rw-r--r-- | qutebrowser/config/qtargs.py | 9 | ||||
-rw-r--r-- | qutebrowser/html/warning-webkit.html | 6 | ||||
-rw-r--r-- | qutebrowser/utils/version.py | 2 | ||||
-rw-r--r-- | tests/conftest.py | 5 | ||||
-rw-r--r-- | tests/end2end/fixtures/quteprocess.py | 4 | ||||
-rw-r--r-- | tests/end2end/test_invocations.py | 80 | ||||
-rw-r--r-- | tests/helpers/testutils.py | 42 | ||||
-rw-r--r-- | tests/unit/config/test_qtargs.py | 26 |
12 files changed, 251 insertions, 82 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index d46a19709..9df2ce00c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -47,6 +47,9 @@ Changed than `rofi` to ask for a password. - The `content.headers.custom` setting now accepts empty strings as values, resulting in an empty header being sent. +- Renamed settings: + * `qt.low_end_device_mode` -> `qt.chromium.low_end_device_mode` + * `qt.process_model` -> `qt.chromium.process_model` Added ~~~~~ @@ -57,6 +60,7 @@ Added which shows relative tab numbers. - New `input.mode_override` option which allows overriding the current mode based on the new URL when navigating or switching tabs. +- New `qt.chromium.sandboxing` setting which allows to disable Chromium's sandboxing (mainly intended for development and testing) Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index a51463352..0adc95fdd 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -287,13 +287,14 @@ |<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in download prompts. |<<prompt.radius,prompt.radius>>|Rounding radius (in pixels) for the edges of prompts. |<<qt.args,qt.args>>|Additional arguments to pass to Qt, without leading `--`. +|<<qt.chromium.low_end_device_mode,qt.chromium.low_end_device_mode>>|When to use Chromium's low-end device mode. +|<<qt.chromium.process_model,qt.chromium.process_model>>|Which Chromium process model to use. +|<<qt.chromium.sandboxing,qt.chromium.sandboxing>>|What sandboxing mechanisms in Chromium to use. |<<qt.environ,qt.environ>>|Additional environment variables to set. |<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use. |<<qt.force_platformtheme,qt.force_platformtheme>>|Force a Qt platformtheme to use. |<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine. |<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling. -|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode. -|<<qt.process_model,qt.process_model>>|Which Chromium process model to use. |<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3. |<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start. |<<scrolling.bar,scrolling.bar>>|When/how to show the scrollbar. @@ -3772,6 +3773,73 @@ Type: <<types,List of String>> Default: empty +[[qt.chromium.low_end_device_mode]] +=== qt.chromium.low_end_device_mode +When to use Chromium's low-end device mode. +This improves the RAM usage of renderer processes, at the expense of performance. + +This setting requires a restart. + +This setting is only available with the QtWebEngine backend. + +Type: <<types,String>> + +Valid values: + + * +always+: Always use low-end device mode. + * +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM). + * +never+: Never use low-end device mode. + +Default: +pass:[auto]+ + +[[qt.chromium.process_model]] +=== qt.chromium.process_model +Which Chromium process model to use. +Alternative process models use less resources, but decrease security and robustness. +See the following pages for more details: + + - https://www.chromium.org/developers/design-documents/process-models + - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models + +This setting requires a restart. + +This setting is only available with the QtWebEngine backend. + +Type: <<types,String>> + +Valid values: + + * +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated. + * +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness. + * +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`. + +Default: +pass:[process-per-site-instance]+ + +[[qt.chromium.sandboxing]] +=== qt.chromium.sandboxing +What sandboxing mechanisms in Chromium to use. +Chromium has various sandboxing layers, which should be enabled for normal browser usage. Mainly for testing and development, it's possible to disable individual sandboxing layers via this setting. +Open `chrome://sandbox` to see the current sandbox status. +Changing this setting is only recommended if you know what you're doing, as it **disables one of Chromium's security layers**. To avoid sandboxing being accidentally disabled persistently, this setting can only be set via `config.py`, not via `:set`. +See the Chromium documentation for more details: +- https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/linux/sandboxing.md[Linux] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows] - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)] + +This setting requires a restart. + +This setting can only be set in config.py. + +This setting is only available with the QtWebEngine backend. + +Type: <<types,String>> + +Valid values: + + * +enable-all+: Enable all available sandboxing mechanisms. + * +disable-seccomp-bpf+: Disable the Seccomp BPF filter sandbox (Linux only). + * +disable-all+: Disable all sandboxing (**not recommended!**). + +Default: +pass:[enable-all]+ + [[qt.environ]] === qt.environ Additional environment variables to set. @@ -3837,48 +3905,6 @@ Type: <<types,Bool>> Default: +pass:[false]+ -[[qt.low_end_device_mode]] -=== qt.low_end_device_mode -When to use Chromium's low-end device mode. -This improves the RAM usage of renderer processes, at the expense of performance. - -This setting requires a restart. - -This setting is only available with the QtWebEngine backend. - -Type: <<types,String>> - -Valid values: - - * +always+: Always use low-end device mode. - * +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM). - * +never+: Never use low-end device mode. - -Default: +pass:[auto]+ - -[[qt.process_model]] -=== qt.process_model -Which Chromium process model to use. -Alternative process models use less resources, but decrease security and robustness. -See the following pages for more details: - - - https://www.chromium.org/developers/design-documents/process-models - - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models - -This setting requires a restart. - -This setting is only available with the QtWebEngine backend. - -Type: <<types,String>> - -Valid values: - - * +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated. - * +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness. - * +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`. - -Default: +pass:[process-per-site-instance]+ - [[qt.workarounds.locale]] === qt.workarounds.locale Work around locale parsing issues in QtWebEngine 5.15.3. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index bc312f108..f22be1ddc 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -34,7 +34,7 @@ show it. *'URL'*:: URLs to open on startup (empty as a window separator). -=== optional arguments +=== options *-h*, *--help*:: show this help message and exit diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 59f16511f..1d41d147d 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -239,6 +239,9 @@ qt.force_platformtheme: based on the desktop environment. qt.process_model: + renamed: qt.chromium.process_model + +qt.chromium.process_model: type: name: String valid_values: @@ -268,6 +271,9 @@ qt.process_model: - https://doc.qt.io/qt-5/qtwebengine-features.html#process-models qt.low_end_device_mode: + renamed: qt.chromium.low_end_device_mode + +qt.chromium.low_end_device_mode: type: name: String valid_values: @@ -284,6 +290,39 @@ qt.low_end_device_mode: This improves the RAM usage of renderer processes, at the expense of performance. +qt.chromium.sandboxing: + type: + name: String + valid_values: + - enable-all: Enable all available sandboxing mechanisms. + - disable-seccomp-bpf: Disable the Seccomp BPF filter sandbox (Linux only). + - disable-all: Disable all sandboxing (**not recommended!**). + default: enable-all + backend: QtWebEngine + restart: true + no_autoconfig: true # due to it being dangerous + # yamllint disable rule:line-length + desc: >- + What sandboxing mechanisms in Chromium to use. + + Chromium has various sandboxing layers, which should be enabled for normal + browser usage. Mainly for testing and development, it's possible to disable + individual sandboxing layers via this setting. + + Open `chrome://sandbox` to see the current sandbox status. + + Changing this setting is only recommended if you know what you're doing, as + it **disables one of Chromium's security layers**. To avoid sandboxing being + accidentally disabled persistently, this setting can only be set via + `config.py`, not via `:set`. + + See the Chromium documentation for more details: + + - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/linux/sandboxing.md[Linux] + - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox.md[Windows] + - https://chromium.googlesource.com/chromium/src/\+/HEAD/docs/design/sandbox_faq.md[FAQ (Windows-centric)] + # yamllint enable rule:line-length + qt.highdpi: type: Bool default: false diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 2f93b7de5..9e7f2620d 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -321,12 +321,12 @@ def _qtwebengine_settings_args(versions: version.WebEngineVersions) -> Iterator[ '--force-webrtc-ip-handling-policy=' 'disable_non_proxied_udp', }, - 'qt.process_model': { + 'qt.chromium.process_model': { 'process-per-site-instance': None, 'process-per-site': '--process-per-site', 'single-process': '--single-process', }, - 'qt.low_end_device_mode': { + 'qt.chromium.low_end_device_mode': { 'auto': None, 'always': '--enable-low-end-device-mode', 'never': '--disable-low-end-device-mode', @@ -338,6 +338,11 @@ def _qtwebengine_settings_args(versions: version.WebEngineVersions) -> Iterator[ True: '--force-prefers-reduced-motion', False: None, }, + 'qt.chromium.sandboxing': { + 'enable-all': None, + 'disable-seccomp-bpf': '--disable-seccomp-filter-sandbox', + 'disable-all': '--no-sandbox', + } } qt_514_ver = utils.VersionNumber(5, 14) diff --git a/qutebrowser/html/warning-webkit.html b/qutebrowser/html/warning-webkit.html index 975f98c1b..f5cf9bf01 100644 --- a/qutebrowser/html/warning-webkit.html +++ b/qutebrowser/html/warning-webkit.html @@ -41,8 +41,8 @@ hopefully help.</p> notification support was added for Qt 5.13.0.</p> <p><b>Resource usage</b>: qutebrowser v1.5.0 added the <span -class="mono">qt.process_model</span> and <span -class="mono">qt.low_end_device_mode</span> settings which can be used to +class="mono">qt.chromium.process_model</span> and <span +class="mono">qt.chromium.low_end_device_mode</span> settings which can be used to decrease the resource usage of QtWebEngine (but come with other drawbacks).</p> <p><b>Not trusting Google</b>: Various people have checked the connections made @@ -78,7 +78,7 @@ security fixes took months to arrive there). You might be better off choosing an method</a>.</p> <p><b>White flashing between loads with a custom stylesheet</b>: This doesn't -seem to happen with <span class="mono">qt.process_model = single-process</span> +seem to happen with <span class="mono">qt.chromium.process_model = single-process</span> set. However, note that that setting comes with decreased security and stability, but QtWebKit doesn't have any process isolation at all.</p> {% endblock %} diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 8da86dd00..33acdce57 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -724,7 +724,7 @@ class WebEngineVersions: ) -def qtwebengine_versions(avoid_init: bool = False) -> WebEngineVersions: +def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions: """Get the QtWebEngine and Chromium version numbers. If we have a parsed user agent, we use it here. If not, we avoid initializing diff --git a/tests/conftest.py b/tests/conftest.py index ad82b4f24..84cae784b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -194,9 +194,8 @@ def pytest_ignore_collect(path): @pytest.fixture(scope='session') def qapp_args(): """Make QtWebEngine unit tests run on older Qt versions + newer kernels.""" - seccomp_args = testutils.seccomp_args(qt_flag=False) - if seccomp_args: - return [sys.argv[0]] + seccomp_args + if testutils.disable_seccomp_bpf_sandbox(): + return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG] return [] diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 14f34b52c..ab8f28d26 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -549,8 +549,8 @@ class QuteProc(testprocess.Process): '--debug-flag', 'werror', '--debug-flag', 'test-notification-service'] - if self.request.config.webengine: - args += testutils.seccomp_args(qt_flag=True) + if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox(): + args += testutils.DISABLE_SECCOMP_BPF_ARGS args.append('about:blank') return args diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index b860feed0..80a3c016b 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -56,8 +56,8 @@ def _base_args(config): else: args += ['--backend', 'webkit'] - if config.webengine: - args += testutils.seccomp_args(qt_flag=True) + if config.webengine and testutils.disable_seccomp_bpf_sandbox(): + args += testutils.DISABLE_SECCOMP_BPF_ARGS args.append('about:blank') return args @@ -826,3 +826,79 @@ def test_json_logging_without_debug(request, quteproc_new, runtime_tmpdir): quteproc_new.exit_expected = True quteproc_new.start(args, env={'XDG_RUNTIME_DIR': str(runtime_tmpdir)}) assert not quteproc_new.is_running() + + +@pytest.mark.qtwebkit_skip +@pytest.mark.parametrize( + 'sandboxing, has_namespaces, has_seccomp, has_yama, expected_result', [ + ('enable-all', True, True, True, "You are adequately sandboxed."), + ('disable-seccomp-bpf', True, False, True, "You are NOT adequately sandboxed."), + ('disable-all', False, False, False, "You are NOT adequately sandboxed."), + ] +) +def test_sandboxing( + request, quteproc_new, sandboxing, + has_namespaces, has_seccomp, has_yama, expected_result, +): + if not request.config.webengine: + pytest.skip("Skipped with QtWebKit") + elif sandboxing == "enable-all" and testutils.disable_seccomp_bpf_sandbox(): + pytest.skip("Full sandboxing not supported") + + args = _base_args(request.config) + [ + '--temp-basedir', + '-s', 'qt.chromium.sandboxing', sandboxing, + ] + quteproc_new.start(args) + + quteproc_new.open_url('chrome://sandbox') + text = quteproc_new.get_content() + print(text) + + not_found_msg = ("The webpage at chrome://sandbox/ might be temporarily down or " + "it may have moved permanently to a new web address.") + if not_found_msg in text.split("\n"): + pytest.skip("chrome://sandbox/ not supported") + + bpf_text = "Seccomp-BPF sandbox" + yama_text = "Ptrace Protection with Yama LSM" + + if "\n\n\n" in text: + # Qt 5.12 + header, rest = text.split("\n", maxsplit=1) + rest, result = rest.rsplit("\n\n", maxsplit=1) + lines = rest.replace("\t\n", "\t").split("\n\n\n") + + expected_status = { + "Namespace Sandbox": "Yes" if has_namespaces else "No", + "Network namespaces": "Yes" if has_namespaces else "No", + "PID namespaces": "Yes" if has_namespaces else "No", + "SUID Sandbox": "No", + + bpf_text: "Yes" if has_seccomp else "No", + f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", + + "Yama LSM Enforcing": "Yes" if has_yama else "No", + } + else: + header, *lines, empty, result = text.split("\n") + assert not empty + + expected_status = { + "Layer 1 Sandbox": "Namespace" if has_namespaces else "None", + + "PID namespaces": "Yes" if has_namespaces else "No", + "Network namespaces": "Yes" if has_namespaces else "No", + + bpf_text: "Yes" if has_seccomp else "No", + f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", + + f"{yama_text} (Broker)": "Yes" if has_yama else "No", + f"{yama_text} (Non-broker)": "No", + } + + assert header == "Sandbox Status" + assert result == expected_result + + status = dict(line.split("\t") for line in lines) + assert status == expected_status diff --git a/tests/helpers/testutils.py b/tests/helpers/testutils.py index 8bb622133..c607718ab 100644 --- a/tests/helpers/testutils.py +++ b/tests/helpers/testutils.py @@ -31,14 +31,9 @@ import importlib.machinery import pytest -from PyQt5.QtCore import qVersion from PyQt5.QtGui import QColor -try: - from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION_STR -except ImportError: - PYQT_WEBENGINE_VERSION_STR = None -from qutebrowser.utils import qtutils, log, utils +from qutebrowser.utils import qtutils, log, utils, version ON_CI = 'CI' in os.environ @@ -267,35 +262,38 @@ def easyprivacy_txt(): return _decompress_gzip_datafile("easyprivacy.txt.gz") -def seccomp_args(qt_flag): - """Get necessary flags to disable the seccomp BPF sandbox. +DISABLE_SECCOMP_BPF_FLAG = "--disable-seccomp-filter-sandbox" +DISABLE_SECCOMP_BPF_ARGS = ["-s", "qt.chromium.sandboxing", "disable-seccomp-bpf"] + + +def disable_seccomp_bpf_sandbox(): + """Check whether we need to disable the seccomp BPF sandbox. This is needed for some QtWebEngine setups, with older Qt versions but newer kernels. - - Args: - qt_flag: Add a '--qt-flag' argument. """ + try: + from PyQt5 import QtWebEngine # pylint: disable=unused-import + except ImportError: + # no QtWebEngine available + return False + affected_versions = set() for base, patch_range in [ - # 5.12.0 to 5.12.7 (inclusive) - ('5.12', range(0, 8)), + # 5.12.0 to 5.12.10 (inclusive) + ('5.12', range(0, 11)), # 5.13.0 to 5.13.2 (inclusive) ('5.13', range(0, 3)), # 5.14.0 ('5.14', [0]), + # 5.15.0 to 5.15.2 (inclusive) + ('5.15', range(0, 3)), ]: for patch in patch_range: - affected_versions.add('{}.{}'.format(base, patch)) - - version = (PYQT_WEBENGINE_VERSION_STR - if PYQT_WEBENGINE_VERSION_STR is not None - else qVersion()) - if version in affected_versions: - disable_arg = 'disable-seccomp-filter-sandbox' - return ['--qt-flag', disable_arg] if qt_flag else ['--' + disable_arg] + affected_versions.add(utils.VersionNumber.parse(f'{base}.{patch}')) - return [] + versions = version.qtwebengine_versions(avoid_init=True) + return versions.webengine in affected_versions def import_userscript(name): diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index d95382624..076ff6e3c 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -249,7 +249,7 @@ class TestWebEngineArgs: ('single-process', True), ]) def test_process_model(self, config_stub, parser, process_model, added): - config_stub.val.qt.process_model = process_model + config_stub.val.qt.chromium.process_model = process_model parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -267,7 +267,7 @@ class TestWebEngineArgs: ('never', '--disable-low-end-device-mode'), ]) def test_low_end_device_mode(self, config_stub, parser, low_end_device_mode, arg): - config_stub.val.qt.low_end_device_mode = low_end_device_mode + config_stub.val.qt.chromium.low_end_device_mode = low_end_device_mode parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -277,6 +277,28 @@ class TestWebEngineArgs: else: assert arg in args + @pytest.mark.parametrize('sandboxing, arg', [ + ('enable-all', None), + ('disable-seccomp-bpf', '--disable-seccomp-filter-sandbox'), + ('disable-all', '--no-sandbox'), + ]) + def test_sandboxing(self, config_stub, parser, sandboxing, arg): + config_stub.val.qt.chromium.sandboxing = sandboxing + parsed = parser.parse_args([]) + args = qtargs.qt_args(parsed) + + remaining_flags = { + '--no-sandbox', + '--disable-seccomp-filter-sandbox', + } + if arg is not None: + remaining_flags.remove(arg) + + if arg is not None: + assert arg in args + + assert not set(args) & remaining_flags + @pytest.mark.parametrize('qt_version, referer, arg', [ # 'always' -> no arguments ('5.15.0', 'always', None), |