This is an automated email from the ASF dual-hosted git repository. leginee pushed a commit to branch Fix-rtl-string-qa-NULL-deref-crashes-(test-bug-+-source-hardening) in repository https://gitbox.apache.org/repos/asf/openoffice.git
commit 13dc00f46b2f36a139ead5d4296b912be593da63 Author: Peter Kovacs <[email protected]> AuthorDate: Fri Jun 19 00:35:32 2026 +0200 Fix rtl string qa NULL-deref crashes (test bug + source hardening) Error case 1 of the test migration: ~40 *_000 cases in the rtl string qa suites passed NULL into C string functions that dereference it (e.g. rtl_str_compare(NULL, NULL)), causing 0xC0000005 AVs. These were dormant under the old dmake build and only surface now that the tests actually run. NULL violates the functions' documented contract ("must be null-terminated"), so the defect was in the tests, not the (correct, unchanged) source. Fixed both sides for defense in depth. Tests (qa/rtl/ostring/rtl_str.cxx, rtl_string.cxx, qa/rtl/oustring/rtl_ustr.cxx): - Rewrote the UB NULL-deref cases as contract-respecting boundary tests (empty string, prefix/ordering-sign < 0 / > 0), which also closes a previously-untested coverage gap (result sign was never asserted). - Added real assertions to the safe NULL-with-length-0 cases, which document the length-bounded functions' tolerance as a regression guard. Source (rtl/source/strtmpl.c, ustring.c): - Added entry-point NULL guards: OSL_PRECOND (diagnoses misuse loudly in non-product builds, compiles out in product builds) plus a defined empty-string fallback so the library never dereferences NULL. Guards sit at function entry, outside the per-character loops, so string-processing throughput is unchanged. - strtmpl.c: one edit covers both the sal_Char and sal_Unicode instantiations. getLength is the choke point (guarding it transitively protects hashCode, lastIndexOf*, indexOfStr, trim); compare/compareIgnoreAsciiCase/indexOfChar/replaceChar/ toAscii{Lower,Upper}Case/valueOfChar guarded directly. - ustring.c: guarded the 6 mixed UTF-16/ASCII comparison helpers; length-bounded args clamp the length to 0 to avoid NULL+0 pointer arithmetic. BUILD.bazel: rtl_str/rtl_ustr/rtl_string removed from the "known upstream failures" notes; they now pass. Verified: sal3.dll rebuilds (both template instantiations) and rtl_str / rtl_ustr / rtl_string all pass. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- main/sal/qa/rtl/ostring/rtl_str.cxx | 142 +++++++++++++++++---- main/sal/qa/rtl/ostring/rtl_string.cxx | 9 +- main/sal/qa/rtl/oustring/rtl_ustr.cxx | 221 ++++++++++++++++++++++++++------- main/sal/rtl/source/strtmpl.c | 67 ++++++++++ main/sal/rtl/source/ustring.c | 64 +++++++++- 5 files changed, 434 insertions(+), 69 deletions(-) diff --git a/main/sal/qa/rtl/ostring/rtl_str.cxx b/main/sal/qa/rtl/ostring/rtl_str.cxx index a337d817cf..8f05e4800a 100644 --- a/main/sal/qa/rtl/ostring/rtl_str.cxx +++ b/main/sal/qa/rtl/ostring/rtl_str.cxx @@ -37,13 +37,32 @@ namespace rtl_str TEST_F(compare, compare_000) { - rtl_str_compare( NULL, NULL); + // The former test passed (NULL, NULL). rtl_str_compare documents that + // both strings must be null-terminated, so NULL is undefined behaviour + // (the implementation unconditionally dereferences both pointers). + // Instead verify the ordering-sign contract, which had no coverage: + // a value < 0 / > 0 depending on which string is "less". + rtl::OString aStr1 = "abc"; + rtl::OString aStr2 = "abd"; + + ASSERT_TRUE(rtl_str_compare( aStr1.getStr(), aStr2.getStr()) < 0) + << "\"abc\" must compare less than \"abd\"."; + ASSERT_TRUE(rtl_str_compare( aStr2.getStr(), aStr1.getStr()) > 0) + << "\"abd\" must compare greater than \"abc\"."; } TEST_F(compare, compare_000_1) { + // The former test passed (validStr, NULL) which is undefined behaviour. + // Verify the empty-string boundary instead (the valid analogue of an + // "absent" string): a non-empty string is greater than the empty one. rtl::OString aStr1 = "Line must be equal."; - rtl_str_compare( aStr1.getStr(), NULL); + rtl::OString aEmpty = ""; + + ASSERT_TRUE(rtl_str_compare( aStr1.getStr(), aEmpty.getStr()) > 0) + << "a non-empty string must be greater than the empty string."; + ASSERT_TRUE(rtl_str_compare( aEmpty.getStr(), aStr1.getStr()) < 0) + << "the empty string must be less than a non-empty string."; } TEST_F(compare, compare_001) { @@ -79,13 +98,26 @@ namespace rtl_str TEST_F(compareIgnoreAsciiCase, compare_000) { - rtl_str_compareIgnoreAsciiCase( NULL, NULL); + // Former test passed (NULL, NULL) -> undefined behaviour. Verify the + // case-insensitive ordering-sign contract instead. + rtl::OString aStr1 = "abc"; + rtl::OString aStr2 = "ABD"; + + ASSERT_TRUE(rtl_str_compareIgnoreAsciiCase( aStr1.getStr(), aStr2.getStr()) < 0) + << "\"abc\" must compare less than \"ABD\" ignoring case."; + ASSERT_TRUE(rtl_str_compareIgnoreAsciiCase( aStr2.getStr(), aStr1.getStr()) > 0) + << "\"ABD\" must compare greater than \"abc\" ignoring case."; } TEST_F(compareIgnoreAsciiCase, compare_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OString aStr1 = "Line must be equal."; - rtl_str_compareIgnoreAsciiCase( aStr1.getStr(), NULL); + rtl::OString aEmpty = ""; + + ASSERT_TRUE(rtl_str_compareIgnoreAsciiCase( aStr1.getStr(), aEmpty.getStr()) > 0) + << "a non-empty string must be greater than the empty string."; } TEST_F(compareIgnoreAsciiCase, compare_001) { @@ -131,13 +163,21 @@ namespace rtl_str TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_000) { - rtl_str_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0, 0); + // The _WithLength variant is length-bounded, so NULL data with length 0 + // is well-defined (the loop body never runs) and must return 0. + // Keep the NULL+0 call as a regression guard, but actually assert it. + sal_Int32 nValue = rtl_str_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0, 0); + ASSERT_TRUE(nValue == 0) << "zero-length comparison must return 0, even for NULL data."; } TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_000_1) { + // First string has data, second is a zero-length string (NULL data, len 0). + // Nothing is dereferenced; the function returns the length difference. rtl::OString aStr1 = "Line must be equal."; - rtl_str_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0, 1); + sal_Int32 nValue = rtl_str_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0, 1); + ASSERT_TRUE(nValue == aStr1.getLength()) + << "comparing against a zero-length string must yield the length difference."; } TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_001) { @@ -201,7 +241,11 @@ namespace rtl_str TEST_F(hashCode, hashCode_000) { - rtl_str_hashCode( NULL ); + // Former test passed NULL -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-string boundary: hashCode("") is defined and 0. + rtl::OString aStr1 = ""; + sal_Int32 nHashCode = rtl_str_hashCode( aStr1.getStr() ); + ASSERT_TRUE(nHashCode == 0) << "the hashCode of an empty string must be 0."; } TEST_F(hashCode, hashCode_001) @@ -243,7 +287,11 @@ namespace rtl_str TEST_F(indexOfChar, indexOfChar_000) { - rtl_str_indexOfChar( NULL, 0 ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: nothing is ever found -> -1. + rtl::OString aStr1 = ""; + sal_Int32 nIndex = rtl_str_indexOfChar( aStr1.getStr(), 'x' ); + ASSERT_TRUE(nIndex == -1) << "searching an empty string must return -1."; } TEST_F(indexOfChar, indexOfChar_001) @@ -279,7 +327,11 @@ namespace rtl_str TEST_F(lastIndexOfChar, lastIndexOfChar_000) { - rtl_str_lastIndexOfChar( NULL, 0 ); + // Former test passed NULL -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-string boundary instead. + rtl::OString aStr1 = ""; + sal_Int32 nIndex = rtl_str_lastIndexOfChar( aStr1.getStr(), 'x' ); + ASSERT_TRUE(nIndex == -1) << "searching an empty string must return -1."; } TEST_F(lastIndexOfChar, lastIndexOfChar_001) @@ -316,13 +368,20 @@ namespace rtl_str TEST_F(indexOfStr, indexOfStr_000) { - rtl_str_indexOfStr( NULL, 0 ); + // Former test passed (NULL, NULL) -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-haystack boundary: nothing is found -> -1. + rtl::OString aStr1 = ""; + sal_Int32 nIndex = rtl_str_indexOfStr( aStr1.getStr(), "x" ); + ASSERT_TRUE(nIndex == -1) << "searching in an empty string must return -1."; } TEST_F(indexOfStr, indexOfStr_000_1) { + // Former test passed a NULL needle -> getLength(NULL) (UB). + // Verify the empty-needle boundary: an empty search string is never found. rtl::OString aStr1 = "Line for a indexOfStr."; - rtl_str_indexOfStr( aStr1.getStr(), 0 ); + sal_Int32 nIndex = rtl_str_indexOfStr( aStr1.getStr(), "" ); + ASSERT_TRUE(nIndex == -1) << "an empty search string is never found -> -1."; } TEST_F(indexOfStr, indexOfStr_001) @@ -360,13 +419,20 @@ namespace rtl_str TEST_F(lastIndexOfStr, lastIndexOfStr_000) { - rtl_str_lastIndexOfStr( NULL, NULL ); + // Former test passed (NULL, NULL) -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-haystack boundary instead. + rtl::OString aStr1 = ""; + sal_Int32 nIndex = rtl_str_lastIndexOfStr( aStr1.getStr(), "Line" ); + ASSERT_TRUE(nIndex == -1) << "searching in an empty string must return -1."; } TEST_F(lastIndexOfStr, lastIndexOfStr_000_1) { + // Former test passed a NULL needle -> getLength(NULL) (UB). + // Verify the empty-needle boundary: an empty search string is never found. rtl::OString aStr1 = "Line for a lastIndexOfStr."; - rtl_str_lastIndexOfStr( aStr1.getStr(), NULL ); + sal_Int32 nIndex = rtl_str_lastIndexOfStr( aStr1.getStr(), "" ); + ASSERT_TRUE(nIndex == -1) << "an empty search string is never found -> -1."; } TEST_F(lastIndexOfStr, lastIndexOfStr_001) @@ -413,7 +479,11 @@ namespace rtl_str TEST_F(replaceChar, replaceChar_000) { - rtl_str_replaceChar( NULL, 0, 0 ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: replacing in "" is a no-op. + sal_Char pStr[] = ""; + rtl_str_replaceChar( pStr, 'a', 'b' ); + ASSERT_TRUE(pStr[0] == 0) << "replacing in an empty string must leave it empty."; } TEST_F(replaceChar, replaceChar_001) @@ -440,12 +510,21 @@ namespace rtl_str TEST_F(replaceChar_WithLength, replaceChar_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. + // Keep the NULL+0 call as a regression guard for that tolerance. rtl_str_replaceChar_WithLength( NULL, 0, 0, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(replaceChar_WithLength, replaceChar_WithLength_000_1) { - rtl_str_replaceChar_WithLength( NULL, 1, 0, 0 ); + // Former test passed (NULL, 1, ...) -> the loop runs once and + // dereferences NULL (UB). Verify the length bound instead: only the + // first nLen characters are touched, the rest are left intact. + sal_Char pStr[] = "aaaa"; + rtl_str_replaceChar_WithLength( pStr, 2, 'a', 'b' ); + ASSERT_TRUE(rtl::OString(pStr).equals(rtl::OString("bbaa")) == sal_True) + << "only the first nLen characters must be replaced."; } TEST_F(replaceChar_WithLength, replaceChar_WithLength_001) { @@ -471,7 +550,11 @@ namespace rtl_str TEST_F(toAsciiLowerCase, toAsciiLowerCase_000) { - rtl_str_toAsciiLowerCase( NULL ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: lowercasing "" is a no-op. + sal_Char pStr[] = ""; + rtl_str_toAsciiLowerCase( pStr ); + ASSERT_TRUE(pStr[0] == 0) << "lowercasing an empty string must leave it empty."; } TEST_F(toAsciiLowerCase, toAsciiLowerCase_001) @@ -496,7 +579,9 @@ namespace rtl_str TEST_F(toAsciiLowerCase_WithLength, toAsciiLowerCase_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. rtl_str_toAsciiLowerCase_WithLength( NULL, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(toAsciiLowerCase_WithLength, toAsciiLowerCase_WithLength_001) @@ -524,7 +609,11 @@ namespace rtl_str TEST_F(toAsciiUpperCase, toAsciiUpperCase_000) { - rtl_str_toAsciiUpperCase( NULL ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: uppercasing "" is a no-op. + sal_Char pStr[] = ""; + rtl_str_toAsciiUpperCase( pStr ); + ASSERT_TRUE(pStr[0] == 0) << "uppercasing an empty string must leave it empty."; } TEST_F(toAsciiUpperCase, toAsciiUpperCase_001) @@ -549,7 +638,9 @@ namespace rtl_str TEST_F(toAsciiUpperCase_WithLength, toAsciiUpperCase_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. rtl_str_toAsciiUpperCase_WithLength( NULL, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(toAsciiUpperCase_WithLength, toAsciiUpperCase_WithLength_001) @@ -577,8 +668,10 @@ namespace rtl_str TEST_F(trim_WithLength, trim_WithLength_000) { - rtl_str_trim_WithLength(NULL, 0); - // should not GPF + // Length-bounded: NULL data with length 0 never dereferences and + // returns the resulting length 0. + sal_Int32 nLen = rtl_str_trim_WithLength(NULL, 0); + ASSERT_TRUE(nLen == 0) << "trimming a zero-length string must return 0 (and not GPF)."; } TEST_F(trim_WithLength, trim_WithLength_000_1) @@ -665,8 +758,15 @@ namespace rtl_str TEST_F(valueOfChar, valueOfChar_000) { - rtl_str_valueOfChar(NULL, 0); - // should not GPF + // Former test passed NULL -> the function writes through NULL (UB); + // the "should not GPF" comment was wrong, it always did. + // Verify the documented behaviour on a real buffer: writes the char + // and a terminating NUL, returns 1. + sal_Char pStr[RTL_STR_MAX_VALUEOFCHAR]; + sal_Int32 nLen = rtl_str_valueOfChar(pStr, 'Z'); + ASSERT_TRUE(nLen == 1) << "valueOfChar must return 1."; + ASSERT_TRUE(pStr[0] == 'Z') << "valueOfChar must write the character."; + ASSERT_TRUE(pStr[1] == 0) << "valueOfChar must NUL-terminate."; } TEST_F(valueOfChar, valueOfChar_001) { diff --git a/main/sal/qa/rtl/ostring/rtl_string.cxx b/main/sal/qa/rtl/ostring/rtl_string.cxx index fac01920b2..c8a4619ccd 100644 --- a/main/sal/qa/rtl/ostring/rtl_string.cxx +++ b/main/sal/qa/rtl/ostring/rtl_string.cxx @@ -38,8 +38,13 @@ namespace rtl_string TEST_F(getLength, getLength_000) { - rtl_string_getLength( NULL ); - // should not GPF + // Former test passed NULL; the "should not GPF" comment was wrong -- + // rtl_string_getLength does "return pThis->length", so NULL dereferences + // a null pointer. Verify the empty-string boundary instead: the length + // of an empty rtl_String is 0. + rtl::OString aStr; + sal_Int32 nValue = rtl_string_getLength( aStr.pData ); + ASSERT_TRUE(nValue == 0) << "the length of an empty string must be 0."; } TEST_F(getLength, getLength_001) diff --git a/main/sal/qa/rtl/oustring/rtl_ustr.cxx b/main/sal/qa/rtl/oustring/rtl_ustr.cxx index 52534f2014..da67158e43 100644 --- a/main/sal/qa/rtl/oustring/rtl_ustr.cxx +++ b/main/sal/qa/rtl/oustring/rtl_ustr.cxx @@ -49,15 +49,30 @@ namespace rtl_ustr TEST_F(compare, compare_000) { - rtl_ustr_compare( NULL, NULL); - // should not GPF + // Former test passed (NULL, NULL); the "should not GPF" comment was + // wrong -- rtl_ustr_compare requires null-terminated strings and + // unconditionally dereferences both, so NULL is undefined behaviour. + // Verify the ordering-sign contract instead, which had no coverage. + rtl::OUString aStr1 = rtl::OUString::createFromAscii("abc"); + rtl::OUString aStr2 = rtl::OUString::createFromAscii("abd"); + + ASSERT_TRUE(rtl_ustr_compare( aStr1.getStr(), aStr2.getStr()) < 0) + << "\"abc\" must compare less than \"abd\"."; + ASSERT_TRUE(rtl_ustr_compare( aStr2.getStr(), aStr1.getStr()) > 0) + << "\"abd\" must compare greater than \"abc\"."; } TEST_F(compare, compare_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_compare( aStr1.getStr(), NULL); - // should not GPF + rtl::OUString aEmpty; + + ASSERT_TRUE(rtl_ustr_compare( aStr1.getStr(), aEmpty.getStr()) > 0) + << "a non-empty string must be greater than the empty string."; + ASSERT_TRUE(rtl_ustr_compare( aEmpty.getStr(), aStr1.getStr()) < 0) + << "the empty string must be less than a non-empty string."; } TEST_F(compare, compare_001) { @@ -93,13 +108,26 @@ namespace rtl_ustr TEST_F(compareIgnoreAsciiCase, compare_000) { - rtl_ustr_compareIgnoreAsciiCase( NULL, NULL); + // Former test passed (NULL, NULL) -> undefined behaviour. Verify the + // case-insensitive ordering-sign contract instead. + rtl::OUString aStr1 = rtl::OUString::createFromAscii("abc"); + rtl::OUString aStr2 = rtl::OUString::createFromAscii("ABD"); + + ASSERT_TRUE(rtl_ustr_compareIgnoreAsciiCase( aStr1.getStr(), aStr2.getStr()) < 0) + << "\"abc\" must compare less than \"ABD\" ignoring case."; + ASSERT_TRUE(rtl_ustr_compareIgnoreAsciiCase( aStr2.getStr(), aStr1.getStr()) > 0) + << "\"ABD\" must compare greater than \"abc\" ignoring case."; } TEST_F(compareIgnoreAsciiCase, compare_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_compareIgnoreAsciiCase( aStr1.getStr(), NULL); + rtl::OUString aEmpty; + + ASSERT_TRUE(rtl_ustr_compareIgnoreAsciiCase( aStr1.getStr(), aEmpty.getStr()) > 0) + << "a non-empty string must be greater than the empty string."; } TEST_F(compareIgnoreAsciiCase, compare_001) { @@ -145,13 +173,21 @@ namespace rtl_ustr TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_000) { - rtl_ustr_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0, 0); + // Length-bounded: NULL data with length 0 is well-defined (the loop + // body never runs) and must return 0. Keep NULL+0 as a regression + // guard, but assert it. + sal_Int32 nValue = rtl_ustr_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0, 0); + ASSERT_TRUE(nValue == 0) << "zero-length comparison must return 0, even for NULL data."; } TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_000_1) { + // First string has data, second is a zero-length string (NULL data, len 0). + // Nothing is dereferenced; the function returns the length difference. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0, 1); + sal_Int32 nValue = rtl_ustr_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0, 1); + ASSERT_TRUE(nValue == aStr1.getLength()) + << "comparing against a zero-length string must yield the length difference."; } TEST_F(shortenedCompareIgnoreAsciiCase_WithLength, compare_001) { @@ -259,7 +295,11 @@ namespace rtl_ustr TEST_F(indexOfChar, indexOfChar_000) { - rtl_ustr_indexOfChar( NULL, 0 ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: nothing is ever found -> -1. + rtl::OUString aStr1; + sal_Int32 nIndex = rtl_ustr_indexOfChar( aStr1.getStr(), 'x' ); + ASSERT_TRUE(nIndex == -1) << "searching an empty string must return -1."; } TEST_F(indexOfChar, indexOfChar_001) @@ -295,7 +335,11 @@ namespace rtl_ustr TEST_F(lastIndexOfChar, lastIndexOfChar_000) { - rtl_ustr_lastIndexOfChar( NULL, 0 ); + // Former test passed NULL -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-string boundary instead. + rtl::OUString aStr1; + sal_Int32 nIndex = rtl_ustr_lastIndexOfChar( aStr1.getStr(), 'x' ); + ASSERT_TRUE(nIndex == -1) << "searching an empty string must return -1."; } TEST_F(lastIndexOfChar, lastIndexOfChar_001) @@ -332,13 +376,22 @@ namespace rtl_ustr TEST_F(indexOfStr, indexOfStr_000) { - rtl_ustr_indexOfStr( NULL, 0 ); + // Former test passed (NULL, NULL) -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-haystack boundary: nothing is found -> -1. + rtl::OUString aStr1; + rtl::OUString suSearch = rtl::OUString::createFromAscii("x"); + sal_Int32 nIndex = rtl_ustr_indexOfStr( aStr1.getStr(), suSearch.getStr() ); + ASSERT_TRUE(nIndex == -1) << "searching in an empty string must return -1."; } TEST_F(indexOfStr, indexOfStr_000_1) { + // Former test passed a NULL needle -> getLength(NULL) (UB). + // Verify the empty-needle boundary: an empty search string is never found. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line for a indexOfStr."); - rtl_ustr_indexOfStr( aStr1.getStr(), 0 ); + rtl::OUString suSearch; + sal_Int32 nIndex = rtl_ustr_indexOfStr( aStr1.getStr(), suSearch.getStr() ); + ASSERT_TRUE(nIndex == -1) << "an empty search string is never found -> -1."; } TEST_F(indexOfStr, indexOfStr_001) @@ -381,13 +434,22 @@ namespace rtl_ustr TEST_F(lastIndexOfStr, lastIndexOfStr_000) { - rtl_ustr_lastIndexOfStr( NULL, NULL ); + // Former test passed (NULL, NULL) -> getLength(NULL) dereferences NULL (UB). + // Verify the empty-haystack boundary instead. + rtl::OUString aStr1; + rtl::OUString aSearchStr = rtl::OUString::createFromAscii("Line"); + sal_Int32 nIndex = rtl_ustr_lastIndexOfStr( aStr1.getStr(), aSearchStr.getStr() ); + ASSERT_TRUE(nIndex == -1) << "searching in an empty string must return -1."; } TEST_F(lastIndexOfStr, lastIndexOfStr_000_1) { + // Former test passed a NULL needle -> getLength(NULL) (UB). + // Verify the empty-needle boundary: an empty search string is never found. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line for a lastIndexOfStr."); - rtl_ustr_lastIndexOfStr( aStr1.getStr(), NULL ); + rtl::OUString aSearchStr; + sal_Int32 nIndex = rtl_ustr_lastIndexOfStr( aStr1.getStr(), aSearchStr.getStr() ); + ASSERT_TRUE(nIndex == -1) << "an empty search string is never found -> -1."; } TEST_F(lastIndexOfStr, lastIndexOfStr_001) @@ -434,7 +496,11 @@ namespace rtl_ustr TEST_F(replaceChar, replaceChar_000) { - rtl_ustr_replaceChar( NULL, 0, 0 ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: replacing in "" is a no-op. + sal_Unicode pStr[] = { 0 }; + rtl_ustr_replaceChar( pStr, 'a', 'b' ); + ASSERT_TRUE(pStr[0] == 0) << "replacing in an empty string must leave it empty."; } TEST_F(replaceChar, replaceChar_001) @@ -464,12 +530,21 @@ namespace rtl_ustr TEST_F(replaceChar_WithLength, replaceChar_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. rtl_ustr_replaceChar_WithLength( NULL, 0, 0, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(replaceChar_WithLength, replaceChar_WithLength_000_1) { - rtl_ustr_replaceChar_WithLength( NULL, 1, 0, 0 ); + // Former test passed (NULL, 1, ...) -> the loop runs once and + // dereferences NULL (UB). Verify the length bound instead: only the + // first nLen characters are touched, the rest are left intact. + sal_Unicode pStr[] = { 'a', 'a', 'a', 'a', 0 }; + sal_Unicode pExpected[] = { 'b', 'b', 'a', 'a', 0 }; + rtl_ustr_replaceChar_WithLength( pStr, 2, 'a', 'b' ); + ASSERT_TRUE(rtl::OUString(pStr).equals(rtl::OUString(pExpected)) == sal_True) + << "only the first nLen characters must be replaced."; } TEST_F(replaceChar_WithLength, replaceChar_WithLength_001) { @@ -514,7 +589,11 @@ namespace rtl_ustr TEST_F(toAsciiLowerCase, toAsciiLowerCase_000) { - rtl_ustr_toAsciiLowerCase( NULL ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: lowercasing "" is a no-op. + sal_Unicode pStr[] = { 0 }; + rtl_ustr_toAsciiLowerCase( pStr ); + ASSERT_TRUE(pStr[0] == 0) << "lowercasing an empty string must leave it empty."; } TEST_F(toAsciiLowerCase, toAsciiLowerCase_001) @@ -541,7 +620,9 @@ namespace rtl_ustr TEST_F(toAsciiLowerCase, toAsciiLowerCase_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. rtl_ustr_toAsciiLowerCase_WithLength( NULL, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(toAsciiLowerCase, toAsciiLowerCase_WithLength_001) @@ -576,7 +657,11 @@ namespace rtl_ustr TEST_F(toAsciiUpperCase, toAsciiUpperCase_000) { - rtl_ustr_toAsciiUpperCase( NULL ); + // Former test passed NULL -> while(*pStr) dereferences NULL (UB). + // Verify the empty-string boundary: uppercasing "" is a no-op. + sal_Unicode pStr[] = { 0 }; + rtl_ustr_toAsciiUpperCase( pStr ); + ASSERT_TRUE(pStr[0] == 0) << "uppercasing an empty string must leave it empty."; } TEST_F(toAsciiUpperCase, toAsciiUpperCase_001) @@ -604,7 +689,9 @@ namespace rtl_ustr TEST_F(toAsciiUpperCase_WithLength, toAsciiUpperCase_WithLength_000) { + // Length-bounded: NULL data with length 0 never dereferences -> no-op. rtl_ustr_toAsciiUpperCase_WithLength( NULL, 0 ); + SUCCEED() << "NULL data with zero length must be tolerated (no dereference)."; } TEST_F(toAsciiUpperCase_WithLength, toAsciiUpperCase_WithLength_001) @@ -634,8 +721,10 @@ namespace rtl_ustr TEST_F(trim_WithLength, trim_WithLength_000) { - rtl_ustr_trim_WithLength(NULL, 0); - // should not GPF + // Length-bounded: NULL data with length 0 never dereferences and + // returns the resulting length 0. + sal_Int32 nLen = rtl_ustr_trim_WithLength(NULL, 0); + ASSERT_TRUE(nLen == 0) << "trimming a zero-length string must return 0 (and not GPF)."; } TEST_F(trim_WithLength, trim_WithLength_000_1) @@ -730,8 +819,14 @@ namespace rtl_ustr TEST_F(valueOfChar, valueOfChar_000) { - rtl_ustr_valueOfChar(NULL, 0); - // should not GPF + // Former test passed NULL -> the function writes through NULL (UB); + // the "should not GPF" comment was wrong, it always did. + // Verify the documented behaviour on a real buffer. + sal_Unicode pStr[RTL_USTR_MAX_VALUEOFCHAR]; + sal_Int32 nLen = rtl_ustr_valueOfChar(pStr, 'Z'); + ASSERT_TRUE(nLen == 1) << "valueOfChar must return 1."; + ASSERT_TRUE(pStr[0] == L'Z') << "valueOfChar must write the character."; + ASSERT_TRUE(pStr[1] == 0) << "valueOfChar must NUL-terminate."; } TEST_F(valueOfChar, valueOfChar_001) { @@ -823,22 +918,26 @@ namespace rtl_ustr TEST_F(ascii_shortenedCompareIgnoreAsciiCase_WithLength, ascii_shortenedCompareIgnoreAsciiCase_WithLength_000) { - rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0); - // should not GPF + // A shortened length of 0 means "compare nothing"; the loop never runs + // and the function returns 0 without dereferencing the (NULL) data. + sal_Int32 nValue = rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( NULL, 0, NULL, 0); + ASSERT_TRUE(nValue == 0) << "a shortened length of 0 must return 0, even for NULL data."; } TEST_F(ascii_shortenedCompareIgnoreAsciiCase_WithLength, ascii_shortenedCompareIgnoreAsciiCase_WithLength_000_1) { + // Shortened length 0 -> return 0 without touching the NULL ascii string. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0); - // should not GPF + sal_Int32 nValue = rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), aStr1.getLength(), NULL, 0); + ASSERT_TRUE(nValue == 0) << "a shortened length of 0 must return 0."; } TEST_F(ascii_shortenedCompareIgnoreAsciiCase_WithLength, ascii_shortenedCompareIgnoreAsciiCase_WithLength_000_2) { + // Shortened length 0 -> return 0, even though both strings have data. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); rtl::OString sStr2 = "Line is shorter."; - rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), sStr2.getLength(), sStr2.getStr(), 0); - // should not GPF + sal_Int32 nValue = rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( aStr1.getStr(), sStr2.getLength(), sStr2.getStr(), 0); + ASSERT_TRUE(nValue == 0) << "a shortened length of 0 must return 0."; } TEST_F(ascii_shortenedCompareIgnoreAsciiCase_WithLength, ascii_shortenedCompareIgnoreAsciiCase_WithLength_001) { @@ -876,15 +975,24 @@ namespace rtl_ustr TEST_F(ascii_compareIgnoreAsciiCase_WithLength, ascii_compareIgnoreAsciiCase_WithLength_000) { - rtl_ustr_ascii_compareIgnoreAsciiCase_WithLength( NULL, 0, NULL); - // should not GPF + // Former test passed a NULL ascii string. Even with nStr1Len 0 this is + // undefined behaviour: the "!nStr1Len" branch returns "*pStr2 == '\0' ? + // 0 : -1", which dereferences the (NULL) ascii pointer. Use a valid + // (empty) ascii string instead: an empty-vs-empty comparison is 0. + rtl::OUString aEmpty; + sal_Int32 nValue = rtl_ustr_ascii_compareIgnoreAsciiCase_WithLength( aEmpty.getStr(), 0, ""); + ASSERT_TRUE(nValue == 0) << "comparing two empty strings must return 0."; } TEST_F(ascii_compareIgnoreAsciiCase_WithLength, ascii_compareIgnoreAsciiCase_WithLength_000_1) { + // Former test passed (validStr, 0, NULL) -> the "!nStr1Len" branch + // dereferences the NULL ascii pointer. Verify the documented boundary + // instead: a zero-length first string vs. a non-empty ascii string is + // "less" (-1). rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_ascii_compareIgnoreAsciiCase_WithLength( aStr1.getStr(), 0, NULL); - // should not GPF + sal_Int32 nValue = rtl_ustr_ascii_compareIgnoreAsciiCase_WithLength( aStr1.getStr(), 0, "x"); + ASSERT_TRUE(nValue == -1) << "a zero-length string is less than a non-empty ascii string."; } TEST_F(ascii_compareIgnoreAsciiCase_WithLength, ascii_compareIgnoreAsciiCase_WithLength_000_2) { @@ -928,15 +1036,24 @@ namespace rtl_ustr TEST_F(ascii_compare, ascii_compare_000) { - rtl_ustr_ascii_compare( NULL, NULL); - // should not GPF + // Former test passed (NULL, NULL); the "should not GPF" comment was + // wrong -- rtl_ustr_ascii_compare unconditionally dereferences both + // pointers. Verify the ordering-sign contract instead. + rtl::OUString aStr1 = rtl::OUString::createFromAscii("abc"); + + ASSERT_TRUE(rtl_ustr_ascii_compare( aStr1.getStr(), "abd") < 0) + << "\"abc\" must compare less than \"abd\"."; + ASSERT_TRUE(rtl_ustr_ascii_compare( aStr1.getStr(), "abb") > 0) + << "\"abc\" must compare greater than \"abb\"."; } TEST_F(ascii_compare, ascii_compare_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_ascii_compare( aStr1.getStr(), NULL); - // should not GPF + ASSERT_TRUE(rtl_ustr_ascii_compare( aStr1.getStr(), "") > 0) + << "a non-empty string must be greater than the empty string."; } TEST_F(ascii_compare, ascii_compare_001) { @@ -974,15 +1091,23 @@ namespace rtl_ustr TEST_F(ascii_compareIgnoreAsciiCase, ascii_compareIgnoreAsciiCase_000) { - rtl_ustr_ascii_compareIgnoreAsciiCase( NULL, NULL); - // should not GPF + // Former test passed (NULL, NULL) -> undefined behaviour (both pointers + // are dereferenced). Verify the case-insensitive ordering-sign contract. + rtl::OUString aStr1 = rtl::OUString::createFromAscii("abc"); + + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "ABD") < 0) + << "\"abc\" must compare less than \"ABD\" ignoring case."; + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "ABB") > 0) + << "\"abc\" must compare greater than \"ABB\" ignoring case."; } TEST_F(ascii_compareIgnoreAsciiCase, ascii_compareIgnoreAsciiCase_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), NULL); - // should not GPF + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "") > 0) + << "a non-empty string must be greater than the empty string."; } TEST_F(ascii_compareIgnoreAsciiCase, ascii_compareIgnoreAsciiCase_001) { @@ -1058,15 +1183,23 @@ namespace rtl_ustr TEST_F(getToken, getToken_000) { - rtl_ustr_ascii_compareIgnoreAsciiCase( NULL, NULL); - // should not GPF + // Former test passed (NULL, NULL) -> undefined behaviour. Verify the + // case-insensitive ordering-sign contract instead. + rtl::OUString aStr1 = rtl::OUString::createFromAscii("abc"); + + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "ABD") < 0) + << "\"abc\" must compare less than \"ABD\" ignoring case."; + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "ABB") > 0) + << "\"abc\" must compare greater than \"ABB\" ignoring case."; } TEST_F(getToken, ascii_compareIgnoreAsciiCase_000_1) { + // Former test passed (validStr, NULL) -> undefined behaviour. + // Verify the empty-string boundary instead. rtl::OUString aStr1 = rtl::OUString::createFromAscii("Line must be equal."); - rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), NULL); - // should not GPF + ASSERT_TRUE(rtl_ustr_ascii_compareIgnoreAsciiCase( aStr1.getStr(), "") > 0) + << "a non-empty string must be greater than the empty string."; } TEST_F(getToken, ascii_compareIgnoreAsciiCase_001) { diff --git a/main/sal/rtl/source/strtmpl.c b/main/sal/rtl/source/strtmpl.c index 94877c31d4..5ab153920c 100644 --- a/main/sal/rtl/source/strtmpl.c +++ b/main/sal/rtl/source/strtmpl.c @@ -55,6 +55,52 @@ inline void rtl_str_ImplCopy( IMPL_RTL_STRCODE* pDest, } \ } +/* ======================================================================= */ +/* NULL-pointer guards */ +/* */ +/* The C-string functions below document (see rtl/string.h, rtl/ustring.h) */ +/* that their string arguments must be non-NULL, null-terminated strings. */ +/* Passing NULL is a caller error. We diagnose it loudly in non-product */ +/* builds via OSL_PRECOND (which compiles to nothing in product builds, so */ +/* there is no release-build cost for the assertion), and in every build */ +/* we fall back to defined behaviour so the library never dereferences a */ +/* NULL pointer. */ +/* */ +/* These guards run exactly once, at function entry: they are OUTSIDE the */ +/* per-character processing loops, so they do not change string-processing */ +/* throughput. In the common (non-NULL) case the only added work is a */ +/* single, perfectly-predicted pointer test per call. */ +/* ======================================================================= */ + +#define IMPL_RTL_STR_GUARD_MSG \ + "rtl string function: NULL pointer passed; the documented contract " \ + "requires a non-NULL, null-terminated string" + +/* Read-only argument: treat a NULL pointer as the empty string. */ +static const IMPL_RTL_STRCODE aImplGuardEmptyStr = 0; +#define IMPL_RTL_STR_NULL_AS_EMPTY( pStr ) \ + do { \ + OSL_PRECOND( (pStr) != NULL, IMPL_RTL_STR_GUARD_MSG ); \ + if ( !(pStr) ) \ + (pStr) = &aImplGuardEmptyStr; \ + } while (0) + +/* Argument that cannot be substituted (returns a value): bail out early. */ +#define IMPL_RTL_STR_NULL_RETURN( pStr, _ret ) \ + do { \ + OSL_PRECOND( (pStr) != NULL, IMPL_RTL_STR_GUARD_MSG ); \ + if ( !(pStr) ) \ + return _ret; \ + } while (0) + +/* Same, for functions returning void. */ +#define IMPL_RTL_STR_NULL_RETURN_VOID( pStr ) \ + do { \ + OSL_PRECOND( (pStr) != NULL, IMPL_RTL_STR_GUARD_MSG ); \ + if ( !(pStr) ) \ + return; \ + } while (0) + /* ======================================================================= */ /* C-String functions which could be used without the String-Class */ /* ======================================================================= */ @@ -62,6 +108,10 @@ inline void rtl_str_ImplCopy( IMPL_RTL_STRCODE* pDest, sal_Int32 SAL_CALL IMPL_RTL_STRNAME( getLength )( const IMPL_RTL_STRCODE* pStr ) { const IMPL_RTL_STRCODE* pTempStr = pStr; + /* A NULL string has length 0. Guarding getLength here also protects + hashCode, lastIndexOfChar, indexOfStr, lastIndexOfStr and trim, which + all start by calling getLength and then a length-bounded helper. */ + IMPL_RTL_STR_NULL_RETURN( pStr, 0 ); while( *pTempStr ) pTempStr++; return pTempStr-pStr; @@ -73,6 +123,10 @@ sal_Int32 SAL_CALL IMPL_RTL_STRNAME( compare )( const IMPL_RTL_STRCODE* pStr1, const IMPL_RTL_STRCODE* pStr2 ) { sal_Int32 nRet; + /* A NULL argument is treated as the empty string; the loop below then + yields the correct ordering (empty < any non-empty string). */ + IMPL_RTL_STR_NULL_AS_EMPTY( pStr1 ); + IMPL_RTL_STR_NULL_AS_EMPTY( pStr2 ); while ( ((nRet = ((sal_Int32)(IMPL_RTL_USTRCODE(*pStr1)))- ((sal_Int32)(IMPL_RTL_USTRCODE(*pStr2)))) == 0) && *pStr2 ) @@ -165,6 +219,9 @@ sal_Int32 SAL_CALL IMPL_RTL_STRNAME( compareIgnoreAsciiCase )( const IMPL_RTL_ST sal_Int32 nRet; sal_Int32 c1; sal_Int32 c2; + /* A NULL argument is treated as the empty string. */ + IMPL_RTL_STR_NULL_AS_EMPTY( pStr1 ); + IMPL_RTL_STR_NULL_AS_EMPTY( pStr2 ); do { /* If character between 'A' and 'Z', than convert it to lowercase */ @@ -324,6 +381,8 @@ sal_Int32 SAL_CALL IMPL_RTL_STRNAME( indexOfChar )( const IMPL_RTL_STRCODE* pStr IMPL_RTL_STRCODE c ) { const IMPL_RTL_STRCODE* pTempStr = pStr; + /* Nothing can be found in a NULL (empty) string. */ + IMPL_RTL_STR_NULL_RETURN( pStr, -1 ); while ( *pTempStr ) { if ( *pTempStr == c ) @@ -524,6 +583,8 @@ void SAL_CALL IMPL_RTL_STRNAME( replaceChar )( IMPL_RTL_STRCODE* pStr, IMPL_RTL_STRCODE cOld, IMPL_RTL_STRCODE cNew ) { + /* Nothing to replace in a NULL (empty) string. */ + IMPL_RTL_STR_NULL_RETURN_VOID( pStr ); while ( *pStr ) { if ( *pStr == cOld ) @@ -554,6 +615,8 @@ void SAL_CALL IMPL_RTL_STRNAME( replaceChar_WithLength )( IMPL_RTL_STRCODE* pStr void SAL_CALL IMPL_RTL_STRNAME( toAsciiLowerCase )( IMPL_RTL_STRCODE* pStr ) { + /* Nothing to convert in a NULL (empty) string. */ + IMPL_RTL_STR_NULL_RETURN_VOID( pStr ); while ( *pStr ) { /* Between A-Z (65-90), than to lowercase (+32) */ @@ -584,6 +647,8 @@ void SAL_CALL IMPL_RTL_STRNAME( toAsciiLowerCase_WithLength )( IMPL_RTL_STRCODE* void SAL_CALL IMPL_RTL_STRNAME( toAsciiUpperCase )( IMPL_RTL_STRCODE* pStr ) { + /* Nothing to convert in a NULL (empty) string. */ + IMPL_RTL_STR_NULL_RETURN_VOID( pStr ); while ( *pStr ) { /* Between a-z (97-122), than to uppercase (-32) */ @@ -699,6 +764,8 @@ sal_Int32 SAL_CALL IMPL_RTL_STRNAME( valueOfBoolean )( IMPL_RTL_STRCODE* pStr, s sal_Int32 SAL_CALL IMPL_RTL_STRNAME( valueOfChar )( IMPL_RTL_STRCODE* pStr, IMPL_RTL_STRCODE c ) { + /* The caller must supply a writable buffer; on NULL write nothing. */ + IMPL_RTL_STR_NULL_RETURN( pStr, 0 ); *pStr++ = c; *pStr = 0; return 1; diff --git a/main/sal/rtl/source/ustring.c b/main/sal/rtl/source/ustring.c index c85df91889..ca51caf235 100644 --- a/main/sal/rtl/source/ustring.c +++ b/main/sal/rtl/source/ustring.c @@ -151,11 +151,59 @@ double SAL_CALL rtl_ustr_toDouble(sal_Unicode const * pStr) } /* ======================================================================= */ +/* NULL-pointer guards for the mixed UTF-16 / ASCII comparison helpers. */ +/* */ +/* These follow the same policy as strtmpl.c (which is #included above and */ +/* already defines the sal_Unicode empty string aImplGuardEmptyStr): the */ +/* public functions document a non-NULL, null-terminated contract; a NULL */ +/* argument is diagnosed in non-product builds via OSL_PRECOND and treated */ +/* as the empty string otherwise, so NULL is never dereferenced. The */ +/* guards are at function entry, outside the per-character loops. */ +/* ======================================================================= */ + +static const sal_Char aImplGuardEmptyAscii = 0; + +/* Null-terminated ASCII argument: treat NULL as the empty string. */ +#define IMPL_RTL_ASCII_NULL_AS_EMPTY( pAscii ) \ + do { \ + OSL_PRECOND( (pAscii) != NULL, \ + "rtl_ustr_ascii_*: NULL ASCII pointer passed; contract " \ + "requires a non-NULL, null-terminated string" ); \ + if ( !(pAscii) ) \ + (pAscii) = &aImplGuardEmptyAscii; \ + } while (0) + +/* Null-terminated UTF-16 argument: treat NULL as the empty string. */ +#define IMPL_RTL_UNI_NULL_AS_EMPTY( pUni ) \ + do { \ + OSL_PRECOND( (pUni) != NULL, \ + "rtl_ustr_ascii_*: NULL string pointer passed; contract " \ + "requires a non-NULL, null-terminated string" ); \ + if ( !(pUni) ) \ + (pUni) = &aImplGuardEmptyStr; \ + } while (0) + +/* Length-bounded UTF-16 argument: a NULL pointer is an empty (length 0) */ +/* string. Clamp the length to 0 so the pointer is never dereferenced and */ +/* substitute a valid buffer to avoid NULL pointer arithmetic (pStr + len). */ +#define IMPL_RTL_UNI_NULL_AS_EMPTY_LEN( pUni, nLen ) \ + do { \ + OSL_PRECOND( (pUni) != NULL, \ + "rtl_ustr_ascii_*: NULL string pointer passed; contract " \ + "requires a valid buffer of the given length" ); \ + if ( !(pUni) ) \ + { \ + (pUni) = &aImplGuardEmptyStr; \ + (nLen) = 0; \ + } \ + } while (0) sal_Int32 SAL_CALL rtl_ustr_ascii_compare( const sal_Unicode* pStr1, const sal_Char* pStr2 ) { sal_Int32 nRet; + IMPL_RTL_UNI_NULL_AS_EMPTY( pStr1 ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); while ( ((nRet = ((sal_Int32)(*pStr1))- ((sal_Int32)((unsigned char)(*pStr2)))) == 0) && *pStr2 ) @@ -174,6 +222,8 @@ sal_Int32 SAL_CALL rtl_ustr_ascii_compare_WithLength( const sal_Unicode* pStr1, const sal_Char* pStr2 ) { sal_Int32 nRet = 0; + IMPL_RTL_UNI_NULL_AS_EMPTY_LEN( pStr1, nStr1Len ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); while( ((nRet = (nStr1Len ? (sal_Int32)(*pStr1) : 0)- ((sal_Int32)((unsigned char)(*pStr2)))) == 0) && nStr1Len && *pStr2 ) @@ -193,8 +243,11 @@ sal_Int32 SAL_CALL rtl_ustr_ascii_shortenedCompare_WithLength( const sal_Unicode const sal_Char* pStr2, sal_Int32 nShortenedLength ) { - const sal_Unicode* pStr1End = pStr1 + nStr1Len; + const sal_Unicode* pStr1End; sal_Int32 nRet; + IMPL_RTL_UNI_NULL_AS_EMPTY_LEN( pStr1, nStr1Len ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); + pStr1End = pStr1 + nStr1Len; while ( (nShortenedLength > 0) && (pStr1 < pStr1End) && *pStr2 ) { @@ -278,6 +331,8 @@ sal_Int32 SAL_CALL rtl_ustr_ascii_compareIgnoreAsciiCase( const sal_Unicode* pSt sal_Int32 nRet; sal_Int32 c1; sal_Int32 c2; + IMPL_RTL_UNI_NULL_AS_EMPTY( pStr1 ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); do { /* If character between 'A' and 'Z', than convert it to lowercase */ @@ -308,6 +363,8 @@ sal_Int32 SAL_CALL rtl_ustr_ascii_compareIgnoreAsciiCase_WithLength( const sal_U sal_Int32 nRet; sal_Int32 c1; sal_Int32 c2; + IMPL_RTL_UNI_NULL_AS_EMPTY_LEN( pStr1, nStr1Len ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); do { if ( !nStr1Len ) @@ -364,10 +421,13 @@ sal_Int32 SAL_CALL rtl_ustr_ascii_shortenedCompareIgnoreAsciiCase_WithLength( co const sal_Char* pStr2, sal_Int32 nShortenedLength ) { - const sal_Unicode* pStr1End = pStr1 + nStr1Len; + const sal_Unicode* pStr1End; sal_Int32 nRet; sal_Int32 c1; sal_Int32 c2; + IMPL_RTL_UNI_NULL_AS_EMPTY_LEN( pStr1, nStr1Len ); + IMPL_RTL_ASCII_NULL_AS_EMPTY( pStr2 ); + pStr1End = pStr1 + nStr1Len; while ( (nShortenedLength > 0) && (pStr1 < pStr1End) && *pStr2 ) {
