mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-02 11:50:31 +03:00
Merge remote-tracking branch 'origin/master' into dev
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
Brings the go2rtc security fixes from #24658 to dev. Dev-specific changes in the merge: - frigate/config/env.py stays deleted; the environment_vars protection for GO2RTC_ALLOW_ARBITRARY_EXEC moves to apply_config_env_vars in frigate/util/env.py, so the value never reaches os.environ - services.py imports substitute_frigate_vars from frigate.util.env and maps UnknownVariableError to the unknown variable message - go2rtc stream tests patch dev's import-time exec override snapshot and its FRIGATE_* variable sources - test_http_media.py keeps dev's _insert_recording helper; the one added on master replaced it and broke the dev tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+14
-1
@@ -165,12 +165,25 @@ def reload_sources(warn: bool = True) -> None:
|
||||
_rebuild(warn)
|
||||
|
||||
|
||||
# Security overrides that must come from the container environment, which is
|
||||
# controlled by the operator, rather than from the config, which admins can edit
|
||||
_PROTECTED_ENV_VARS = frozenset({"GO2RTC_ALLOW_ARBITRARY_EXEC"})
|
||||
|
||||
|
||||
def apply_config_env_vars(values: Mapping[str, object]) -> None:
|
||||
"""Install the environment_vars block as the lowest priority source.
|
||||
|
||||
Unprefixed keys only set os.environ.
|
||||
Unprefixed keys only set os.environ. Protected security overrides are
|
||||
skipped.
|
||||
"""
|
||||
for key, value in values.items():
|
||||
if key.upper() in _PROTECTED_ENV_VARS:
|
||||
logger.warning(
|
||||
"Ignoring %s in environment_vars, it must be set in the container environment",
|
||||
key,
|
||||
)
|
||||
continue
|
||||
|
||||
resolved = str(value)
|
||||
|
||||
if key.startswith("FRIGATE_"):
|
||||
|
||||
+121
-4
@@ -14,6 +14,7 @@ import subprocess as sp
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
@@ -29,6 +30,7 @@ from frigate.const import (
|
||||
SHM_FRAMES_VAR,
|
||||
)
|
||||
from frigate.util.builtin import clean_camera_user_pass, escape_special_characters
|
||||
from frigate.util.env import UnknownVariableError, substitute_frigate_vars
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -511,8 +513,9 @@ def get_intel_gpu_stats(
|
||||
Each DRM client FD exposes monotonic per-engine busy counters via
|
||||
/proc/<pid>/fdinfo/<fd>. For i915 this requires kernel 6.5 or newer:
|
||||
earlier kernels omit the per-engine counters whenever GuC submission is
|
||||
active, which is the default on 12th gen and newer. Xe has exposed them
|
||||
since its first release. We sample twice and divide busy-time deltas by
|
||||
active, which is the default on 12th gen and newer. i915 never exposes
|
||||
them on GPUs older than Broadwell (Gen8). Xe has exposed them since its
|
||||
first release. We sample twice and divide busy-time deltas by
|
||||
wall-clock to derive utilization. Render/3D and Compute are pooled into
|
||||
"compute"; Video and VideoEnhance into "dec". Overall "gpu" is the sum of
|
||||
those pools (clamped to 100%).
|
||||
@@ -581,10 +584,11 @@ def get_intel_gpu_stats(
|
||||
# i915 suppresses per-client engine counters while GuC submission is
|
||||
# active on kernels older than 6.5 (kernel commit 1324680a80eb lifted
|
||||
# this), which covers stock Debian 12 and Ubuntu 22.04 on 12th gen
|
||||
# and newer.
|
||||
# and newer. It never publishes them for pre-Broadwell GPUs.
|
||||
logger.warning(
|
||||
"Unable to collect Intel GPU stats: found %d DRM client(s) for %s but "
|
||||
"no per-engine counters. Kernel 6.5 or newer is required.",
|
||||
"the kernel reports no usage for them. GPUs older than Broadwell "
|
||||
"(5th gen) are not supported. 12th gen and newer need kernel 6.5+",
|
||||
len(snapshot_a),
|
||||
"/".join(sorted({client["driver"] for client in snapshot_a.values()})),
|
||||
)
|
||||
@@ -1027,6 +1031,119 @@ def is_restricted_go2rtc_source(stream_source: str) -> bool:
|
||||
return not is_go2rtc_arbitrary_exec_allowed()
|
||||
|
||||
|
||||
# go2rtc expands an `ffmpeg:` source into an `exec:` command line built from its
|
||||
# `#key=value` parameters, so only parameters that map to fixed presets or plain
|
||||
# numbers are accepted from the API
|
||||
_GO2RTC_FFMPEG_NAME_PARAMS = frozenset({"video", "audio", "hardware"})
|
||||
_GO2RTC_FFMPEG_NUMERIC_PARAMS = frozenset(
|
||||
{"width", "height", "rotate", "bitrate", "timeout"}
|
||||
)
|
||||
_GO2RTC_FFMPEG_FLAG_PARAMS = frozenset({"async"})
|
||||
# a preset or codec name, optionally with a sample rate (e.g. opus/16000)
|
||||
_GO2RTC_FFMPEG_NAME_VALUE = re.compile(r"^([A-Za-z0-9_]*|[A-Za-z0-9_]+/[0-9]{1,6})$")
|
||||
_GO2RTC_FFMPEG_NUMERIC_VALUE = re.compile(r"^-?[0-9]+[kKmM]?$")
|
||||
|
||||
|
||||
def _is_safe_go2rtc_ffmpeg_source(stream_source: str) -> bool:
|
||||
"""Check that an `ffmpeg:` source only uses allowlisted parameters."""
|
||||
if any(c in stream_source for c in "'\"") or re.search(r"\s", stream_source):
|
||||
return False
|
||||
|
||||
_, *params = stream_source.split("#")
|
||||
for param in params:
|
||||
key, has_value, value = param.partition("=")
|
||||
if key in _GO2RTC_FFMPEG_NAME_PARAMS:
|
||||
if not _GO2RTC_FFMPEG_NAME_VALUE.match(value):
|
||||
return False
|
||||
elif key in _GO2RTC_FFMPEG_NUMERIC_PARAMS:
|
||||
if not _GO2RTC_FFMPEG_NUMERIC_VALUE.match(value):
|
||||
return False
|
||||
elif key in _GO2RTC_FFMPEG_FLAG_PARAMS:
|
||||
if has_value:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_restricted_go2rtc_api_source(stream_source: str) -> bool:
|
||||
"""Check if a stream source submitted through the API is restricted.
|
||||
|
||||
In addition to the echo/expr/exec sources, this restricts `ffmpeg:` sources
|
||||
whose parameters could inject arbitrary ffmpeg arguments. The
|
||||
GO2RTC_ALLOW_ARBITRARY_EXEC override lifts both restrictions.
|
||||
"""
|
||||
source = stream_source.strip()
|
||||
if is_restricted_go2rtc_source(source):
|
||||
return True
|
||||
if not source.startswith("ffmpeg:"):
|
||||
return False
|
||||
if _is_safe_go2rtc_ffmpeg_source(source[len("ffmpeg:") :]):
|
||||
return False
|
||||
return not is_go2rtc_arbitrary_exec_allowed()
|
||||
|
||||
|
||||
def _go2rtc_stream_sources(stream: Any) -> list[Any]:
|
||||
return stream if isinstance(stream, list) else [stream]
|
||||
|
||||
|
||||
class Go2rtcStreamIssue(str, Enum):
|
||||
"""Why a go2rtc stream source was rejected."""
|
||||
|
||||
restricted = "restricted"
|
||||
unknown_variable = "unknown_variable"
|
||||
invalid_placeholder = "invalid_placeholder"
|
||||
|
||||
|
||||
def find_restricted_go2rtc_stream_changes(
|
||||
old_streams: dict[str, Any] | None, new_streams: dict[str, Any] | None
|
||||
) -> dict[str, Go2rtcStreamIssue]:
|
||||
"""Return the go2rtc streams with new sources that must be rejected.
|
||||
|
||||
Used when the config is changed through the API. A source that is already
|
||||
in the saved config for the same stream is left alone, so streams the
|
||||
operator configured on disk keep working; any other source must resolve
|
||||
its {FRIGATE_*} placeholders and pass `is_restricted_go2rtc_api_source`.
|
||||
"""
|
||||
old_streams = old_streams if isinstance(old_streams, dict) else {}
|
||||
new_streams = new_streams if isinstance(new_streams, dict) else {}
|
||||
issues: dict[str, Go2rtcStreamIssue] = {}
|
||||
|
||||
for name, stream in new_streams.items():
|
||||
existing = _go2rtc_stream_sources(old_streams.get(name))
|
||||
|
||||
for source in _go2rtc_stream_sources(stream):
|
||||
if source in existing:
|
||||
continue
|
||||
|
||||
if not isinstance(source, str):
|
||||
# the map form is only usable with arbitrary exec enabled
|
||||
if is_go2rtc_arbitrary_exec_allowed():
|
||||
continue
|
||||
issues[name] = Go2rtcStreamIssue.restricted
|
||||
break
|
||||
|
||||
# only sources whose placeholders resolve here are accepted, so the
|
||||
# resolved value checked below is what go2rtc loads at startup
|
||||
try:
|
||||
resolved_source = substitute_frigate_vars(source)
|
||||
except UnknownVariableError:
|
||||
issues[name] = Go2rtcStreamIssue.unknown_variable
|
||||
break
|
||||
except ValueError:
|
||||
issues[name] = Go2rtcStreamIssue.invalid_placeholder
|
||||
break
|
||||
|
||||
if is_restricted_go2rtc_api_source(
|
||||
source
|
||||
) or is_restricted_go2rtc_api_source(resolved_source):
|
||||
issues[name] = Go2rtcStreamIssue.restricted
|
||||
break
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||
"""Run ffprobe on stream."""
|
||||
clean_path = escape_special_characters(path)
|
||||
|
||||
Reference in New Issue
Block a user