This is an automated email from the ASF dual-hosted git repository.
leginee pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/openoffice.git
The following commit(s) were added to refs/heads/trunk by this push:
new f0853378f1 Fix rtl string qa NULL-deref crashes (test bug + source
hardening) (#486)
f0853378f1 is described below
commit f0853378f1a90e669189f0f58595ee993d034643
Author: Peter <[email protected]>
AuthorDate: Thu Aug 6 22:50:38 2026 +0000
Fix rtl string qa NULL-deref crashes (test bug + source hardening) (#486)
* 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]>
* use guards only in debug mode.
* improved comment by elaborate that it has been the former test and not
just The former
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
main/sal/qa/rtl/ostring/rtl_str.cxx | 144 +++++++++++++++++----
main/sal/qa/rtl/ostring/rtl_string.cxx | 9 +-
main/sal/qa/rtl/oustring/rtl_ustr.cxx | 221 ++++++++++++++++++++++++++-------
main/sal/rtl/source/strtmpl.c | 78 ++++++++++++
main/sal/rtl/source/ustring.c | 64 +++++++++-
5 files changed, 447 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..1d79f90757 100644
--- a/main/sal/qa/rtl/ostring/rtl_str.cxx
+++ b/main/sal/qa/rtl/ostring/rtl_str.cxx
@@ -37,13 +37,34 @@ namespace rtl_str
TEST_F(compare, compare_000)
{
- rtl_str_compare( NULL, NULL);
+ // A former version of this 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)
{
+ // A former version of this 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 +100,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 +165,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 +243,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 +289,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 +329,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 +370,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 +421,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 +481,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 +512,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 +552,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 +581,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 +611,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 +640,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 +670,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 +760,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..e27a52f4de 100644
--- a/main/sal/rtl/source/strtmpl.c
+++ b/main/sal/rtl/source/strtmpl.c
@@ -55,6 +55,63 @@ 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. In non-product builds (OSL_DEBUG_LEVEL */
+/* > 0) we diagnose it loudly via OSL_PRECOND and then fall back to defined */
+/* behaviour so the diagnostic build never dereferences a NULL pointer. */
+/* */
+/* In product builds the guards compile away entirely: callers must honour */
+/* the documented non-NULL contract, and we add zero release-build cost */
+/* (not even a pointer test) to these hot string primitives. */
+/* */
+/* Even in debug, the guards run exactly once, at function entry: they are */
+/* OUTSIDE the per-character processing loops, so they do not change */
+/* string-processing throughput. */
+/* ======================================================================= */
+
+#define IMPL_RTL_STR_GUARD_MSG \
+ "rtl string function: NULL pointer passed; the documented contract " \
+ "requires a non-NULL, null-terminated string"
+
+#if OSL_DEBUG_LEVEL > 0
+
+/* 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)
+
+#else /* product build: guards compile away, callers must honour contract */
+
+#define IMPL_RTL_STR_NULL_AS_EMPTY( pStr ) ((void)0)
+#define IMPL_RTL_STR_NULL_RETURN( pStr, _ret ) ((void)0)
+#define IMPL_RTL_STR_NULL_RETURN_VOID( pStr ) ((void)0)
+
+#endif
+
/* ======================================================================= */
/* C-String functions which could be used without the String-Class */
/* ======================================================================= */
@@ -62,6 +119,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 +134,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 +230,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 +392,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 +594,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 +626,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 +658,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 +775,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 )
{