Hi,

I think the quoted bit of the code is correct, it’s the other end that’s wrong. 
 The invariant for GSTinyString should be that *if* byte 9 exists, it is not 
stored in the string, it is null.  When reading back (`-characterAtIndex:` or 
`-getCharacters:range:`), this should be enforced, but it sounds as if it’s 
reading byte 9 via address arithmetic instead.

David

> On 11 Nov 2026, at 00:35, [email protected] wrote:
> 
> Fuzzing gnustep-base finding #1
> 
> This seems legitimate to me. The unit test fails before the fix
> and passes after. 
> 
> Full patch available here : 
> https://github.com/poroussel/libs-base/commit/bbbd327b2ab6a5b7747091261eedbb4e4dbf428e
> 
> Let me know if this is useful,
> Philippe
> 
> ---
> 
> diff --git a/Source/GSString.m b/Source/GSString.m
> index c102cb73a..b142a9567 100644
> --- a/Source/GSString.m
> +++ b/Source/GSString.m
> @@ -1194,20 +1194,16 @@ createTinyString(const char *str, int length)
>       return nil;
>     }
> 
> -  /* String too long to fit in a pointer, give up
> +  /* String too long to fit in a pointer, give up.
> +   * Only eight 7-bit characters fit; a ninth character (even a nul)
> +   * would overlap the length and tag bits and produce a string whose
> +   * length is 9 while all the methods assume at most 8 characters.
>    */
> -  if (length > 9)
> +  if (length > 8)
>     {
>       return nil;
>     }
> 
> -  /* String would fit if the last byte was an implicit 0, but it isn't.
> -   */
> -  if ((length == 9) && str[8] != '\0')
> -    {
> -      return nil;
> -    }
> -  
>   s |= length << TINY_STRING_LENGTH_SHIFT;
>   for (i = 0 ; i<length ; i++)
>     {
> diff --git a/Tests/base/NSString/tiny_nine_chars.m 
> b/Tests/base/NSString/tiny_nine_chars.m
> new file mode 100644
> index 000000000..7c748e0fe
> --- /dev/null
> +++ b/Tests/base/NSString/tiny_nine_chars.m
> @@ -0,0 +1,45 @@
> +/*
> + * Nine byte ASCII strings whose last byte is a nul must not be stored as
> + * tiny strings: only eight 7-bit characters fit in a pointer, and the
> + * ninth character would overlap the length/tag bits.  Found by fuzzing
> + * GSMimeParser (stack buffer overflow in -[GSTinyString 
> mutableCopyWithZone:]).
> + */
> +#import "Testing.h"
> +#import <Foundation/NSAutoreleasePool.h>
> +#import <Foundation/NSString.h>
> +
> +int main(int argc, char **argv)
> +{
> +  NSAutoreleasePool  *arp = [NSAutoreleasePool new];
> +  const char         *bytes = "abcdefgh";    /* 8 chars + nul */
> +  unichar            chars[9] = {'a','b','c','d','e','f','g','h',0};
> +  NSString           *s;
> +  NSString           *u;
> +  NSMutableString    *m;
> +
> +  START_SET("nine character string ending in nul")
> +
> +  s = AUTORELEASE([[NSString alloc] initWithBytes: bytes
> +                                         length: 9
> +                                       encoding: NSASCIIStringEncoding]);
> +  u = AUTORELEASE([[NSString alloc] initWithCharacters: chars length: 9]);
> +
> +  PASS([s length] == 9, "string has length 9");
> +  PASS([s characterAtIndex: 8] == 0, "ninth character is a nul");
> +  PASS([s isEqual: u] && [u isEqual: s], "equals the unichar version");
> +  PASS([s hash] == [u hash], "hash matches the unichar version");
> +
> +  m = AUTORELEASE([s mutableCopy]);
> +  PASS([m length] == 9 && [m characterAtIndex: 8] == 0,
> +    "mutable copy keeps all nine characters");
> +  PASS_EQUAL(m, u, "mutable copy is equal");
> +  PASS([s longLongValue] == 0, "longLongValue does not overflow");
> +  PASS_EQUAL([s substringToIndex: 8], @"abcdefgh", "substring is correct");
> +  PASS_EQUAL([[s substringToIndex: 8] stringByAppendingString: @"ijklmnop"],
> +    @"abcdefghijklmnop", "appending to the substring works");
> +
> +  END_SET("nine character string ending in nul")
> +
> +  [arp release];
> +  return 0;
> +}
> -- 
> 2.39.5
> 
> 


Reply via email to