ANT-2026-3DSGRTDD · osgeo/gdal

heap-buffer-overflow high

Severity Claude high · Security research firm high · Maintainer -

Discovered by Claude Opus 4.6

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.

ANT-2026-3DSGRTDD: Heap-buffer-overflow in gbits.c:62

GDAL's GRIB2 driver calls gdal_comunpack (comunpack.c:150) to decode complex-packed data from Section 7, which in turn invokes gdal_gbits to extract bitfields from a packed byte buffer. That buffer is a 228-byte heap region allocated by ReadGrib2Record (degrib2.cpp:1008). With a crafted GRIB2 file, the bit offset/width parameters passed down from g2_unpack7 cause gdal_gbits to index one byte past the end of the allocation. An attacker who can supply a GRIB2 file to any GDAL-based application gets a 1-byte out-of-bounds heap read — primarily a denial-of-service, with marginal information-disclosure potential.

Target

Project: gdal
Location: gbits.c:62

Technical Details

ASAN: "READ of size 1 at 0x740e9e602fe4 ... 0 bytes after 228-byte region". gdal_gbits performs bit-level extraction from a packed byte array but does not validate that the computed byte offset stays within the bounds of the buffer supplied by gdal_comunpack. The offset and bit-width are derived from fields inside the GRIB2 message (Section 5/7 metadata), so a malformed file can push the read past the end of the realloc'd c_ipack buffer.

Crash trace (truncated — full trace in attached crash.log):

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3933186869
INFO: Loaded 1 modules   (1158073 inline 8-bit counters): 1158073 [0x5949ea59ba60, 0x5949ea6b6619), 
INFO: Loaded 1 PC tables (1158073 PCs): 1158073 [0x5949ea6b6620,0x5949eb8621b0), 
/out/gdal_filesystem_fuzzer: Running 1 inputs 1 time(s) each.
Running: /tmp/poc
EXIT_CODE:1


=== ASAN Report ===
=================================================================
==27==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x740e9e602fe4 at pc 0x5949e6203e4d bp 0x7ffc95821c60 sp 0x7ffc95821c58
READ of size 1 at 0x740e9e602fe4 thread T0
    #0 0x5949e6203e4c in gdal_gbits /src/gdal/frmts/grib/degrib/g2clib/gbits.c:62:54
    #1 0x5949e6203e4c in gdal_gbit /src/gdal/frmts/grib/degrib/g2clib/gbits.c:6:14
    #2 0x5949e62093d2 in gdal_comunpack /src/gdal/frmts/grib/degrib/g2clib/comunpack.c:150:15
    #3 0x5949e6207d6a in gdal_g2_unpack7 /src/gdal/frmts/grib/degrib/g2clib/g2_unpack7.c:122:13
    #4 0x5949e4ba41b7 in gdal_g2_getfld /src/gdal/frmts/grib/degrib/g2clib/g2_getfld.c:527:16
    #5 0x5949e4b951f3 in unpk_g2ncep /src/gdal/frmts/grib/degrib/degrib/grib2api.c:878:11
    #6 0x5949e4b64ed3 in ReadGrib2Record /src/gdal/frmts/grib/degrib/degrib/degrib2.cpp:1187:7
    #7 0x5949e4b20ec4 in GRIBRasterBand::ReadGribData(VSIVirtualHandle*, unsigned long long, int, double**, grib_MetaData**) /src/gdal/frmts/grib/gribdataset.cpp:1160:5
    [... 35 more frames — full trace in crash.log]

Reproduction

  1. Construct a GRIB2 message where Section 5 declares complex packing and Section 7 metadata (group widths/references/bit counts) implies more packed bits than the actual Section 7 payload contains
  2. Deliver the file to a target that reads raster data from it (e.g., gdalinfo -checksum, tile server, format conversion pipeline)
  3. GRIBRasterBand::IReadBlock triggers LoadData → ReadGrib2Record → g2_unpack7 → comunpack → gbits, which reads past the end of the c_ipack heap buffer

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-3DSGRTDD.


Reference: ANT-2026-3DSGRTDD
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Ada Logics.

Verdict
true positive
Severity
high
UPSTREAM FIX

The change that resolved this finding.

diff --git a/autotest/gdrivers/grib.py b/autotest/gdrivers/grib.py
index a9269453afae..72d8cfe1ddc0 100755
--- a/autotest/gdrivers/grib.py
+++ b/autotest/gdrivers/grib.py
@@ -13,6 +13,7 @@
 # SPDX-License-Identifier: MIT
 ###############################################################################
 
+import base64
 import os
 import shutil
 import struct
@@ -2438,3 +2439,17 @@ def test_grib_grib2_tmerc_negative_false_easting_false_northing(tmp_vsimem):
             "+proj=tmerc +lat_0=-1 +lon_0=-2 +k=1 +x_0=-300000 +y_0=-400000"
             in ds.GetSpatialRef().ExportToProj4()
         )
+
+
+@gdaltest.enable_exceptions()
+def test_grib_complex_unpacking_invalid_bits_per_packed_value(tmp_vsimem):
+
+    with gdal.VSIFile(tmp_vsimem / "src.grib2", "wb") as infile:
+        infile.write(
+            base64.b64decode(
+                "R1JJQgAAAAIAAAAAAAAA4gAAABUBAAcAAAIBAQfoAQEAAAAAAQAAAEgDAAAAABAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxBQAAABAAAwAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAQjyAAAAAAEAAAAQAAJbAAAABgb/AAAAGAcBookAAVwDBAUlBwgJCgUMDQ4PNzc3Nw=="
+            )
+        )
+
+    with pytest.raises(Exception, match="Error reading GRIB data"):
+        gdal.Translate("", tmp_vsimem / "src.grib2", format="MEM")
diff --git a/frmts/grib/degrib/g2clib/comunpack.c b/frmts/grib/degrib/g2clib/comunpack.c
index 5ec35ee48f6e..0638a0d70072 100644
--- a/frmts/grib/degrib/g2clib/comunpack.c
+++ b/frmts/grib/degrib/g2clib/comunpack.c
@@ -147,7 +147,7 @@ int comunpack(unsigned char *cpack,g2int cpack_length,g2int lensec,g2int idrsnum
       if (idrsnum == 3) {
          if (nbitsd != 0) {
 // one mistake here should be unsigned int
-              gbit(cpack,&ival1,iofst,nbitsd);
+              int ok = gbit2(cpack, cpack_length, &ival1, iofst,nbitsd) != -1;
               iofst=iofst+nbitsd;
 //              gbit(cpack,&isign,iofst,1);
 //              iofst=iofst+1
@@ -156,7 +156,7 @@ int comunpack(unsigned char *cpack,g2int cpack_length,g2int lensec,g2int idrsnum
 //              if (isign == 1) ival1=-ival1;
               if (idrstmpl[16] == 2) {
 // one mistake here should be unsigned int
-                 gbit(cpack,&ival2,iofst,nbitsd);
+                 ok = ok && gbit2(cpack, cpack_length, &ival2,iofst,nbitsd) != -1;
                  iofst=iofst+nbitsd;
 //                 gbit(cpack,&isign,iofst,1);
 //                 iofst=iofst+1;
@@ -164,10 +164,19 @@ int comunpack(unsigned char *cpack,g2int cpack_length,g2int lensec,g2int idrsnum
 //                 iofst=iofst+nbitsd-1;
 //                 if (isign == 1) ival2=-ival2;
               }
-              gbit(cpack,&isign,iofst,1);
+              ok = ok && gbit2(cpack, cpack_length, &isign,iofst,1) != -1;
               iofst=iofst+1;
-              gbit(cpack,&minsd,iofst,nbitsd-1);
+              ok = ok && gbit2(cpack, cpack_length, &minsd,iofst,nbitsd-1) != - 1;
               iofst=iofst+nbitsd-1;
+
+              if (!ok)
+              {
+                  free(ifld);
+                  free(gref);
+                  free(gwidth);
+                  return -1;
+              }
+
               if (isign == 1 && minsd != INT_MIN) minsd=-minsd;
          }
          else {
diff --git a/frmts/grib/gribdataset.cpp b/frmts/grib/gribdataset.cpp
index b0be93a0f5c2..7e778af38ebc 100644
--- a/frmts/grib/gribdataset.cpp
+++ b/frmts/grib/gribdataset.cpp
@@ -847,7 +847,7 @@ CPLErr GRIBRasterBand::LoadData()
         ReadGribData(poGDS->fp, start, subgNum, &m_Grib_Data, &m_Grib_MetaData);
         if (!m_Grib_Data)
         {
-            CPLError(CE_Failure, CPLE_AppDefined, "Out of memory.");
+            CPLError(CE_Failure, CPLE_AppDefined, "Error reading GRIB data.");
             if (m_Grib_MetaData != nullptr)
             {
                 MetaFree(m_Grib_MetaData);

https://github.com/OSGeo/gdal/commit/60d61b5181a6288ebc29624fbac29c81f92c9e7c

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-24 Reported to tracker
  2. 2026-05-11 Sent to maintainer
  3. 2026-05-13 Maintainer acknowledged
  4. 2026-06-05 Patch released
  5. 2026-07-20 Publicly revealed
PROVENANCE

SHA-3-512 hash:

b6a6a1804140a36fe0508887afae650be2caf1685e7440aaf27e56df624712155cfeecb3f3c13f3d060ecbe2605e547688529b05c3913d3d252f89615f639f50

Committed 2026-05-13 10:55 PT

Revealed 2026-07-20 22:00 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-3DSGRTDD",
  "bug_class": "Heap-buffer-overflow",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-03-24T18:29:30+00:00",
  "description": "GDAL's GRIB2 driver calls gdal_comunpack (comunpack.c:150) to decode complex-packed data from Section 7, which in turn invokes gdal_gbits to extract bitfields from a packed byte buffer. That buffer is a 228-byte heap region allocated by ReadGrib2Record (degrib2.cpp:1008). With a crafted GRIB2 file, the bit offset/width parameters passed down from g2_unpack7 cause gdal_gbits to index one byte past the end of the allocation. An attacker who can supply a GRIB2 file to any GDAL-based application gets a 1-byte out-of-bounds heap read — primarily a denial-of-service, with marginal information-disclosure potential.",
  "discovered_at": null,
  "location": "gbits.c:62",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "gdal",
  "reproduction": [
    "Construct a GRIB2 message where Section 5 declares complex packing and Section 7 metadata (group widths/references/bit counts) implies more packed bits than the actual Section 7 payload contains",
    "Deliver the file to a target that reads raster data from it (e.g., gdalinfo -checksum, tile server, format conversion pipeline)",
    "GRIBRasterBand::IReadBlock triggers LoadData → ReadGrib2Record → g2_unpack7 → comunpack → gbits, which reads past the end of the c_ipack heap buffer"
  ],
  "technical_details": "ASAN: \"READ of size 1 at 0x740e9e602fe4 ... 0 bytes after 228-byte region\". gdal_gbits performs bit-level extraction from a packed byte array but does not validate that the computed byte offset stays within the bounds of the buffer supplied by gdal_comunpack. The offset and bit-width are derived from fields inside the GRIB2 message (Section 5/7 metadata), so a malformed file can push the read past the end of the realloc'd c_ipack buffer.",
  "title": "Heap-buffer-overflow in gbits.c:62",
  "vendor_severity": "high"
}