C strings aren't bad. They can't be, because they don't exist. C doesn't have strings. And that is the issue.
As you say, things get a lot better when you actually introduce strings as a concrete concept rather than a set of lose conventions.
So that strcmp expects a string rather than a pointer to a character is just convention. In languages which actually has strings as a concrete concept, like say Pascal and derivatives, you can actually differentiate between those two cases.
[1]: https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#...
char s[3] = "foo"; // not null-terminated!Byte count.
In practice, in C context, character == char == byte. Other concepts have to use different names to avoid confusion with the language spec.
C has string literals though, and those bake a specific string representation into the language (of course libraries can use their own string representation, but those then need at least some conversion function from string literals).
WUFFS doesn't have strings. That's what a language which doesn't have strings looks like, you can't write "Hello, world" in WUFFS because it involves Strings, which WUFFS doesn't have, and I/O, which WUFFS also doesn't have.
A pretence that C doesn't have strings because it lacks a concrete string type in the language itself also seems like you'd be claiming C++ doesn't have strings, Zig doesn't have strings, and Rust came pretty close to not having strings (for a while it was mooted to make Rust's str just a slice [u8] but today Rust does bless str as a distinct type even though e.g. &str and &[u8] aren't very different)
Zig and Rust I don't know enough about.
And I'm not pretending. C has string literals which are of a non-distinct type. You can't distinguish between a string literal and an array of characters. This is the crucial bit.
The result is that the standard library, and lots of other code, relies on convention alone to pass strings around. This has been and continues to be the source for countless serious bugs. The kind of bugs which are a total non-issue in languages which has strings.
[1]: https://en.cppreference.com/w/cpp/language/user_literal
But it doesn't have to stay that way. Someone else in the comments mentioned antirez's sds library for dynamic strings. This works, but you could also easily roll your own. All you need is an init function, and perhaps an assert or other check at the end of it that the string has a nul terminator.
At that point, type checking will let you blindly pass those strings (or their char arrays) to any of those C functions without worry.