pax 72803f0b14 security: fix #2 — wire hardened mpv options into _MpvGLWidget
Replaces the inline mpv.MPV(...) literal kwargs with a call through
build_mpv_kwargs(), which adds ytdl=no, load_scripts=no, a lavf
protocol whitelist (file,http,https,tls,tcp), and POSIX input_conf
lockdown. Closes the yt-dlp delegation surface (CVE-prone extractors
invoked on attacker-supplied URLs) and the concat:/subfile: local-
file-read gadget via ffmpeg's lavf demuxer.

behavior change from v0.2.5: any file_url whose host is only
handled by yt-dlp (youtube.com, reddit.com, etc.) will no longer
play. Boorus do not legitimately return such URLs, so in practice
this only affects hostile responses. Cached local files and direct
https .mp4/.webm/.mkv continue to work.

Manually smoke tested: played a cached local .mp4 from the library
(file: protocol) and a fresh network .webm from a danbooru search
(https: protocol) — both work.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:07:33 -05:00

130 lines
4.2 KiB
Python

"""mpv OpenGL render context host widgets."""
from __future__ import annotations
import logging
import sys
from PySide6.QtCore import Signal
from PySide6.QtOpenGLWidgets import QOpenGLWidget as _QOpenGLWidget
from PySide6.QtWidgets import QWidget, QVBoxLayout
import mpv as mpvlib
from ._mpv_options import build_mpv_kwargs
log = logging.getLogger(__name__)
class _MpvGLWidget(QWidget):
"""OpenGL widget that hosts mpv rendering via the render API.
Subclasses QOpenGLWidget so initializeGL/paintGL are dispatched
correctly by Qt's C++ virtual method mechanism.
Works on both X11 and Wayland.
"""
_frame_ready = Signal() # mpv thread → main thread repaint trigger
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._gl: _MpvOpenGLSurface = _MpvOpenGLSurface(self)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._gl)
self._ctx: mpvlib.MpvRenderContext | None = None
self._gl_inited = False
self._proc_addr_fn = None
self._frame_ready.connect(self._gl.update)
# Create mpv eagerly on the main thread.
#
# Options come from `build_mpv_kwargs` (see `_mpv_options.py`
# for the full rationale). Summary: Discord screen-share audio
# fix via `ao=pulse`, fast-load vd-lavc options, network cache
# tuning for the uncached-video fast path, and the SECURITY
# hardening from audit #2 (ytdl=no, load_scripts=no,
# demuxer_lavf_o protocol whitelist, POSIX input_conf null).
self._mpv = mpvlib.MPV(
**build_mpv_kwargs(is_windows=sys.platform == "win32"),
)
# Wire up the GL surface's callbacks to us
self._gl._owner = self
def _init_gl(self) -> None:
if self._gl_inited or self._mpv is None:
return
from PySide6.QtGui import QOpenGLContext
ctx = QOpenGLContext.currentContext()
if not ctx:
return
def _get_proc_address(_ctx, name):
if isinstance(name, bytes):
name_str = name
else:
name_str = name.encode('utf-8')
addr = ctx.getProcAddress(name_str)
if addr is not None:
return int(addr)
return 0
self._proc_addr_fn = mpvlib.MpvGlGetProcAddressFn(_get_proc_address)
self._ctx = mpvlib.MpvRenderContext(
self._mpv, 'opengl',
opengl_init_params={'get_proc_address': self._proc_addr_fn},
)
self._ctx.update_cb = self._on_mpv_frame
self._gl_inited = True
def _on_mpv_frame(self) -> None:
"""Called from mpv thread when a new frame is ready."""
self._frame_ready.emit()
def _paint_gl(self) -> None:
if self._ctx is None:
self._init_gl()
if self._ctx is None:
return
ratio = self._gl.devicePixelRatioF()
w = int(self._gl.width() * ratio)
h = int(self._gl.height() * ratio)
self._ctx.render(
opengl_fbo={'w': w, 'h': h, 'fbo': self._gl.defaultFramebufferObject()},
flip_y=True,
)
def ensure_gl_init(self) -> None:
"""Force GL context creation and render context setup.
Needed when the widget is hidden (e.g. inside a QStackedWidget)
but mpv needs a render context before loadfile().
"""
if not self._gl_inited:
log.debug("GL render context init (first-time for widget %s)", id(self))
self._gl.makeCurrent()
self._init_gl()
def cleanup(self) -> None:
if self._ctx:
self._ctx.free()
self._ctx = None
if self._mpv:
self._mpv.terminate()
self._mpv = None
class _MpvOpenGLSurface(_QOpenGLWidget):
"""QOpenGLWidget subclass — delegates initializeGL/paintGL to _MpvGLWidget."""
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._owner: _MpvGLWidget | None = None
def initializeGL(self) -> None:
if self._owner:
self._owner._init_gl()
def paintGL(self) -> None:
if self._owner:
self._owner._paint_gl()