You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openoffice.apache.org by pe...@apache.org on 2026/06/21 04:29:47 UTC

(openoffice) branch Fix-rtl-string-qa-NULL-deref-crashes-(test-bug-+-source-hardening) created (now 13dc00f46b)

This is an automated email from the ASF dual-hosted git repository.

leginee pushed a change to branch Fix-rtl-string-qa-NULL-deref-crashes-(test-bug-+-source-hardening)
in repository https://gitbox.apache.org/repos/asf/openoffice.git


      at 13dc00f46b Fix rtl string qa NULL-deref crashes (test bug + source hardening)

This branch includes the following new commits:

     new 13dc00f46b Fix rtl string qa NULL-deref crashes (test bug + source hardening)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(openoffice) 01/01: Fix rtl string qa NULL-deref crashes (test bug + source hardening)

Posted by pe...@apache.org.
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 <pe...@apache.org>
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 <no...@anthropic.com>
---
 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 )
     {