Skip to content

[BUG] Malformed COCO RLE counts can overflow the vendored pycocotools decode buffer #2889

Description

@starsalt0124

Describe the bug

The vendored COCO mask implementation under detection/retinaface/rcnn/pycocotools accepts compressed RLE objects whose decoded run lengths do not match the declared mask size.

_mask.decode(...) allocates the destination mask from the untrusted size field:

h, w, n = Rs._R[0].h, Rs._R[0].w, Rs._n
masks = Masks(h, w, n)
rleDecode(<RLE*>Rs._R, masks._mask, n)

But rleFrString(...) decodes the untrusted counts string into R->cnts without checking that the run-length total equals h * w. rleDecode(...) then trusts R->cnts[j] and writes that many bytes to the destination pointer:

for( k=0; k<R[i].cnts[j]; k++ ) *(M++)=v;

As a result, a malformed compressed RLE can make the native decoder write past the heap allocation.

Affected paths:

  • detection/retinaface/rcnn/pycocotools/mask.py::decode
  • detection/retinaface/rcnn/pycocotools/coco.py::annToMask
  • detection/retinaface/rcnn/pycocotools/_mask.pyx::_frString
  • detection/retinaface/rcnn/pycocotools/_mask.pyx::decode
  • detection/retinaface/rcnn/pycocotools/maskApi.c::rleFrString
  • detection/retinaface/rcnn/pycocotools/maskApi.c::rleDecode

The attacker-controlled input can be a COCO annotation/result file, or any caller-provided RLE dictionary passed to rcnn.pycocotools.mask.decode(...).

To Reproduce

I reproduced this with an isolated ASan build of the vendored _mask.pyx, maskApi.c, and maskApi.h.

Minimal PoC:

import _mask

_mask.decode([{"size": [1, 1], "counts": b"02"}])

This declares a 1-byte mask (size=[1, 1]) while counts=b"02" decodes to run lengths [0, 2], causing a 2-byte write into a 1-byte destination.

Build and run:

python setup.py build_ext --inplace
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.8 \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1 \
python poc_decode_overflow.py

Observed ASan output:

ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
#0 rleDecode .../maskApi.c:46
#1 __pyx_pf_5_mask_6decode .../_mask.c
0x... is located 0 bytes after 1-byte region
allocated by Masks.__cinit__

Expected behavior

Malformed RLE input should be rejected with a Python exception before native decoding writes to the destination buffer.

Recommended checks:

  • Validate that h, w, and n are positive.
  • Check that h * w * n cannot overflow size_t.
  • After rleFrString(...), verify that each decoded RLE's run-length total equals the declared h * w.
  • Reject decoded negative/delta values that wrap into huge uint counts.
  • Check malloc results before use.
  • Consider changing rleDecode(...) to take the output capacity and fail if a run would write past it.

Environment

Local verification environment:

Repo commit: 658b034e7fc0f4b08a01e11347b6118d8d04c76b
OS: Ubuntu 24.04.3 LTS under WSL2
Python: 3.13.9
NumPy: 2.5.0
Cython: 3.2.5
GCC/G++: 13.3.0
ASan runtime: /usr/lib/x86_64-linux-gnu/libasan.so.8

The RetinaFace Makefile builds the vendored extension with:

cd detection/retinaface
make

which includes:

cd rcnn/pycocotools/; python setup.py build_ext --inplace

Additional context

The cross-language path is:

COCO annotation/result RLE
  -> ann["segmentation"]["counts"] and ann["segmentation"]["size"]
  -> COCO.annToMask(...)
  -> maskUtils.decode(rle)
  -> pycocotools.mask.decode(...)
  -> Cython extension _mask.decode(...)
  -> _frString(...) / rleFrString(...)
  -> R->cnts
  -> rleDecode(...)
  -> heap out-of-bounds write

This is at least a reliable process crash/DoS for code that decodes untrusted COCO RLE masks, and it is native heap memory corruption rather than a normal Python-level parsing error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions