From: Kobi Ginon Date: Wed, 22 Apr 2026 18:06:56 +0000 (+0300) Subject: orch/cephadm: fix redeploy --force, validate container image ref X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=699b387b45493f21a7e93fea42c0352d66936c08;p=ceph.git orch/cephadm: fix redeploy --force, validate container image ref The redeploy handler had no boolean "force" parameter, so the CLI could bind --force to the optional image argument. Pass force through to daemon_action, validate container image ref in cephadm, and guard against --force being captured as the image in the CLI. Fixes: https://tracker.ceph.com/issues/75967 Signed-off-by: Kobi Ginon Made-with: Cursor --- diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index e6704e8a023c..fbaae2b95618 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -62,7 +62,7 @@ from mgr_module import ( NotifyType, MonCommandFailed, ) -from mgr_util import build_url, NvmeofMetadataPoolHelper +from mgr_util import build_url, is_valid_container_image_ref, NvmeofMetadataPoolHelper import orchestrator from orchestrator.module import to_format, Format @@ -2722,6 +2722,9 @@ Then run the following: raise OrchestratorError( f'Cannot redeploy {daemon_type}.{daemon_id} with a new image: Supported ' f'types are: {", ".join(CEPH_IMAGE_TYPES)}') + if not is_valid_container_image_ref(image): + raise OrchestratorError( + f'Invalid container image {image!r} (not a valid container image reference)') self.check_mon_command({ 'prefix': 'config set', diff --git a/src/pybind/mgr/mgr_util.py b/src/pybind/mgr/mgr_util.py index fa44e6c56b90..d862145a9792 100644 --- a/src/pybind/mgr/mgr_util.py +++ b/src/pybind/mgr/mgr_util.py @@ -17,6 +17,7 @@ import errno import socket import time import logging +import re import sys from ipaddress import ip_address from threading import Lock, Condition @@ -62,6 +63,67 @@ UNDERLINE_SEQ = "\033[4m" logger = logging.getLogger(__name__) +# NAME and TAG are taken verbatim from the OCI distribution spec: +# https://github.com/opencontainers/distribution-spec/blob/main/spec.md +# DIGEST is based on the OCI image-spec descriptor grammar: +# https://github.com/opencontainers/image-spec/blob/main/descriptor.md +# REGISTRY is a practical heuristic, not defined by either spec. +# +# Catches malformed input. +# Not a full OCI image reference parser. + +# distribution-spec, verbatim: +NAME = r"[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" + +# distribution-spec, verbatim: +TAG = r"[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}" + +# image-spec descriptor grammar translated literally: +GENERIC_DIGEST_RE = re.compile( + r"^[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$", + re.ASCII, +) + +# Practical heuristic for hostname[:port]. +REGISTRY = r"(?:[a-zA-Z0-9.-]+(?::[0-9]+)?)" + +STRICT_KNOWN_DIGESTS = { + "sha256": re.compile(r"^[a-f0-9]{64}$", re.ASCII), + "sha512": re.compile(r"^[a-f0-9]{128}$", re.ASCII), + "blake3": re.compile(r"^[a-f0-9]{64}$", re.ASCII), +} + +IMAGE_RE = re.compile( + rf""" + ^ + (?:{REGISTRY}/)? + {NAME} + (?::{TAG})? + (?:@(?P[a-z0-9]+(?:[+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+))? + $ + """, + re.VERBOSE | re.ASCII, +) + + +def is_valid_digest(digest: str) -> bool: + if not GENERIC_DIGEST_RE.fullmatch(digest): + return False + algorithm, encoded = digest.split(":", 1) + checker = STRICT_KNOWN_DIGESTS.get(algorithm) + if checker is None: + return True + return checker.fullmatch(encoded) is not None + + +def is_valid_container_image_ref(ref: str) -> bool: + """Basic sanity check for OCI/Docker-style container image references.""" + m = IMAGE_RE.fullmatch(ref) + if m is None: + return False + digest = m.group("digest") + return digest is None or is_valid_digest(digest) + class PortAlreadyInUse(Exception): pass diff --git a/src/pybind/mgr/orchestrator/module.py b/src/pybind/mgr/orchestrator/module.py index 83db20187f7b..d2df42ceec04 100644 --- a/src/pybind/mgr/orchestrator/module.py +++ b/src/pybind/mgr/orchestrator/module.py @@ -22,7 +22,13 @@ from ceph.deployment.hostspec import SpecValidationError from ceph.deployment.utils import unwrap_ipv6 from ceph.utils import datetime_now from ceph.cephadm.images import NonCephImageServiceTypes -from mgr_util import to_pretty_timedelta, format_bytes, parse_combined_pem_file, NvmeofMetadataPoolHelper +from mgr_util import ( + is_valid_container_image_ref, + to_pretty_timedelta, + format_bytes, + parse_combined_pem_file, + NvmeofMetadataPoolHelper, +) from mgr_module import MgrModule, HandleCommandResult, Option from object_format import Format @@ -1791,11 +1797,16 @@ Usage: @OrchestratorCLICommand.Write('orch daemon redeploy') def _daemon_action_redeploy(self, name: str, - image: Optional[str] = None) -> HandleCommandResult: + image: Optional[str] = None, + force: bool = False) -> HandleCommandResult: """Redeploy a daemon (with a specific image)""" if '.' not in name: raise OrchestratorError('%s is not a valid daemon name' % name) - completion = self.daemon_action("redeploy", name, image=image) + if image is not None and not is_valid_container_image_ref(image): + raise OrchestratorError( + f'Invalid container image {image!r} (not a valid container image reference)' + ) + completion = self.daemon_action("redeploy", name, image=image, force=force) raise_if_exception(completion) return HandleCommandResult(stdout=completion.result_str()) diff --git a/src/pybind/mgr/tests/test_mgr_util.py b/src/pybind/mgr/tests/test_mgr_util.py index b9307ccca427..fb17ac271b36 100644 --- a/src/pybind/mgr/tests/test_mgr_util.py +++ b/src/pybind/mgr/tests/test_mgr_util.py @@ -85,3 +85,71 @@ class TestCephFsEarmarkResolver: mock_parse_earmark.side_effect = mgr_util.EarmarkParseError result = resolver.check_earmark("error.test", mgr_util.EarmarkTopScope.SMB) assert result is False + + +_SHA256_OK = "a" * 64 +_SHA256_BAD_HEX = "g" * 64 +_SHA256_SHORT = "a" * 63 +_SHA512_OK = "b" * 128 +_SHA512_SHORT = "b" * 127 +_BLAKE3_OK = "c" * 64 + + +@pytest.mark.parametrize( + "digest, expected", + [ + (f"sha256:{_SHA256_OK}", True), + (f"sha512:{_SHA512_OK}", True), + (f"blake3:{_BLAKE3_OK}", True), + (f"sha256:{_SHA256_BAD_HEX}", False), + (f"sha256:{_SHA256_SHORT}", False), + (f"sha512:{_SHA512_SHORT}", False), + ("notadigest", False), + ("", False), + ("nocolon", False), + # Unknown algorithm: generic descriptor grammar only (no length check). + ("foo:bar", True), + ("acme:encoded-value_1", True), + ], +) +def test_is_valid_digest(digest: str, expected: bool): + assert mgr_util.is_valid_digest(digest) is expected + + +@pytest.mark.parametrize( + "ref, expected", + [ + # Typical registry + path + tag + ("quay.io/ceph/ceph", True), + ("quay.io/ceph/ceph:v18.2.0", True), + ("registry.example.com:5000/foo/bar:latest", True), + ("docker.io/library/nginx:1.25", True), + ("gcr.io/google-containers/pause:3.1", True), + ("mcr.microsoft.com/devcontainers/base:debian", True), + (f"quay.io/foo/bar@sha256:{_SHA256_OK}", True), + (f"localhost:5000/myapp/service@sha512:{_SHA512_OK}", True), + (f"127.0.0.1:5000/img/name@blake3:{_BLAKE3_OK}", True), + # Shorthand names (no registry) + ("ceph/ceph", True), + ("library/nginx:1", True), + ("alpine", True), + ("postgres:16-alpine", True), + # Tag charset (OCI distribution-spec tag) + ("quay.io/x/img:v1.2.3-rc.1", True), + ("registry.io/a/b/c/d/e:1", True), + # Reject common CLI/flag confusions + ("--force", False), + ("-f", False), + ("--image", False), + ("", False), + # Path traversal–looking strings can still match the permissive REGISTRY/NAME + # heuristic (e.g. ".."/"escape"). Use characters NAME/TAG reject instead: + ("quay.io/ceph/ceph:bad tag", False), + ("!not/valid", False), + # Malformed or wrong-length digest + (f"quay.io/foo@sha256:{_SHA256_BAD_HEX}", False), + (f"docker.io/lib/img@sha256:{_SHA256_SHORT}", False), + ], +) +def test_is_valid_container_image_ref(ref: str, expected: bool): + assert mgr_util.is_valid_container_image_ref(ref) is expected