Foundation crash on macOS

Somebody should really look into this Swift 6.0.3 macOS bus error:

import Foundation
let snafu = String(format: "%-25s %-15s %-10s", "XXXX", "YYYYYYY", "ZZZZZZ")

Frame zero has the bus error in _platform_strlen.

You need to use %@ instead of %s as your format placeholder to print String parameters through the variadic format APIs.

%s requires that the parameter in the corresponding position be a C string—a pointer to a NUL-terminated array of C characters. But when you pass in a string literal like "XXXX", you're passing a Swift String struct value.

The variadic arguments following the format string in String(format:) have the type any CVarArg. Inside Foundation, String was made to conform to CVarArg such that using it in a varargs list bridges it over to NSString from Objective-C and passes that instead. So this would be equivalent to the Objective-C code:

[NSString stringWithFormat:"%-25s %-15s %-10s", @"XXXX", @"YYYYYYY", @"ZZZZZZ"]

which would similarly crash because it would try to treat an NSString * value as a C string. %@ is the Objective-C object placeholder, which works correctly.

It's unfortunate that this (easy to make) mistake can cause a crash, but that's the nature of the beast when C varargs are involved.

4 Likes

Why is there no type conflict warning from swiftc? If I try to compile C code that specifies %u but I pass in a long parameter, I'll get a warning at least:

% cat foo.c
#include <stdio.h>
int main() {
	printf("%u\n", 1000000L);
	return 0;
}
% gcc foo.c 
foo.c:3:17: warning: format specifies type 'unsigned int' but the argument has type 'long' [-Wformat]
    3 |         printf("%u\n", 1000000L);
      |                 ~~     ^~~~~~~~
      |                 %ld
1 warning generated.

Many C compilers have added custom attributes/logic that let it type-match the varargs arguments based on the placeholder strings, by baking in knowledge of those specific strings.

Theoretically the Swift compiler could do something similar if it wanted, but I think the argument would be why it makes sense to invest in type-checking hacks for unsafe legacy APIs instead of just using better type-safe APIs in the first place.

3 Likes

Is it really a "hack" though, if multiple compilers implement it just fine, and have offered that feature for years?

As for unsafeness, they could simply remove %s and only support %@ and the unsafeness would be gone.

There are a large variety of sources of unsafety with the String(format:) API, removing just %s would be insufficient. There are many, many combinations of mismatches (some of which get obfuscated away by Swift's APIs such as Int having varying width on different platforms). As @allevato mentioned the C compiler has additional checking at compile time to do its best to identify mismatches between format specifiers and arguments which we do not have in the Swift compiler. However in Swift, which is designed to be a memory safe language, we instead have completely safe alternatives rather than safe guards put up around unsafe APIs. In this case, that safe API is string interpolations. I believe the code you have above can be written as a combination of string interpolations plus using the String APIs provided to pad to a certain length, all without any possibility for unsafety.

In general in Swift it's a non-goal to make String(format:) safe - rather instead I'd encourage using safe replacements for String(format:) where possible such as string interpolations (or String(localized:) on Apple platforms for localized strings).

3 Likes