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 )
     {

Reply via email to