diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 442a414157..eccaa25290 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,10 @@ updates: interval: "daily" exclude-paths: - ".github/workflows/ci_workflow_old.yml" + groups: + codeql: + patterns: + - "github/codeql-action/*" - package-ecosystem: "pip" directory: "/website" diff --git a/.github/workflows/analysis_workflow.yml b/.github/workflows/analysis_workflow.yml index 74afface28..cb8582d805 100644 --- a/.github/workflows/analysis_workflow.yml +++ b/.github/workflows/analysis_workflow.yml @@ -35,7 +35,7 @@ jobs: CC: gcc steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 50 - name: Create build directories @@ -105,7 +105,7 @@ jobs: - name: Setup container run: sudo rm -rf /usr/local/lib64/cmake/glew - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 50 - name: Create build directories @@ -184,7 +184,7 @@ jobs: - name: Setup container run: sudo rm -rf /usr/local/lib64/cmake/glew - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 50 - name: Create build directories diff --git a/.github/workflows/bazel_build.yml b/.github/workflows/bazel_build.yml index caa5b21ab3..e9d25d15e3 100644 --- a/.github/workflows/bazel_build.yml +++ b/.github/workflows/bazel_build.yml @@ -39,10 +39,10 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Mount Bazel cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: "/home/runner/.cache/bazel" key: bazel-ubuntu-24 @@ -57,10 +57,10 @@ jobs: runs-on: windows-2025 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Mount Bazel cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: "/home/runner/.cache/bazel" key: bazel-windows-2025 @@ -75,10 +75,10 @@ jobs: runs-on: macos-15 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Mount Bazel cache - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: "/home/runner/.cache/bazel" key: bazel-macos-15 diff --git a/.github/workflows/ci_freebsd.yml b/.github/workflows/ci_freebsd.yml index f44bb814bf..0bd2f3167f 100644 --- a/.github/workflows/ci_freebsd.yml +++ b/.github/workflows/ci_freebsd.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Build and test on FreeBSD - uses: vmactions/freebsd-vm@b84ab5559b5a1bb4b8ee2737d2506a16e1737636 # v1.4.8 + uses: vmactions/freebsd-vm@77ed28d336d03fe19a3f4f7266c1d2c4714dd79d # v1.5.2 with: release: '15.0' envs: GITHUB_REPOSITORY GITHUB_REF GITHUB_SHA diff --git a/.github/workflows/ci_steps.yml b/.github/workflows/ci_steps.yml index 7939eeab23..4d13a96fd6 100644 --- a/.github/workflows/ci_steps.yml +++ b/.github/workflows/ci_steps.yml @@ -88,7 +88,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set Up Directories & Environment run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 49eea65889..545d0d28d5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -61,14 +61,14 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL # cary: Pin the version to the SHA for 2.18.0, since there appears to # be a problem with 2.18.1 leading to a "No space left on # device" failure - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: # cary: the "linked" setting is necessary to force the run to pick up # the version specified in the action. @@ -99,7 +99,7 @@ jobs: - name: Perform CodeQL Analysis # Pin the version to the SHA for 2.18.0 - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: category: "/language:${{matrix.language}}" # For pull_request, upload against refs/pull//head + head SHA so the Code diff --git a/.github/workflows/python-wheels-publish-test.yml b/.github/workflows/python-wheels-publish-test.yml index eba5cf5127..3fee06aeac 100644 --- a/.github/workflows/python-wheels-publish-test.yml +++ b/.github/workflows/python-wheels-publish-test.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ github.ref }} @@ -77,11 +77,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Python if: ${{ matrix.os == 'ubuntu-latest' }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.x' @@ -139,6 +139,6 @@ jobs: path: dist - name: Publish distribution 📦 to TestPyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 with: repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/python-wheels-publish.yml b/.github/workflows/python-wheels-publish.yml index cd940d0986..5dd2995435 100644 --- a/.github/workflows/python-wheels-publish.yml +++ b/.github/workflows/python-wheels-publish.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref }} @@ -73,11 +73,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Python if: ${{ matrix.os == 'ubuntu-latest' }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.x' @@ -132,4 +132,4 @@ jobs: path: dist - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index c513b50561..8154629d86 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -61,11 +61,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Python if: ${{ matrix.os == 'ubuntu-latest' }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.x' diff --git a/.github/workflows/release-sign.yml b/.github/workflows/release-sign.yml index a24953d6f8..7268470814 100644 --- a/.github/workflows/release-sign.yml +++ b/.github/workflows/release-sign.yml @@ -55,7 +55,7 @@ jobs: shell: bash - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index fd72fa70d7..618cc9631a 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -28,7 +28,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -54,6 +54,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: results.sarif diff --git a/.github/workflows/snyk-scan-cron.yml b/.github/workflows/snyk-scan-cron.yml index 513e1bb709..23de3209df 100644 --- a/.github/workflows/snyk-scan-cron.yml +++ b/.github/workflows/snyk-scan-cron.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'AcademySoftwareFoundation/openexr' steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0 id: snyk diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 20611035f5..658b6bbd55 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Create build directory run: mkdir _build - name: Install doxygen diff --git a/CHANGES.md b/CHANGES.md index ca88cf4651..ce2243477d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ # OpenEXR Release Notes +* [Version 3.4.13](#version-3413-june-19-2026) June 19, 2026 * [Version 3.4.12](#version-3412-may-24-2026) May 24, 2026 * [Version 3.4.11](#version-3411-april-29-2026) April 29, 2026 * [Version 3.4.10](#version-3410-april-17-2026) April 17, 2026 @@ -16,6 +17,7 @@ * [Version 3.4.2](#version-342-october-15-2025) October 15, 2025 * [Version 3.4.1](#version-341-october-8-2025) October 8, 2025 * [Version 3.4.0](#version-340-september-5-2025) September 5, 2025 +* [Version 3.3.12](#version-3312-june-21-2026) June 21, 2026 * [Version 3.3.11](#version-3311-april-29-2026) April 29, 2026 * [Version 3.3.10](#version-339-april-17-2026) April 17, 2026 * [Version 3.3.9](#version-339-april-4-2026) April 4, 2026 @@ -28,6 +30,7 @@ * [Version 3.3.2](#version-332-november-11-2024) November 11, 2024 * [Version 3.3.1](#version-331-october-8-2024) October 8, 2024 * [Version 3.3.0](#version-330-september-30-2024) September 30, 2024 +* [Version 3.2.10](#version-3210-june-21-2026) June 21, 2026 * [Version 3.2.9](#version-329-april-29-2026) April 29, 2026 * [Version 3.2.8](#version-328-april-17-2026) April 17, 2026 * [Version 3.2.7](#version-327-april-3-2026) April 3, 2026 @@ -104,6 +107,93 @@ * [Version 1.0.1](#version-101) * [Version 1.0](#version-10) +## Version 3.4.13 (June 19, 2026) + +Patch release that addresses several bugs and security +vulnerabilities. + +* :bug: Fix a regression introduced in v3.4.11 in decoding of DWAA compression +* :bug: Fix to handling deep images and very large images with the OpenEXRUtil library +* :bug: Fix initiliazation issue in B44A decoding +* :bug: Validate HTJ2K chunk header length before decode +* :hammer_and_wrench: Fix when building statically and using the vendored OpenJPH library + +For the python module: + +* :snake: :sparkles: Support NumPy scalar values Box2i and V2f tuple bindings + +This release addresses the following security vulnerabilities: + +* [CVE-2026-55373](https://www.cve.org/CVERecord?id=CVE-2026-55373) + OpenEXRUtil `SampleCountChannel` `endEdit()` can loop forever on `UINT_MAX` sample counts +* [CVE-2026-55371](https://www.cve.org/CVERecord?id=CVE-2026-55371) + OpenEXRCore `exr_attr_set_bytes()` accepts NULL `type_hint` with positive `hint_length` +* [CVE-2026-55059](https://www.cve.org/CVERecord?id=CVE-2026-55059) + OpenEXRUtil `SampleCountChannel` row setter heap out-of-bounds write +* [CVE-2026-54920](https://www.cve.org/CVERecord?id=CVE-2026-54920) + Integer Overflow and Use of Uninitialized Pointer leading to Invalid Delete in OpenEXRUtil Image Resize +* [CVE-2026-53532](https://www.cve.org/CVERecord?id=CVE-2026-53532) + Unhandled assert abort in HTJ2K decoder via crafted QCD marker (DoS) + +### Merged Pull Requests + +* [2476](https://github.com/AcademySoftwareFoundation/openexr/pull/2476) + Fix the DWAA size checks regression +* [2472](https://github.com/AcademySoftwareFoundation/openexr/pull/2472) + Unlock write context in exr_get_chunk_table_offset() return paths +* [2471](https://github.com/AcademySoftwareFoundation/openexr/pull/2471) + Add section on CVE Assignment to SECURITY.md +* [2470](https://github.com/AcademySoftwareFoundation/openexr/pull/2470) + Reject NULL bytes type_hint and data in exr_attr_bytes_create() +* [2468](https://github.com/AcademySoftwareFoundation/openexr/pull/2468) + Fix infinite loop in SampleCountChannel roundListSizeUp() +* [2466](https://github.com/AcademySoftwareFoundation/openexr/pull/2466) + Fix install manifest for linux build 13 +* [2464](https://github.com/AcademySoftwareFoundation/openexr/pull/2464) + Fix heap out-of-bounds write in SampleCountChannel row setter. +* [2463](https://github.com/AcademySoftwareFoundation/openexr/pull/2463) + Fix integer overflow and invalid delete in OpenEXRUtil Image::resize() +* [2451](https://github.com/AcademySoftwareFoundation/openexr/pull/2451) + Initialize B44 tables before B44A decode +* [2445](https://github.com/AcademySoftwareFoundation/openexr/pull/2445) + Fix: Allow NumPy scalar values in Box2i and V2f tuple bindings +* [2444](https://github.com/AcademySoftwareFoundation/openexr/pull/2444) + Fix vendored OpenJPH for static builds +* [2442](https://github.com/AcademySoftwareFoundation/openexr/pull/2442) + Bugfix: Fix interger overflows +* [2438](https://github.com/AcademySoftwareFoundation/openexr/pull/2438) + Break release.py into separate small scripts for each step +* [2436](https://github.com/AcademySoftwareFoundation/openexr/pull/2436) + Release notes and news for v3.4.12 +* [2434](https://github.com/AcademySoftwareFoundation/openexr/pull/2434) + Validate HTJ2K chunk header length before decode +* [2433](https://github.com/AcademySoftwareFoundation/openexr/pull/2433) + Tighten python publish workflow security/efficiency +* [2430](https://github.com/AcademySoftwareFoundation/openexr/pull/2430) + Pin idna>=3.15 in website/requirements.txt + +### Merged Workflow Pull Requests + +* [2479](https://github.com/AcademySoftwareFoundation/openexr/pull/2479) + Split macOS wheel arches across CI runners +* [2474](https://github.com/AcademySoftwareFoundation/openexr/pull/2474) + Bump msys2/setup-msys2 from 2.31.1 to 2.32.0 +* [2467](https://github.com/AcademySoftwareFoundation/openexr/pull/2467) + Bump sigstore/gh-action-sigstore-python from 3.3.0 to 3.4.0 +* [2462](https://github.com/AcademySoftwareFoundation/openexr/pull/2462) + Update idna requirement from >=3.17 to >=3.18 in /website +* [2450](https://github.com/AcademySoftwareFoundation/openexr/pull/2450) + Update idna requirement from >=3.15 to >=3.17 in /website +* [2449](https://github.com/AcademySoftwareFoundation/openexr/pull/2449) + Bump vmactions/freebsd-vm from 1.4.5 to 1.4.6 +* [2437](https://github.com/AcademySoftwareFoundation/openexr/pull/2437) + Bump github/codeql-action from 4.35.4 to 4.36.0 +* [2433](git@github.com:AcademySoftwareFoundation/openexr/pull/2433) + Tighten python publish workflow security/efficiency +* [2430](git@github.com:AcademySoftwareFoundation/openexr/pull/2430) + Pin idna>=3.15 in website/requirements.txt + + ## Version 3.4.12 (May 24, 2026) Patch release that addresses several bugs and security @@ -1035,6 +1125,34 @@ Fetch master branch of libdeflate on main * [1852](https://github.com/AcademySoftwareFoundation/openexr/pull/1852) Add an option to use TBB as the global provider +## Version 3.3.12 (June 21, 2026) + +This release addresses the following security vulnerabilities: + +* [CVE-2026-55373](https://www.cve.org/CVERecord?id=CVE-2026-55373) + OpenEXRUtil SampleCountChannel endEdit() can loop forever on UINT_MAX sample counts +* [CVE-2026-55059](https://www.cve.org/CVERecord?id=CVE-2026-55059) + OpenEXRUtil SampleCountChannel row setter heap out-of-bounds write +* [CVE-2026-54920](https://www.cve.org/CVERecord?id=CVE-2026-54920) + Integer Overflow and Use of Uninitialized Pointer leading to Invalid Delete in OpenEXRUtil Image Resize + +### Merged Pull Requests + + * [2476](git@github.com:AcademySoftwareFoundation/openexr/pull/2476) +Fix the DWAA size checks regression + * [2472](git@github.com:AcademySoftwareFoundation/openexr/pull/2472) +Unlock write context in exr_get_chunk_table_offset() return paths + * [2468](git@github.com:AcademySoftwareFoundation/openexr/pull/2468) +Fix infinite loop in SampleCountChannel roundListSizeUp() + * [2464](git@github.com:AcademySoftwareFoundation/openexr/pull/2464) +Fix heap out-of-bounds write in SampleCountChannel row setter. + * [2463](git@github.com:AcademySoftwareFoundation/openexr/pull/2463) +Fix integer overflow and invalid delete in OpenEXRUtil Image::resize() + * [2442](git@github.com:AcademySoftwareFoundation/openexr/pull/2442) +Bugfix: Fix interger overflows + * [2438](git@github.com:AcademySoftwareFoundation/openexr/pull/2438) +Break release.py into separate small scripts for each step + ## Version 3.3.11 (April 29, 2026) Patch release for 3.3 that addresses the following security @@ -1760,6 +1878,36 @@ Fix macOS arm64 build Propagate dwa core 3 1 * [1418](https://github.com/AcademySoftwareFoundation/openexr/pull/1418) +## Version 3.2.10 (June 21, 2026) + +Patch release for 3.2 addressing security issues. + +This release addresses the following security vulnerabilities: + +* [CVE-2026-55373](https://www.cve.org/CVERecord?id=CVE-2026-55373) + OpenEXRUtil SampleCountChannel endEdit() can loop forever on UINT_MAX sample counts +* [CVE-2026-55059](https://www.cve.org/CVERecord?id=CVE-2026-55059) + OpenEXRUtil SampleCountChannel row setter heap out-of-bounds write +* [CVE-2026-54920](https://www.cve.org/CVERecord?id=CVE-2026-54920) + Integer Overflow and Use of Uninitialized Pointer leading to Invalid Delete in OpenEXRUtil Image Resize + +### Merged Pull Requests + + * [2476](git@github.com:AcademySoftwareFoundation/openexr/pull/2476) +Fix the DWAA size checks regression + * [2472](git@github.com:AcademySoftwareFoundation/openexr/pull/2472) +Unlock write context in exr_get_chunk_table_offset() return paths + * [2468](git@github.com:AcademySoftwareFoundation/openexr/pull/2468) +Fix infinite loop in SampleCountChannel roundListSizeUp() + * [2464](git@github.com:AcademySoftwareFoundation/openexr/pull/2464) +Fix heap out-of-bounds write in SampleCountChannel row setter. + * [2463](git@github.com:AcademySoftwareFoundation/openexr/pull/2463) +Fix integer overflow and invalid delete in OpenEXRUtil Image::resize() + * [2442](git@github.com:AcademySoftwareFoundation/openexr/pull/2442) +Bugfix: Fix interger overflows + * [2438](git@github.com:AcademySoftwareFoundation/openexr/pull/2438) +Break release.py into separate small scripts for each step + ## Version 3.2.9 (April 29, 2026) Patch release for 3.2 that addresses the following security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e9518a315..9225172a42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ explains our contribution process and procedures: * [Versioning Policy](#Versioning-Policy) * [Contributing to the Website](#Contributing-to-the-Website) * [Creating a Patch Release](#Creating-a-Patch-Release) -* [Creating a Major/Minor Release](#Creating-a-Major/Minor-Release) +* [Creating a Major/Minor Release](#creating-a-majorminor-release) For a description of the roles and responsibilities of the various members of the OpenEXR community, see [GOVERNANCE](GOVERNANCE.md), and @@ -241,7 +241,7 @@ who may discuss, offer constructive feedback, request changes, or approve the work. 6. Upon receiving the required number of committer approvals (as -outlined in [Required Approvals](#required-approvals)), a committer +outlined in [Required Approvals](#code-review-and-required-approvals)), a committer other than the PR contributor may merge changes into the ``main`` branch. @@ -479,7 +479,7 @@ target. Generation is off by default. Building the website requires that ``sphinx``, ``breathe``, and ``doxygen`` are installed. It further requires the [sphinx-press-theme] (https://pypi.org/project/sphinx-press-theme). Complete dependencies -are described in the [requirements.txt](website/requirements.txtg) +are described in the [requirements.txt](website/requirements.txt) file. On Debian/Ubuntu Linux: diff --git a/MODULE.bazel b/MODULE.bazel index 32199806cd..70c5018857 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -11,4 +11,4 @@ bazel_dep(name = "imath", version = "3.2.2.bcr.1") bazel_dep(name = "libdeflate", version = "1.25") bazel_dep(name = "openjph", version = "0.27.3.bcr.1") bazel_dep(name = "platforms", version = "1.1.0") -bazel_dep(name = "rules_cc", version = "0.2.19") +bazel_dep(name = "rules_cc", version = "0.2.22") diff --git a/README.md b/README.md index 7c9ec69607..501e8084b9 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ package. OpenEXR is a project of the [Academy Software Foundation](https://www.aswf.io). See the project's [governance -policies](GOVERNANCE.md), [contribution guidelines](CONTRIBUTING.md), and [code of conduct](CODE_OF_CONDUCT) +policies](GOVERNANCE.md), [contribution guidelines](CONTRIBUTING.md), and [code of conduct](CODE_OF_CONDUCT.md) for more information. # Building OpenEXR diff --git a/SECURITY.md b/SECURITY.md index f613e5de98..c1503e2059 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,27 +18,46 @@ rapidly and post patches within 14 days if possible. ## CVE Assignment -We request a CVE when an untrusted party can plausibly trigger the flaw -through normal product inputs (for example, a crafted EXR or other data -the application is meant to process); otherwise we treat it as a regular -bugfix, document it in a GitHub security advisory or release notes when -helpful, and do not request a CVE for API-only or caller-controlled -failures with no realistic adversarial path. +Our criteria for requesting a CVE are: + +- Memory safety violations (out-of-bounds write, use-after-free, heap + corruption) reachable on mainstream 64-bit architectures through the + public library API, confirmed in an unsanitized build on a mainstream + platform, and affecting library code so that every downstream consumer + inherits the risk. + +We do not request a CVE for: + +- 32-bit-only bugs — bugs on 32-bit systems are real and will be fixed, + but since the vast majority of downstream packages (Linux distros, + conda-forge, vcpkg) build for 64-bit exclusively, a CVE creates + unnecessary work for maintainers who are not actually affected. + +- Developer-tool-only crashes (exrmetrics, exrcheck) — the library + itself and standard utility programs are safe; only users who run + one of the developer tools on untrusted input are exposed. Flaws whose root cause lies in a bundled dependency (such as OpenJPH) are fixed upstream; the upstream project owns the CVE when one is -warranted. Note that this applies even when a flaw is detected in -dependency code vendored into OpenEXR's `external/` source directory. -OpenEXR addresses them by updating the dependency and noting the -upstream advisory in release notes. We request a CVE for OpenEXR -itself only when the flaw is in our code and untrusted input can reach -it through normal use of the library. +warranted. This applies even when a flaw is detected in dependency code +vendored into OpenEXR's `external/` source directory. OpenEXR addresses +such flaws by updating the dependency and noting the upstream advisory +in the release notes. We request a CVE for OpenEXR itself only when the +flaw is in our code and untrusted input can reach it through normal use +of the library. ## Known Vulnerabilities | CVE | Affected Versions | Patched Versions | | --- | ----------------- | ---------------- | +| [CVE-2026-55373](https://www.cve.org/CVERecord?id=CVE-2026-55373) | 2.x, 3.0.x, 3.1.x, 3.2.0–3.2.9, 3.3.0–3.3.11, 3.4.0–3.4.12 | 3.2.10, 3.3.12, 3.4.13 | +| [CVE-2026-55371](https://www.cve.org/CVERecord?id=CVE-2026-55371) | 3.4.0–3.4.12 | 3.4.13 | +| [CVE-2026-55059](https://www.cve.org/CVERecord?id=CVE-2026-55059) | 2.x, 3.0.x, 3.1.x, 3.2.0–3.2.9, 3.3.0–3.3.11, 3.4.0–3.4.12 | 3.2.10, 3.3.12, 3.4.13 | +| [CVE-2026-54920](https://www.cve.org/CVERecord?id=CVE-2026-54920) | 2.x, 3.0.x, 3.1.x, 3.2.0–3.2.9, 3.3.0–3.3.11, 3.4.0–3.4.12 | 3.2.10, 3.3.12, 3.4.13 | +| [CVE-2026-53532](https://www.cve.org/CVERecord?id=CVE-2026-53532) | 3.4.0–3.4.12 | 3.4.13 | +| [CVE-2026-45696](https://www.cve.org/CVERecord?id=CVE-2026-45696) | 3.4.0–3.4.11 | 3.4.12 | +| [CVE-2026-44663](https://www.cve.org/CVERecord?id=CVE-2026-44663) | 3.4.0–3.4.11 | 3.4.12 | | [CVE-2026-42217](https://www.cve.org/CVERecord?id=CVE-2026-42217) | 3.2.0–3.2.8, 3.3.0–3.3.10, 3.4.0–3.4.10 | 3.2.9, 3.3.11, 3.4.11 | | [CVE-2026-42216](https://www.cve.org/CVERecord?id=CVE-2026-42216) | 3.2.0–3.2.8, 3.3.0–3.3.10, 3.4.0–3.4.10 | 3.2.9, 3.3.11, 3.4.11 | | [CVE-2026-41142](https://www.cve.org/CVERecord?id=CVE-2026-41142) | 3.2.0–3.2.8, 3.3.0–3.3.10, 3.4.0–3.4.10 | 3.2.9, 3.3.11, 3.4.11 | diff --git a/pyproject.toml b/pyproject.toml index d966f6f9c4..18267dc070 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,9 +82,9 @@ test-extras = ["test"] test-skip = ["*universal2:arm64"] build-verbosity = 1 -manylinux-x86_64-image = "manylinux2014" -manylinux-i686-image = "manylinux2014" -manylinux-aarch64-image = "manylinux2014" +manylinux-x86_64-image = "manylinux_2_28" +manylinux-i686-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" # Needed for full C++17 support [tool.cibuildwheel.macos.environment] diff --git a/src/bin/exrmetrics/exrmetrics.cpp b/src/bin/exrmetrics/exrmetrics.cpp index 3258d5151a..a1b3d4d968 100644 --- a/src/bin/exrmetrics/exrmetrics.cpp +++ b/src/bin/exrmetrics/exrmetrics.cpp @@ -52,6 +52,25 @@ using std::vector; using std::chrono::steady_clock; using std::isinf; +namespace { + +// +// Validate that count*sampleSize casts to size_t without overflow, before +// using the value to resize a std::vector. +// + +size_t +vectorSize (uint64_t count, int sampleSize = 1) +{ + const uint64_t sampleSizeU = static_cast (sampleSize); + const uint64_t maxSize = std::numeric_limits::max (); + if (sampleSizeU > 0 && count > maxSize / sampleSizeU) + throw IEX_NAMESPACE::OverflowExc ("Integer multiplication overflow."); + return static_cast (count * sampleSizeU); +} + +} // namespace + double timing (steady_clock::time_point start, steady_clock::time_point end) { @@ -86,8 +105,6 @@ initScanLine ( int numChans = channelCount (in.header ()); pixelData.resize (numChans); - uint64_t offsetToOrigin = width * static_cast (dw.min.y) + - static_cast (dw.min.x); int channelNumber = 0; size_t rawSize = 0; @@ -100,13 +117,14 @@ initScanLine ( size_t pixelsInChannel = (width / i.channel ().xSampling) * (height / i.channel ().ySampling); rawSize += pixelsInChannel * samplesize; - pixelData[channelNumber].resize (numPixels * samplesize); + pixelData[channelNumber].resize (vectorSize (numPixels, samplesize)); buf.insert ( i.name (), - Slice ( + Slice::Make ( i.channel ().type, - pixelData[channelNumber].data () - offsetToOrigin * samplesize, + pixelData[channelNumber].data (), + dw, samplesize, samplesize * width, i.channel ().xSampling, @@ -212,7 +230,7 @@ initTiled ( { int samplesize = pixelTypeSize (i.channel ().type); pixelData[levelIndex][channelNumber].resize ( - numPixels * samplesize); + vectorSize (numPixels, samplesize)); buf[levelIndex].insert ( i.name (), @@ -331,7 +349,7 @@ initAndReadDeepScanLine ( uint64_t height = dw.max.y + 1 - dw.min.y; uint64_t numPixels = width * height; int numChans = channelCount (in.header ()); - sampleCount.resize (numPixels); + sampleCount.resize (vectorSize (numPixels)); uint64_t offsetToOrigin = width * static_cast (dw.min.y) + static_cast (dw.min.x); @@ -349,7 +367,7 @@ initAndReadDeepScanLine ( i != outHeader.channels ().end (); ++i) { - pixelPtrs[channelNumber].resize (numPixels); + pixelPtrs[channelNumber].resize (vectorSize (numPixels)); int samplesize = pixelTypeSize (i.channel ().type); buf.insert ( i.name (), @@ -499,7 +517,7 @@ initAndReadDeepTiled ( static_cast (dw.min.x); pixelPtrs.resize (numChans); - sampleCount.resize (numPixels); + sampleCount.resize (vectorSize (numPixels)); buf.insertSampleCountSlice (Slice ( UINT, @@ -513,7 +531,7 @@ initAndReadDeepTiled ( i != outHeader.channels ().end (); ++i) { - pixelPtrs[channelNumber].resize (numPixels); + pixelPtrs[channelNumber].resize (vectorSize (numPixels)); int samplesize = pixelTypeSize (i.channel ().type); buf.insert ( i.name (), diff --git a/src/bin/exrmetrics/main.cpp b/src/bin/exrmetrics/main.cpp index 866889d8ea..76dd34c107 100644 --- a/src/bin/exrmetrics/main.cpp +++ b/src/bin/exrmetrics/main.cpp @@ -58,6 +58,7 @@ usageMessage (ostream& stream, const char* program_name, bool verbose = false) "\n" " -o file file to write to. If no file specified, uses a memory buffer\n" " note: file may be overwritten multiple times during tests\n" + "\n" " -p n part number to copy, or \"all\" for all parts\n" " default is \"all\" \n" "\n" @@ -73,35 +74,44 @@ usageMessage (ostream& stream, const char* program_name, bool verbose = false) << ",orig,all\n" " default orig: retains original method)\n" + "\n" " --convert shorthand options for writing a new file with no metrics:\n" - " -p all --type orig --time none --type orig --no-size --passes 1\n" - " change pixel type or compression by specifying --type or -z after --convert\n" + " -p all --time none --pixelmode orig --no-size --passes 1\n" + " change pixel data type/compression by specifying --pixelmode/-z after --convert\n" + "\n" " --bench shorthand options for robust performance benchmarking:\n" " -p all --compression all --time write,reread --passes 10 --type half,float --no-size --csv\n" "\n" " -16 rgba|all [DEPRECATED] force 16 bit half float: either just RGBA, or all channels\n" - " Use --type half or --type mixed instead\n" - " --pixelmode list list of pixel types to use (float,half,mixed,orig)\n" + " Use --pixelmode half or --pixelmode mixed instead\n" + "\n" + " --pixelmode list list of pixel data types to use (float,half,mixed,orig)\n" " mixed uses half for RGBA, float for others. Default is 'orig'\n" - " --time list comma separated list of operations to report timing for.\n" + "\n" + " --time list comma separated list of operations to report timing for.\n" " operations can be any of read,write,reread (use --time none for no timing)\n" - " --no-size don't output size data\n" - " --part-disk-size Output the on-disk size of the data portion of each part in a multipart EXR file\n" + "\n" + " --no-size don't output size data\n" + "\n" + " --part-disk-size Output the on-disk size of the data portion of each part in a multipart EXR file\n" " (including chunk headers). File output (-o) is not supported when using this option\n" - " --json print output as JSON dictionary (Default mode)\n" - " --csv print output in csv mode. If passes>1, show median timing\n" + "\n" + " --json print output as JSON dictionary (Default mode)\n" + "\n" + " --csv print output in csv mode. If passes>1, show median timing\n" " default is JSON mode\n" + "\n" " --passes num write and re-read file num times (default 1)\n" - " --distortion compute LogMSE per part, comparing original vs. re-read after compression.\n" + " --distortion compute LogMSE per part, comparing original vs. re-read after compression.\n" " Parts must have uniform half or float channel types. Samples that are non-finite\n" " in the original are skipped. Samples that are finite in the original and not finite\n" " in the re-read result in Nan." "\n" " -h, --help print this message\n" + "\n" " -v output progress messages\n" "\n" - " --version print version information\n" - "\n"; + " --version print version information\n"; } } diff --git a/src/bin/exrmultipart/exrmultipart.cpp b/src/bin/exrmultipart/exrmultipart.cpp index 885197dc60..7c5fc8f035 100644 --- a/src/bin/exrmultipart/exrmultipart.cpp +++ b/src/bin/exrmultipart/exrmultipart.cpp @@ -32,7 +32,9 @@ #include #include #include +#include #include +#include #include #include #include // pair @@ -51,6 +53,31 @@ using namespace OPENEXR_IMF_NAMESPACE; # define IMF_PATH_SEPARATOR "/" #endif +namespace { + +// +// Validate that count * samplesize fits in size_t before resizing a vector. +// + +size_t +vectorSize (int64_t count, size_t samplesize) +{ + if (count < 0) + throw IEX_NAMESPACE::OverflowExc ("invalid vector size"); + + const size_t size_t_max = std::numeric_limits::max (); + if (sizeof (size_t) < sizeof (int64_t) && count > static_cast (size_t_max)) + throw IEX_NAMESPACE::OverflowExc ("invalid vector size"); + + const size_t c = static_cast (count); + if (samplesize > 0 && c > size_t_max / samplesize) + throw IEX_NAMESPACE::OverflowExc ("invalid vector size"); + + return c * samplesize; +} + +} // namespace + void copy_tile ( MultiPartInputFile& input, @@ -323,7 +350,7 @@ convert ( // compute size of channel size_t samplesize = sizeof (float); if (chan.channel ().type == HALF) { samplesize = sizeof (half); } - channelstore[i].resize (samplesize * pixel_count); + channelstore[i].resize (vectorSize (pixel_count, samplesize)); output_framebuffers[part].insert ( output_channels[i].name, diff --git a/src/bin/exrmultiview/makeMultiView.cpp b/src/bin/exrmultiview/makeMultiView.cpp index 1beebfef12..c78a06bbeb 100644 --- a/src/bin/exrmultiview/makeMultiView.cpp +++ b/src/bin/exrmultiview/makeMultiView.cpp @@ -107,6 +107,14 @@ makeMultiView ( string inChanName = j.name (); string outChanName = insertViewName (inChanName, viewNames, i); + if (inChannel.xSampling != 1 || inChannel.ySampling != 1) + THROW (IEX_NAMESPACE::ArgExc, + "no support for subsampling:" + << " channel " << inChanName + << " of " << inFileNames[i] + << " has subsampling " << inChannel.xSampling + << ", " << inChannel.ySampling); + image.addChannel (outChanName, inChannel); image.channel (outChanName).black (); diff --git a/src/lib/OpenEXR/ImfArray.h b/src/lib/OpenEXR/ImfArray.h index 395fc67e56..2182120e0a 100644 --- a/src/lib/OpenEXR/ImfArray.h +++ b/src/lib/OpenEXR/ImfArray.h @@ -8,8 +8,11 @@ #include "ImfForward.h" #include "IexBaseExc.h" +#include "IexMathExc.h" #include +#include +#include //------------------------------------------------------------------------- // @@ -160,6 +163,21 @@ template class IMF_EXPORT_TEMPLATE_TYPE Array2D // Implementation //--------------- +namespace { + +inline size_t +array2DElementCount (long sizeX, long sizeY) +{ + const uint64_t x = static_cast (sizeX); + const uint64_t y = static_cast (sizeY); + const uint64_t maxSize = std::numeric_limits::max (); + if (y > 0 && x > maxSize / y) + throw IEX_NAMESPACE::OverflowExc ("Array2D dimensions too large"); + return static_cast (x * y); +} + +} // namespace + template inline void Array::resizeErase (long size) @@ -202,7 +220,7 @@ inline Array2D::Array2D (long sizeX, long sizeY) _sizeX = sizeX; _sizeY = sizeY; - _data = new T[(size_t) sizeX * (size_t) sizeY]; + _data = new T[array2DElementCount (sizeX, sizeY)]; } template inline Array2D::~Array2D () @@ -231,7 +249,7 @@ Array2D::resizeErase (long sizeX, long sizeY) if (sizeX < 0 || sizeY < 0) throw IEX_NAMESPACE::ArgExc ("Array2D dimensions must be non-negative"); - T* tmp = new T[(size_t) sizeX * (size_t) sizeY]; + T* tmp = new T[array2DElementCount (sizeX, sizeY)]; delete[] _data; _sizeX = sizeX; _sizeY = sizeY; @@ -249,7 +267,7 @@ Array2D::resizeEraseUnsafe (long sizeX, long sizeY) _data = 0; _sizeX = 0; _sizeY = 0; - _data = new T[(size_t) sizeX * (size_t) sizeY]; + _data = new T[array2DElementCount (sizeX, sizeY)]; _sizeX = sizeX; _sizeY = sizeY; } diff --git a/src/lib/OpenEXR/ImfTiledMisc.cpp b/src/lib/OpenEXR/ImfTiledMisc.cpp index 9b5f816bee..38ba8929c7 100644 --- a/src/lib/OpenEXR/ImfTiledMisc.cpp +++ b/src/lib/OpenEXR/ImfTiledMisc.cpp @@ -378,7 +378,9 @@ getTiledChunkOffsetTableSize (const Header& header) "Bad level mode getting chunk offset table size"); } delete[] numXTiles; + numXTiles = nullptr; delete[] numYTiles; + numYTiles = nullptr; return static_cast (lineOffsetSize); } diff --git a/src/lib/OpenEXRCore/channel_list.c b/src/lib/OpenEXRCore/channel_list.c index 61fc89acc4..9165d6b026 100644 --- a/src/lib/OpenEXRCore/channel_list.c +++ b/src/lib/OpenEXRCore/channel_list.c @@ -173,7 +173,17 @@ exr_attr_chlist_add_with_length ( if (newcount > clist->num_alloced) { - int nsz = clist->num_alloced * 2; + int nsz; + + /* perhaps an arbitrary limit, but that is a LOT of channels */ + if (clist->num_alloced >= (INT32_MAX / 2)) + { + exr_attr_string_destroy (ctxt, &(nent.name)); + return ctxt->standard_error (ctxt, EXR_ERR_OUT_OF_MEMORY); + } + + nsz = clist->num_alloced * 2; + if (newcount > nsz) nsz = newcount + 1; nlist = (exr_attr_chlist_entry_t*) ctxt->alloc_fn ( sizeof (*nlist) * (size_t) nsz); diff --git a/src/lib/OpenEXRCore/chunk.c b/src/lib/OpenEXRCore/chunk.c index b4f79a4988..f8ab3e9d6d 100644 --- a/src/lib/OpenEXRCore/chunk.c +++ b/src/lib/OpenEXRCore/chunk.c @@ -1006,7 +1006,7 @@ exr_read_scanline_chunk_info ( if (rv != EXR_ERR_SUCCESS) { return rv; } priv_to_native64 (ddata, 3); - if (ddata[0] < 0) + if (ddata[0] < 0 || ddata[0] > (int64_t) INT_MAX) { return ctxt->print_error ( ctxt, @@ -1120,6 +1120,17 @@ exr_read_scanline_chunk_info ( if (cinfo->packed_size == 0 && cinfo->unpacked_size > 0) return ctxt->report_error ( ctxt, EXR_ERR_INVALID_ARGUMENT, "Invalid packed size of 0"); + + if (part->comp_type == EXR_COMPRESSION_NONE && + cinfo->packed_size != cinfo->unpacked_size) + { + return ctxt->print_error ( + ctxt, + EXR_ERR_BAD_CHUNK_LEADER, + "Mismatch between unpacked and packed size with uncompressed data: packed is %" PRIu64 "; unpacked is %" PRIu64, + cinfo->packed_size, cinfo->unpacked_size); + } + return EXR_ERR_SUCCESS; } diff --git a/src/lib/OpenEXRCore/internal_b44.c b/src/lib/OpenEXRCore/internal_b44.c index 99d35a7fdd..f30762ccd8 100644 --- a/src/lib/OpenEXRCore/internal_b44.c +++ b/src/lib/OpenEXRCore/internal_b44.c @@ -694,12 +694,14 @@ internal_exr_undo_b44 ( uint64_t uncompressed_size) { exr_result_t rv; + uint64_t scratch_sz = compute_scratch_buffer_size (decode, uncompressed_size); + if (scratch_sz != (size_t) scratch_sz) return EXR_ERR_OUT_OF_MEMORY; rv = internal_decode_alloc_buffer ( decode, EXR_TRANSCODE_BUFFER_SCRATCH1, &(decode->scratch_buffer_1), &(decode->scratch_alloc_size_1), - compute_scratch_buffer_size (decode, uncompressed_size)); + (size_t) scratch_sz); if (rv != EXR_ERR_SUCCESS) return rv; exrcore_ensure_b44_tables (); @@ -721,12 +723,14 @@ internal_exr_undo_b44a ( uint64_t uncompressed_size) { exr_result_t rv; + uint64_t scratch_sz = compute_scratch_buffer_size (decode, uncompressed_size); + if (scratch_sz != (size_t) scratch_sz) return EXR_ERR_OUT_OF_MEMORY; rv = internal_decode_alloc_buffer ( decode, EXR_TRANSCODE_BUFFER_SCRATCH1, &(decode->scratch_buffer_1), &(decode->scratch_alloc_size_1), - compute_scratch_buffer_size (decode, uncompressed_size)); + (size_t) scratch_sz); if (rv != EXR_ERR_SUCCESS) return rv; exrcore_ensure_b44_tables (); diff --git a/src/lib/OpenEXRCore/internal_dwa_compressor.h b/src/lib/OpenEXRCore/internal_dwa_compressor.h index e9b7123523..1fe256e4b5 100644 --- a/src/lib/OpenEXRCore/internal_dwa_compressor.h +++ b/src/lib/OpenEXRCore/internal_dwa_compressor.h @@ -873,18 +873,21 @@ DwaCompressor_uncompress ( if (unknownCompressedSize > 0) { + size_t actualUnknown; + if (unknownUncompressedSize > me->_planarUncBufferSize[UNKNOWN]) { return EXR_ERR_CORRUPT_CHUNK; } - if (EXR_ERR_SUCCESS != exr_uncompress_buffer ( - me->_decode->context, - compressedUnknownBuf, - unknownCompressedSize, - me->_planarUncBuffer[UNKNOWN], - unknownUncompressedSize, - NULL)) + rv = exr_uncompress_buffer ( + me->_decode->context, + compressedUnknownBuf, + unknownCompressedSize, + me->_planarUncBuffer[UNKNOWN], + unknownUncompressedSize, + &actualUnknown); + if (rv != EXR_ERR_SUCCESS || actualUnknown != unknownUncompressedSize) { return EXR_ERR_CORRUPT_CHUNK; } diff --git a/src/lib/OpenEXRCore/internal_zip.c b/src/lib/OpenEXRCore/internal_zip.c index f7aa6433f3..5fd5eef8c0 100644 --- a/src/lib/OpenEXRCore/internal_zip.c +++ b/src/lib/OpenEXRCore/internal_zip.c @@ -313,7 +313,7 @@ undo_zip_impl ( if (res == EXR_ERR_SUCCESS) { decode->bytes_decompressed = actual_out_bytes; - if (comp_buf_size > actual_out_bytes || actual_out_bytes > uncompressed_size) + if (actual_out_bytes != uncompressed_size) res = EXR_ERR_CORRUPT_CHUNK; else internal_zip_reconstruct_bytes ( diff --git a/src/lib/OpenEXRCore/parse_header.c b/src/lib/OpenEXRCore/parse_header.c index 575707d0ec..af0fd25698 100644 --- a/src/lib/OpenEXRCore/parse_header.c +++ b/src/lib/OpenEXRCore/parse_header.c @@ -557,7 +557,7 @@ extract_attr_float_vector ( tname); } - priv_to_native32 (attrdata, n); + priv_to_native32 (attrdata->arr, n); } return rv; diff --git a/src/test/OpenEXRCoreTest/general_attr.cpp b/src/test/OpenEXRCoreTest/general_attr.cpp index c11be86f0f..4299220313 100644 --- a/src/test/OpenEXRCoreTest/general_attr.cpp +++ b/src/test/OpenEXRCoreTest/general_attr.cpp @@ -803,6 +803,22 @@ testChlistHelper (exr_context_t f) // make sure we can re-delete something? EXRCORE_TEST_RVAL (exr_attr_chlist_destroy (f, &cl)); + + for ( int curc = 0; curc < 384; ++curc ) + { + char cname[32]; + /* use zero pad to avoid dealing with lexical ordering of channels */ + snprintf (cname, 32, "c%03d", curc); + EXRCORE_TEST_RVAL (exr_attr_chlist_add ( + f, &cl, cname, EXR_PIXEL_HALF, EXR_PERCEPTUALLY_LINEAR, 1, 2)); + EXRCORE_TEST (cl.num_channels == (curc + 1)); + EXRCORE_TEST (0 == strcmp (cl.entries[curc].name.str, cname)); + EXRCORE_TEST (cl.entries[curc].pixel_type == EXR_PIXEL_HALF); + EXRCORE_TEST (cl.entries[curc].p_linear == (uint8_t) EXR_PERCEPTUALLY_LINEAR); + EXRCORE_TEST (cl.entries[curc].x_sampling == 1); + EXRCORE_TEST (cl.entries[curc].y_sampling == 2); + } + EXRCORE_TEST_RVAL (exr_attr_chlist_destroy (f, &cl)); } void diff --git a/src/test/OpenEXRTest/testChannels.cpp b/src/test/OpenEXRTest/testChannels.cpp index 17c8fa1b47..2217a83f63 100644 --- a/src/test/OpenEXRTest/testChannels.cpp +++ b/src/test/OpenEXRTest/testChannels.cpp @@ -45,7 +45,8 @@ writeRead ( const Array2D& h2out, const char fileName[], int width, - int height) + int height, + bool manyC) { // // Write an image file with three channels, H1, H2 and H3. @@ -78,6 +79,17 @@ writeRead ( 1, // xSampling 1) // ySampling ); + if (manyC) + { + for ( int curc = 0; curc < 132; ++curc ) + { + char cname[32]; + snprintf (cname, 32, "c%03d", curc); + hdr.channels ().insert ( + cname, + Channel (HALF, 1, 1)); + } + } { FrameBuffer fb; @@ -104,7 +116,10 @@ writeRead ( 1) // ySampling ); - cout << "writing" << flush; + if (manyC) + cout << "Many Channels: writing" << flush; + else + cout << "Values: writing" << flush; remove (fileName); OutputFile out (fileName, hdr); @@ -213,6 +228,41 @@ writeRead ( assert (h4in[y][x] == 3.0); } } + + if (manyC) + { + for ( int curc = 0; curc < 132; ++curc ) + { + char cname[32]; + snprintf (cname, 32, "c%03d", curc); + + cout << " " << curc << flush; + Array2D testcin (h, w); + + FrameBuffer fb; + + /* if the channel isn't there, will fill with 3 but we should have 0 */ + fb.insert ( + cname, // name + Slice ( + HALF, // type + (char*) &testcin[-dy][-dx], // base + sizeof (testcin[0][0]), // xStride + sizeof (testcin[0][0]) * w, // yStride + 1, 1, 3.0) + ); + + in.setFrameBuffer (fb); + in.readPixels (dw.min.y, dw.max.y); + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + assert (testcin[y][x] == 0.0); + } + } + } + } } remove (fileName); @@ -306,7 +356,8 @@ testChannels (const std::string& tempDir) std::string filename = tempDir + "imf_test_channels.exr"; - writeRead (ph1, ph2, filename.c_str (), W, H); + writeRead (ph1, ph2, filename.c_str (), W, H, false); + writeRead (ph1, ph2, filename.c_str (), W, H, true); cout << "ok\n" << endl; } diff --git a/src/wrappers/python/PyOpenEXR.cpp b/src/wrappers/python/PyOpenEXR.cpp index add43ec8c7..458af61699 100644 --- a/src/wrappers/python/PyOpenEXR.cpp +++ b/src/wrappers/python/PyOpenEXR.cpp @@ -431,6 +431,9 @@ PyFile::readPartsFromOpenInput(bool separate_channels) std::vector shape ({height, width}); + if (!separate_channels) + P.validateCoalescedChannelTypes (header.channels (), rgbaChannels); + // // Read the channel data, different for image vs. deep // @@ -448,6 +451,10 @@ PyFile::readPartsFromOpenInput(bool separate_channels) } parts.append(py::cast(PyPart(P))); } + catch (const std::invalid_argument&) + { + throw; + } catch (const std::exception& e) { // Log the error and skip appending this part @@ -467,6 +474,9 @@ PyPart::readPixels(MultiPartInputFile& infile, const ChannelList& channel_list, const std::vector& shape, const std::set& rgbaChannels, const Box2i& dw, bool separate_channels) { + if (!separate_channels) + validateCoalescedChannelTypes (channel_list, rgbaChannels); + FrameBuffer frameBuffer; for (auto c = channel_list.begin(); c != channel_list.end(); c++) @@ -671,11 +681,12 @@ PyPart::setDeepSliceData(const ChannelList& channel_list, size_t height, size_t size_t channel_offset = 0; if (C._nrgba > 0) { - if (!strcmp(c.name(), "G")) + char last = c.name()[strlen(c.name()) - 1]; + if (last == 'G') channel_offset = 1; - else if (!strcmp(c.name(), "B")) + else if (last == 'B') channel_offset = 2; - else if (!strcmp(c.name(), "A")) + else if (last == 'A') channel_offset = 3; } @@ -719,6 +730,9 @@ PyPart::readDeepPixels(MultiPartInputFile& infile, const std::string& type, cons const std::vector& shape, const std::set& rgbaChannels, const Box2i& dw, bool separate_channels) { + if (!separate_channels) + validateCoalescedChannelTypes (channel_list, rgbaChannels); + size_t width = dw.max.x - dw.min.x + 1; size_t height = dw.max.y - dw.min.y + 1; auto dw_offset = dw.min.y * width + dw.min.x; @@ -1177,9 +1191,80 @@ PyPart::writeDeepPixels(MultiPartOutputFile& outfile, const Box2i& dw) const // channel_name is returned as the single character name of the channel // +namespace +{ + +const char* +pixelTypeName (PixelType type) +{ + switch (type) + { + case UINT: return "UINT"; + case HALF: return "HALF"; + case FLOAT: return "FLOAT"; + default: return "unknown"; + } +} + +} // namespace + +// +// Only combine RGB(A) channels into a single numpy array when they all have +// the same pixel type. For example, if red is HALF and green is FLOAT, an +// attempt to return an RGB pixel array will throw an exception. +// +// Note that attempting to combine mixed-type channels causes a failure of +// the entire file read, whereas other read errors simply skip the offending +// part. This is reasonable behavior since the condition is not a defect in +// the data itself, but simply an inability to return the data in the format +// the user requested. + +void +PyPart::validateCoalescedChannelTypes ( + const ChannelList& channel_list, + const std::set& rgbaChannels) const +{ + if (rgbaChannels.empty ()) + return; + + std::map groupType; + + for (auto c = channel_list.begin (); c != channel_list.end (); ++c) + { + if (rgbaChannels.find (c.name ()) == rgbaChannels.end ()) + continue; + + // py_channel_name is the name of the combined channel, i.e. if the + // file has left.R, left.G, left.B, then py_channel_name is "left". + // It's allowable to have "left" be FLOAT and "right" be HALF, but + // all RGB channels within "left" and "right" must have the same + // type. + + std::string py_channel_name; + char channel_name; + if (channelNameToRGBA (channel_list, c.name (), py_channel_name, channel_name) <= 0) + continue; + + const PixelType channelType = c.channel ().type; + auto it = groupType.find (py_channel_name); + if (it == groupType.end ()) + groupType[py_channel_name] = channelType; + else if (it->second != channelType) + { + std::stringstream err; + err << "cannot coalesce channels into \"" << py_channel_name + << "\": channel \"" << c.name () << "\" has pixel type " + << pixelTypeName (channelType) << " but other channels in the group " + << "have pixel type " << pixelTypeName (it->second) + << "; use separate_channels=True"; + throw std::invalid_argument (err.str ()); + } + } +} + int PyPart::channelNameToRGBA(const ChannelList& channel_list, const std::string& name, - std::string& py_channel_name, char& channel_name) + std::string& py_channel_name, char& channel_name) const { py_channel_name = name; channel_name = py_channel_name.back(); diff --git a/src/wrappers/python/PyOpenEXR.h b/src/wrappers/python/PyOpenEXR.h index eb374246bd..3588669daa 100644 --- a/src/wrappers/python/PyOpenEXR.h +++ b/src/wrappers/python/PyOpenEXR.h @@ -140,8 +140,12 @@ class PyPart const std::vector& shape, const std::set& rgbaChannels, const Box2i& dw, bool separate_channels); int channelNameToRGBA(const ChannelList& channel_list, const std::string& name, - std::string& py_channel_name, char& channel_name); - + std::string& py_channel_name, char& channel_name) const; + + void validateCoalescedChannelTypes( + const ChannelList& channel_list, + const std::set& rgbaChannels) const; + }; // diff --git a/src/wrappers/python/tests/test_deep.py b/src/wrappers/python/tests/test_deep.py index dc31ed3a78..14a9464ff2 100644 --- a/src/wrappers/python/tests/test_deep.py +++ b/src/wrappers/python/tests/test_deep.py @@ -224,6 +224,73 @@ def test_tiled_deep(self): os.remove(filename) + def test_mixed_type_rgb_coalesce_deep_rejected(self): + dataWindow = ((0, 0), (0, 0)) + height = width = 1 + + B = np.empty((height, width), dtype=object) + G = np.empty((height, width), dtype=object) + R = np.empty((height, width), dtype=object) + B[0, 0] = np.array([1.0, 2.0], dtype='float16') + G[0, 0] = np.array([1.0, 2.0], dtype='float32') + R[0, 0] = np.array([1.0, 2.0], dtype='float32') + + channels = {"B": B, "G": G, "R": R} + header = { + "compression": OpenEXR.ZIPS_COMPRESSION, + "type": OpenEXR.deepscanline, + "dataWindow": dataWindow, + } + + fd, path = tempfile.mkstemp(suffix=".exr") + os.close(fd) + try: + with OpenEXR.File(header, channels) as outfile: + outfile.write(path) + + with self.assertRaises(Exception) as ctx: + OpenEXR.File(path) + self.assertIn("separate_channels", str(ctx.exception)) + + with OpenEXR.File(path, separate_channels=True) as infile: + self.assertIn("B", infile.channels()) + self.assertIn("G", infile.channels()) + self.assertIn("R", infile.channels()) + finally: + if os.path.exists(path): + os.remove(path) + + def test_mixed_type_rgb_coalesce_flat_rejected(self): + dataWindow = ((0, 0), (0, 0)) + + channels = { + "B": np.array([[1.0]], dtype='float16'), + "G": np.array([[2.0]], dtype='float32'), + "R": np.array([[3.0]], dtype='float32'), + } + header = { + "type": OpenEXR.scanlineimage, + "dataWindow": dataWindow, + } + + fd, path = tempfile.mkstemp(suffix=".exr") + os.close(fd) + try: + with OpenEXR.File(header, channels) as outfile: + outfile.write(path) + + with self.assertRaises(Exception) as ctx: + OpenEXR.File(path) + self.assertIn("separate_channels", str(ctx.exception)) + + with OpenEXR.File(path, separate_channels=True) as infile: + self.assertIn("B", infile.channels()) + self.assertIn("G", infile.channels()) + self.assertIn("R", infile.channels()) + finally: + if os.path.exists(path): + os.remove(path) + if __name__ == '__main__': unittest.main() print("OK") diff --git a/website/latest_news_title.rst b/website/latest_news_title.rst index 7ebd5076d9..819cb831cb 100644 --- a/website/latest_news_title.rst +++ b/website/latest_news_title.rst @@ -1,4 +1,4 @@ .. SPDX-License-Identifier: BSD-3-Clause Copyright (c) Contributors to the OpenEXR Project. -.. |latest-news-title| replace:: **May 24, 2026 - OpenEXR 3.4.12 Released** \ No newline at end of file +.. |latest-news-title| replace:: **June 19, 2026 - OpenEXR 3.4.13 Released** \ No newline at end of file diff --git a/website/news.rst b/website/news.rst index 378956fc61..f7f4a06a83 100644 --- a/website/news.rst +++ b/website/news.rst @@ -17,13 +17,58 @@ News |latest-news-title| -====================================== +======================================= .. _LatestNewsStart: Patch release that addresses several bugs and security vulnerabilities. +* 🐛 Fix a regression introduced in v3.4.11 in decoding of DWAA compression +* 🐛 Fix to handling deep images and very large images with the OpenEXRUtil library +* 🐛 Fix initiliazation issue in B44A decoding +* 🐛 Validate HTJ2K chunk header length before decode +* 🛠️ Fix when building statically and using the vendored OpenJPH library + +For the python module: + +* 🐍 ✨ Support NumPy scalar values Box2i and V2f tuple bindings + +This release addresses the following security vulnerabilities: + +* `CVE-2026-55373 `_ + OpenEXRUtil ``SampleCountChannel`` ``endEdit()`` can loop forever on ``UINT_MAX`` sample counts +* `CVE-2026-55371 `_ + OpenEXRCore ``exr_attr_set_bytes()`` accepts NULL ``type_hint`` with positive ``hint_length`` +* `CVE-2026-55059 `_ + OpenEXRUtil ``SampleCountChannel`` row setter heap out-of-bounds write +* `CVE-2026-54920 `_ + Integer Overflow and Use of Uninitialized Pointer leading to Invalid Delete in OpenEXRUtil Image Resize +* `CVE-2026-53532 `_ + Unhandled assert abort in HTJ2K decoder via crafted QCD marker (DoS) + +.. _LatestNewsEnd: + +June 21, 2026 - OpenEXR 3.3.12 and OpenEXR v3.2.10 Released +=========================================================== + +Patch releases for v3.3 and v3.2 that address the following security +vulnerabilities: + +* `CVE-2026-55373 `_ + OpenEXRUtil ``SampleCountChannel`` ``endEdit()`` can loop forever on ``UINT_MAX`` sample counts +* `CVE-2026-55059 `_ + OpenEXRUtil ``SampleCountChannel`` row setter heap out-of-bounds write +* `CVE-2026-54920 `_ + Integer Overflow and Use of Uninitialized Pointer leading to Invalid Delete in OpenEXRUtil Image Resize + + +May 24, 2026 - OpenEXR 3.4.12 Released +====================================== + +Patch release that addresses several bugs and security +vulnerabilities. + * 🐛 Fix several minor memory leaks recovering from reading invalid files. * 🐛 The compressor API incorrectly identfied ``HTJ2K`` and ``HTJ2K256`` as @@ -58,7 +103,6 @@ This release addresses the following security vulnerabilities: * `OSS-fuzz 507413960 `_ Heap-buffer-overflow in ``generic_unpack`` -.. _LatestNewsEnd: April 29, 2026 - OpenEXR 3.4.11 Released ========================================