Skip to content

Guard against unbounded recursion when parsing nested sequences - #239

Open
Volkopat wants to merge 1 commit into
malaterre:masterfrom
Volkopat:fix/nesting-depth-limit
Open

Volkopat wants to merge 1 commit into
malaterre:masterfrom
Volkopat:fix/nesting-depth-limit

Conversation

@Volkopat

@Volkopat Volkopat commented Oct 28, 2026 •

Copy link
Copy Markdown

Parsing a data set recurses once per nested sequence, and PS3.5 sets no nesting limit, so a conformant file can exhaust the native stack and kill the process before any error is reported.

==63==ERROR: AddressSanitizer: stack-overflow
    #0 gdcm::Item::Read<gdcm::ExplicitDataElement, gdcm::SwapperNoOp>            gdcmItem.h:99
    #1 gdcm::SequenceOfItems::Read<...>                                          gdcmSequenceOfItems.h:129
    #2 gdcm::ValueIO<...>::Read                                                  gdcmValueIO.txx:37
    #3 gdcm::ExplicitDataElement::ReadValue<gdcm::SwapperNoOp>                   gdcmExplicitDataElement.txx:33
    #5 gdcm::DataSet::ReadWithLength<...>
    #6 gdcm::Item::Read<...>                                                     gdcmItem.h:234
    ...

248 frames of that cycle on a file with 6,100 nested sequences. The same file kills the released 3.2.6 wheel with 0xC00000FD (STATUS_STACK_OVERFLOW), and python-gdcm users lose the interpreter.

The bound, stated honestly

I am not claiming real files nest 6,000 deep — they do not. The limit is a function of the stack, not of the depth:

available stack depth at which it dies
8 MB, release build ~6,100
8 MB, ASan build ~800 (sanitizer inflates frames)
512 KB ~100
512 KB, ordinary clinical file parses fine

So if the only configuration is a default 8 MB stack, you need a fairly silly file to hit this. What makes it worth fixing is that 512 KB is not exotic: threads created with a reduced stack, musl's small thread default, and sanitizer builds all sit in that range, and there 100 levels is enough — a depth real data can reach through nested functional groups.

DCMTK hit the same defect and fixed it by tracking depth and refusing beyond a configurable limit; its master handles these files cleanly. This does the same for GDCM.

The change

ParseDepthGuard counts nesting depth per thread (a thread_local, so concurrent parses do not interfere and the hot path takes no lock) and throws gdcm::Exception once the limit is reached. An instance is created at the top of Item::Read — the single point every nesting level passes through — and the limit defaults to 1000, with ParseDepthGuard::SetLimit() available for applications that carry deeper data.

Nothing else changes: the guard only fires where the parser would otherwise have recursed until the stack ran out.

Verified

build result
3.2.6 wheel (unpatched) 0xC00000FD, process dies
master + ASan (unpatched) AddressSanitizer: stack-overflow
this patch exit 1, exception thrown, no signal
this patch, ordinary clinical file unchanged, parses normally

The exception message ("Nesting depth limit exceeded while parsing data set") is swallowed by gdcmdump's catch-all, which is why the tool reports only "Failed to read" — a caller that catches gdcm::Exception sees the reason.

The repository description asks for pull requests rather than issues, so the report lives here.

Parsing recursed once per nested sequence with no depth limit, so a conformant file with thousands of nesting levels exhausted the native stack and killed the process. Track depth per thread and throw gdcm::Exception beyond a configurable limit (default 1000).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant