ucl_strnstr: fix remaining off-by-one over-read (follow-up to 852f752) - #400
Open
huanghuihui0904 wants to merge 2 commits into
Open
ucl_strnstr: fix remaining off-by-one over-read (follow-up to 852f752)#400huanghuihui0904 wants to merge 2 commits into
huanghuihui0904 wants to merge 2 commits into
Conversation
The loop permits a comparison when only mlen bytes remain. However, strncmp starts after the byte consumed by the loop and reads mlen more bytes, so the current byte plus mlen additional bytes must remain. When the haystack ends with a partial match, such as "x:/" against "://", the existing check passes and strncmp reads s[len]. This path is reachable through the .include macro handler. Commit 852f752 fixed another path through the same loop for OSS-Fuzz 28135, where the haystack ended before the needle's first character was found, but it did not cover trailing partial matches. Check the remaining length before dereferencing the haystack and use <= for the boundary condition. This also prevents an out-of-bounds read for single-character needles, where mlen is zero.
Owner
Yes, because it's bullshit. |
Owner
|
I have explained like 100500 times that all macros are not intended to use to parse untrusted data. So yes, OOB read in this path is not worth attention at all. |
Owner
|
I don't mean this should not be addressed, sorry for confusion - this function could be easily used in another places after some changes. |
Apply the same remaining-length check to ucl_strncasestr and add exact-boundary regression coverage for both bounded string search helpers.
Author
|
Thanks for the clarification and the follow-up commit! I reran the reproducer with your patch, and the bug is fixed on my side as well. The changes look good to me. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the root cause of #337 (CVE-2025-11010), which was closed without a fix and is still present on master. That report has the crash stack; what it does not have is why the bounds check lets it through, or a patch. The CVE record places the defect in
ucl_include_common; that is the caller that happens to hand in a buffer with no NUL after it. The off-by-one is inucl_strnstr, and every call site inherits it. This is also distinct from #378, which reports a one-byte over-read inucl_parse_macro_value()(src/ucl_parser.c) reached by input of a similar shape; this PR does not touch that code path, and in the reproducer belowucl_parse_macro_valuehas already returned by the time the read happens.ucl_strnstr()reads one byte past the end of the haystack when the haystack ends with a partial match of the needle. The check onsrc/ucl_util.c:2192admits one byte fewer than thestrncmpon line 2195 goes on to read, so with the needle"://"any buffer whose last two bytes are:/is read one past its end.ucl_strnstrwas written, and remains onmasteras of 04e5e70 (2026-08-16). 852f752708 rewrote this same guard in 2022 but covered a different path through the loop, which is why Bug Report: Heap-Buffer-Overflow in ucl_strnstr at ucl_util.c:2207 #337 could still reproduce in the same function..includeand.try_includemacro handlers.ucl_include_common()callsucl_strnstr()with a pointer straight into the caller's chunk and the macro argument's length, so the read lands one byte past the buffer the application owns. The other in-tree caller,ucl_schema.c:777, passesstrlen(p)and reads the terminating NUL instead./,strncmpreturns 0 and the include is routed toucl_include_url()rather thanucl_include_file(). A non-sanitized build can also fault if the buffer ends at an inaccessible page boundary.How to reproduce
Through the public API, on 04e5e70, with a chunk that is not NUL-terminated. That is what a caller feeding a mmap'd file or a network buffer hands to
ucl_parser_add_chunk().The 22-byte region is the input chunk. The macro argument is a pointer straight into it (
macro_startinucl_parser.c), sodata[len]is one byte past what the application owns.Two things matter when varying the input. The chunk must end exactly at the
/:.include(url=true) x:/\ndoes not trigger it, because the\nis inside the buffer and the over-read lands on it. Andurl = trueis what setsallow_urland reaches theucl_strnstrcall atucl_util.c:1612.Root cause
mlenis computed afterfind++, so it is the length of the needle's tail. When the loop consumess[k]the remaining count islen - k, and the check lets it continue whilelen - k >= mlen. Thestrncmpthat follows starts ats + k + 1and readsmlenbytes, so it touchess[k + mlen]and needslen - k >= mlen + 1. The two differ by one, solen - k == mlenpasses the check andstrncmpreadss[len].For the needle
"://"(mlen == 2) that is a haystack ending in:/: the:matchesc, the/matches the first bytestrncmpcompares, and the second byte it compares is one past the end.What 852f752 covered and what it missed
ucl_strnstron a heap buffer of exactly the stated size, no NUL:abc://x:/://abc@a://b://Row 1 is what 852f752 addressed: running out of haystack without ever meeting the needle's first character.
Row 2 is the case it missed, and is the one
.includereaches.Row 3 is a third path through the same loop, and the one place where 852f752 made the behaviour worse rather than leaving it unchanged. With a single-character needle
mlenis0, so the guard readslen-- < 0and only fires oncelenhas gone negative; because the dereference is evaluated first,s[len]ands[len + 1]are read before it does. That is a bounded two-byte over-read, independent of the haystack's length. The oldlen-- == 0stopped one iteration earlier, so this path used to read one byte past the end and now reads two.It also has a consequence the other rows do not. If
s[len]happens to equal the needle character, the inner loop exits,strncmp(s, "", 0)compares zero bytes and returns 0, and the function returns a pointer one byte past the end of the buffer. The pre-852f752708 code returnedNULLon that input.Every in-tree call site passes
"://", so row 3 is reachable only throughucl_internal.h, but it falls out of the same fix.Row 4 is the control: a needle that is present is still found, at the same offset, in all three versions.
What this fix does
len <= mlenleavesmlen + 1bytes, which is what the followingstrncmpneeds. Checking the remaining length before dereferencingsalso preventss[len]from being read whenmlenis zero, which is row 3 above. With this branch the reproducer above exits 0 with no output.