diff --git a/src/conda/devcontainer-feature.json b/src/conda/devcontainer-feature.json index 8c66d4ef1..50c444828 100644 --- a/src/conda/devcontainer-feature.json +++ b/src/conda/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "conda", - "version": "2.0.1", + "version": "2.0.2", "name": "Conda", "description": "A cross-platform, language-agnostic binary package manager", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/conda", diff --git a/src/conda/install.sh b/src/conda/install.sh index fdc050c7a..113248bf7 100644 --- a/src/conda/install.sh +++ b/src/conda/install.sh @@ -5,7 +5,7 @@ #------------------------------------------------------------------------------------------------------------- VERSION=${VERSION:-"latest"} -ADD_CONDA_FORGE=$ADDCONDAFORGE +ADD_CONDA_FORGE=${ADDCONDAFORGE:-"false"} USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" UPDATE_RC="true" @@ -70,15 +70,10 @@ sudo_if() { if [ "$(id -u)" -eq 0 ] && [ "$USERNAME" != "root" ]; then su - "$USERNAME" -c "$COMMAND" else - $COMMAND + "$@" fi } -install_user_package() { - PACKAGE="$1" - sudo_if "${CONDA_DIR}/bin/python3" -m pip install --user --upgrade "$PACKAGE" -} - accept_anaconda_tos_if_needed() { if ! "${CONDA_DIR}/bin/conda" tos --help > /dev/null 2>&1; then return 0 @@ -95,8 +90,21 @@ accept_anaconda_tos_if_needed() { clean_conda_cache() { "${CONDA_DIR}/bin/conda" clean --all --yes + + # pip upgrades can leave older, extracted Conda packages behind. + for pkg_path in "${CONDA_DIR}"/pkgs/pip-[0-9]*; do + [ -e "${pkg_path}" ] || continue + pkg_name="$(basename "${pkg_path}")" + pkg_version="${pkg_name#pip-}" + pkg_version="${pkg_version%%-*}" + greater_version="$(printf '%s\n%s\n' "${pkg_version}" "26.1" | sort -V | tail -1)" + if [ "${pkg_version}" != "${greater_version}" ]; then + rm -rf "${pkg_path}" + fi + done + find "${CONDA_DIR}" -type f -name '*.pyc' -delete - find "${CONDA_DIR}" -type d -name '__pycache__' -exec rm -rf {} + + find "${CONDA_DIR}" -type d -name '__pycache__' -prune -exec rm -rf {} + rm -rf "${CONDA_DIR}/pkgs/cache" /root/.cache/pip if [ "${USERNAME}" != "root" ]; then rm -rf "/home/${USERNAME}/.cache/pip" @@ -155,12 +163,27 @@ if ! conda --version &> /dev/null ; then chown -R "${USERNAME}:conda" "${CONDA_DIR}" chmod -R g+r+w "${CONDA_DIR}" - - find "${CONDA_DIR}" -type d -print0 | xargs -n 1 -0 chmod g+s - clean_conda_cache + find "${CONDA_DIR}" -type d -print0 | xargs -n 1 -0 chmod g+s fi +# Apply Universal's security patches before committing the Conda installation layer. +# https://github.com/advisories/GHSA-537c-gmf6-5ccf +# https://nvd.nist.gov/vuln/detail/CVE-2025-6176 +sudo_if "${CONDA_DIR}/bin/conda" install -y -c defaults \ + cryptography=50.0.0 \ + pyopenssl=26.4.0 \ + openssl=3.5.8 \ + brotli=1.2.0 + +# Upgrade pip after Conda so the solver cannot downgrade the patched pip version. +# https://github.com/advisories/GHSA-jp4c-xjxw-mgf9 +# https://github.com/advisories/GHSA-mf9w-mj56-hr94 +sudo_if "${CONDA_DIR}/bin/python3" -m pip install --upgrade --no-cache-dir pip +sudo_if "${CONDA_DIR}/bin/python3" -m pip install --upgrade --no-cache-dir python-dotenv==1.2.2 + +clean_conda_cache + # Display a notice on conda when not running in GitHub Codespaces mkdir -p /usr/local/etc/vscode-dev-containers cat << 'EOF' > /usr/local/etc/vscode-dev-containers/conda-notice.txt diff --git a/test/conda/install_conda_patched_packages.sh b/test/conda/install_conda_patched_packages.sh new file mode 100644 index 000000000..e0137e7b1 --- /dev/null +++ b/test/conda/install_conda_patched_packages.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "running as vscode" test "$(id -un)" = "vscode" +source "$(dirname "$0")/patched_packages_common.sh" + +reportResults diff --git a/test/conda/install_conda_patched_packages_with_codespace.sh b/test/conda/install_conda_patched_packages_with_codespace.sh new file mode 100644 index 000000000..ac9163934 --- /dev/null +++ b/test/conda/install_conda_patched_packages_with_codespace.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "running as codespace" test "$(id -un)" = "codespace" +source "$(dirname "$0")/patched_packages_common.sh" + +reportResults diff --git a/test/conda/install_conda_patched_packages_with_codespace/Dockerfile b/test/conda/install_conda_patched_packages_with_codespace/Dockerfile new file mode 100644 index 000000000..84a598d42 --- /dev/null +++ b/test/conda/install_conda_patched_packages_with_codespace/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu:noble + +RUN if id ubuntu > /dev/null 2>&1; then userdel -f -r ubuntu; fi diff --git a/test/conda/install_conda_patched_packages_with_root.sh b/test/conda/install_conda_patched_packages_with_root.sh new file mode 100644 index 000000000..b4ce1eee8 --- /dev/null +++ b/test/conda/install_conda_patched_packages_with_root.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "running as root" test "$(id -u)" = "0" +source "$(dirname "$0")/patched_packages_common.sh" + +reportResults diff --git a/test/conda/patched_packages_common.sh b/test/conda/patched_packages_common.sh new file mode 100644 index 000000000..df030b5c9 --- /dev/null +++ b/test/conda/patched_packages_common.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Inspect cleanup before running Conda or importing packages can recreate caches. +cache_python=(/opt/conda/bin/python3 -B -) +if [ "$(id -u)" -ne 0 ]; then + cache_python=(sudo -n "${cache_python[@]}") +fi +check "installation caches removed" "${cache_python[@]}" "$HOME" <<'PY' +import sys +from pathlib import Path + +from packaging.version import Version + +conda_dir = Path("/opt/conda") +leftovers = list(conda_dir.rglob("*.pyc")) +leftovers.extend(conda_dir.rglob("__pycache__")) +for package in (conda_dir / "pkgs").glob("pip-[0-9]*"): + version = package.name.removeprefix("pip-").split("-", 1)[0] + if Version(version) < Version("26.1"): + leftovers.append(package) +for cache in ( + conda_dir / "pkgs/cache", + Path("/root/.cache/pip"), + Path(sys.argv[1]) / ".cache/pip", +): + if cache.exists(): + leftovers.append(cache) +assert not leftovers, f"Installation cache leftovers: {leftovers}" +PY + +check "patched packages installed in base environment" env PYTHONNOUSERSITE=1 /opt/conda/bin/python3 -B - <<'PY' +import json +import sys +from importlib.metadata import version +from pathlib import Path + +from packaging.version import Version + +assert sys.prefix == "/opt/conda", f"Unexpected Python prefix: {sys.prefix}" +packages = {} +for metadata in Path("/opt/conda/conda-meta").glob("*.json"): + package = json.loads(metadata.read_text()) + packages[package["name"]] = package["version"] + +for name, expected in { + "cryptography": "50.0.0", + "pyopenssl": "26.4.0", + "openssl": "3.5.8", + "brotli": "1.2.0", +}.items(): + actual = packages.get(name) + assert actual == expected, f"{name}: expected {expected}, got {actual}" + +assert Version(version("pip")) >= Version("26.1"), f"Unpatched pip: {version('pip')}" +assert version("python-dotenv") == "1.2.2", f"Unexpected python-dotenv: {version('python-dotenv')}" +PY + +check "base environment writable by runtime user" test -w /opt/conda/conda-meta +check "conda still works" conda --version +check "pip still works" /opt/conda/bin/python3 -m pip --version diff --git a/test/conda/scenarios.json b/test/conda/scenarios.json index f7c2692cb..c9a7792ea 100644 --- a/test/conda/scenarios.json +++ b/test/conda/scenarios.json @@ -1,4 +1,32 @@ { + "install_conda_patched_packages_with_codespace": { + "build": { + "dockerfile": "Dockerfile" + }, + "containerUser": "codespace", + "remoteUser": "codespace", + "features": { + "common-utils": { + "username": "codespace", + "userUid": "1000", + "userGid": "1000" + }, + "conda": {} + } + }, + "install_conda_patched_packages": { + "image": "mcr.microsoft.com/devcontainers/base:noble", + "remoteUser": "vscode", + "features": { + "conda": {} + } + }, + "install_conda_patched_packages_with_root": { + "image": "ubuntu:noble", + "features": { + "conda": {} + } + }, "install_conda": { "image": "mcr.microsoft.com/devcontainers/base:noble", "features": {