Two sibling library-API bugs in CString. Same shape as #1994: a missing input guard turns a degenerate argument into UB / unbounded loop. No in-tree caller passes the bad argument today, but the public API is exposed to module authors and to anyone embedding libznc.
// src/ZNCString.cpp
626:unsignedintCString::Replace(CString&sStr,constCString&sReplace,627:constCString&sWith,constCString&sLeft,628:constCString&sRight,boolbRemoveDelims){...656:}elseif(!bInside&&657:strncmp(p,sReplace.c_str(),uReplaceWidth)==0){658:sStr+=sWith;659:p+=uReplaceWidth-1;// <-- when uReplaceWidth==0: p -= 1
660:uRet++;...665:p++;// <-- back to where we started
When sReplace.empty(), strncmp(_, _, 0) == 0 always matches, the arithmetic at 659 underflows size_t to SIZE_MAX, the p++ at 665 returns to the same byte, and the loop appends sWith indefinitely until ASan kills the process for OOM.
// src/ZNCString.cpp
854:if(!bAllowEmpty){855:while(strncasecmp(p,sDelim.c_str(),uDelimLen)==0){856:p+=uDelimLen;// <-- 0 when sDelim.empty()
857:}858:}...893:if(!bAllowEmpty){894:while(strncasecmp(p,sDelim.c_str(),uDelimLen)==0){895:p+=uDelimLen;// <-- same bug
896:}897:}
strncasecmp(_, _, 0) is unconditionally 0; p never advances; the loop spins on the CPU forever. No allocation, so this hangs rather than OOMs.
PoC:
VCStringout;CStrings="abc";s.Split(/*sDelim=*/"",out,/*bAllowEmpty=*/false);// never returns
Reachability
I grepped every caller in src/ and modules/ and didn't find one that passes an empty sReplace or empty sDelim. So this is the same "library-API correctness, not currently reachable" framing as #1994. I'm just bringing the contracts of these two functions in line with what callers expect.
Fix
// in CString::Replace
if(sReplace.empty())return0;// in CString::Split, guard the prefix-skip loop
if(!bAllowEmpty&&!sDelim.empty()){while(strncasecmp(p,sDelim.c_str(),uDelimLen)==0)p+=uDelimLen;}// and likewise at line 894.
Or, more aggressively, bail out at the top of Split if sDelim.empty().
Happy to PR with a couple of regression tests in test/StringTest.cpp mirroring #1995.
Two sibling library-API bugs in `CString`. Same shape as #1994: a missing input guard turns a degenerate argument into UB / unbounded loop. No in-tree caller passes the bad argument today, but the public API is exposed to module authors and to anyone embedding libznc.
## 1) `CString::Replace`: empty `sReplace` causes pointer underflow + OOM
```cpp
// src/ZNCString.cpp
626: unsigned int CString::Replace(CString& sStr, const CString& sReplace,
627: const CString& sWith, const CString& sLeft,
628: const CString& sRight, bool bRemoveDelims) {
...
656: } else if (!bInside &&
657: strncmp(p, sReplace.c_str(), uReplaceWidth) == 0) {
658: sStr += sWith;
659: p += uReplaceWidth - 1; // <-- when uReplaceWidth==0: p -= 1
660: uRet++;
...
665: p++; // <-- back to where we started
```
When `sReplace.empty()`, `strncmp(_, _, 0) == 0` always matches, the arithmetic at 659 underflows `size_t` to `SIZE_MAX`, the `p++` at 665 returns to the same byte, and the loop appends `sWith` indefinitely until ASan kills the process for OOM.
PoC (3-second guard prevents the OOM kill):
```cpp
#include <cstdio>
#include <signal.h>
#include <unistd.h>
#include <znc/ZNCString.h>
extern "C" void to(int) { _exit(124); }
int main() {
signal(SIGALRM, to); alarm(3);
CString s = "abc";
CString::Replace(s, /*sReplace=*/"", /*sWith=*/"X");
}
```
```
ZNCString.cpp:659:15: runtime error: addition of unsigned offset to
0x... overflowed to 0x...
[3s later] killed by SIGALRM
```
## 2) `CString::Split`: empty `sDelim` + `bAllowEmpty=false` infinite-loops
Same pattern, two locations:
```cpp
// src/ZNCString.cpp
854: if (!bAllowEmpty) {
855: while (strncasecmp(p, sDelim.c_str(), uDelimLen) == 0) {
856: p += uDelimLen; // <-- 0 when sDelim.empty()
857: }
858: }
...
893: if (!bAllowEmpty) {
894: while (strncasecmp(p, sDelim.c_str(), uDelimLen) == 0) {
895: p += uDelimLen; // <-- same bug
896: }
897: }
```
`strncasecmp(_, _, 0)` is unconditionally 0; `p` never advances; the loop spins on the CPU forever. No allocation, so this hangs rather than OOMs.
PoC:
```cpp
VCString out;
CString s = "abc";
s.Split(/*sDelim=*/"", out, /*bAllowEmpty=*/false); // never returns
```
## Reachability
I grepped every caller in `src/` and `modules/` and didn't find one that passes an empty `sReplace` or empty `sDelim`. So this is the same "library-API correctness, not currently reachable" framing as #1994. I'm just bringing the contracts of these two functions in line with what callers expect.
## Fix
```cpp
// in CString::Replace
if (sReplace.empty()) return 0;
// in CString::Split, guard the prefix-skip loop
if (!bAllowEmpty && !sDelim.empty()) {
while (strncasecmp(p, sDelim.c_str(), uDelimLen) == 0) p += uDelimLen;
}
// and likewise at line 894.
```
Or, more aggressively, bail out at the top of `Split` if `sDelim.empty()`.
Happy to PR with a couple of regression tests in `test/StringTest.cpp` mirroring #1995.
<!-- github-sync-id:4326795799 -->
This is not a thing, as far as I know :) (except for cygwin where it's split from the binary for technical reasons)
> embedding libznc
This is not a thing, as far as I know :) (except for cygwin where it's split from the binary for technical reasons)
<!-- github-comment-id:4323496767 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two sibling library-API bugs in
CString. Same shape as #1994: a missing input guard turns a degenerate argument into UB / unbounded loop. No in-tree caller passes the bad argument today, but the public API is exposed to module authors and to anyone embedding libznc.1)
CString::Replace: emptysReplacecauses pointer underflow + OOMWhen
sReplace.empty(),strncmp(_, _, 0) == 0always matches, the arithmetic at 659 underflowssize_ttoSIZE_MAX, thep++at 665 returns to the same byte, and the loop appendssWithindefinitely until ASan kills the process for OOM.PoC (3-second guard prevents the OOM kill):
2)
CString::Split: emptysDelim+bAllowEmpty=falseinfinite-loopsSame pattern, two locations:
strncasecmp(_, _, 0)is unconditionally 0;pnever advances; the loop spins on the CPU forever. No allocation, so this hangs rather than OOMs.PoC:
Reachability
I grepped every caller in
src/andmodules/and didn't find one that passes an emptysReplaceor emptysDelim. So this is the same "library-API correctness, not currently reachable" framing as #1994. I'm just bringing the contracts of these two functions in line with what callers expect.Fix
Or, more aggressively, bail out at the top of
SplitifsDelim.empty().Happy to PR with a couple of regression tests in
test/StringTest.cppmirroring #1995.This is not a thing, as far as I know :) (except for cygwin where it's split from the binary for technical reasons)
Fair, the actual exposure is module authors. Adjusting framing on future ones. :)