No strcpy either
217 points
23 hours ago
| 15 comments
| daniel.haxx.se
| HN
Tharre
19 hours ago
[-]
It's worth noting that strcpy() isn't just bad from a security perspective, on any CPU that's not completely ancient it's bad from a performance perspective as well.

Take the best case scenario, copying a string where the precise length is unknown but we know it will always fit in, say, 64 bytes.

In earlier days, I would always have used strcpy() for this task, avoiding the "wasteful" extra copies memcpy() would make. It felt efficient, after all you only replace a i < len check with buf[i] != null inside your loop right?

But of course it doesn't actually work that way, copying one byte at a time is inefficient so instead we copy as many as possible at once, which is easy to do with just a length check but not so easy if you need to find the null byte. And on top of that you're asking the CPU to predict a branch that depends completely on input data.

reply
amelius
19 hours ago
[-]
We should just move away from null-terminated strings, where we can, as fast as we can.
reply
masklinn
18 hours ago
[-]
We have. C is basically the only langage in any sort of widespread use where terminated strings are a thing.

Which of course causes issues when languages with more proper strings interact with C but there you go.

reply
saghm
13 hours ago
[-]
Given that the C ABI is basically the standard for how arbitrary languages interact, I wouldn't characterize all of the headaches this can cause as just when other languages interact with C; arguably it can come up when any two languages interact at all, even if neither are C.
reply
tialaramex
11 hours ago
[-]
Arguably the C ABI was one of those Worse is Better problems like the C language itself. Better languages already existed, but C was basically free and easy to implement, so now there's C everywhere. It seems likely that if not for this ABI we might have an ABI today where all languages which want to offer FFI can agree on how to represent say the immutable slice reference type (Rust's &[T], C++ std::span)

Just an agreed ABI for slices would be enough that language A's growable array type (Rust's Vec, C++ std::vector, but equally the ArrayList or some languages even call this just "list") of say 32-bit signed integers can give a (read only) reference to language B to look at all these 32-bit signed integers without language's A and B having to agree how growable arrays work at all. In C today you have to go wrestle with the ABI pig for much less.

reply
thayne
17 hours ago
[-]
We should move away from it in C usage as well.

Ideally, the standard would include a type that packages a string with its length, and had functions that used that type and/or took the length as an argument. But even without that it is possible avoid using null terminated strings in a lot of places.

reply
BobbyTables2
12 hours ago
[-]
The standard C library can’t even manipulate NUL terminated strings for common use cases…

Simple things aren’t simple - want to append a formatted string to an existing buffer? Good luck! Now do it with UTF-8!

I truly feel the standard library design did more disservice to C than the language definition itself.

reply
throwaway2037
2 hours ago
[-]
Doesn't C++'s std::string also use a null terminated char* string internally? Do you count that also?
reply
zabzonk
32 minutes ago
[-]
Since C++11 it is required to be null-terminated, you can access the terminator with (for e.g.) operator[], and the string can contain non-terminator null characters.
reply
anal_reactor
1 hour ago
[-]
This doesn't count because it's implemented in a way "if you don't need null-terminated string, you won't see it".
reply
ofalkaed
3 hours ago
[-]
>Which of course causes issues when languages with more proper strings interact with C but there you go.

Is is an issue of "more proper strings" or just languages trying to have their cake and eat it too? have their sense of a string and C interoperability. I think this is were we see the strength of Zig, it's strings are designed around and extend the C idea of string instead of just saying our way is better and we are just going to blame C for any friction.

My standard disclaimer comes into play here, I am not a programmer and very much a humanities sort, I could be completely missing what is obvious. Just trying to understand better.

Edit: That was not quite right, Zig has its string literal for C compatibility. There is something I am missing here in my understanding of strings in the broader sense.

reply
raverbashing
19 hours ago
[-]
Yes

And maybe even have a (arch dependent) string buffer zone where the actual memory length is a multiple of 4 or even 8

reply
samshine
17 hours ago
[-]
I haven't seen a strcpy use a scalar loop in ages. Is this an ARM thing?
reply
amluto
5 hours ago
[-]
Modern x86 CPUs have actual instructions for strcpy that work fairly well. There were several false starts along the way, but the performance is fine now.
reply
adrian_b
3 hours ago
[-]
They have instructions for memcpy/memmove (i.e. rep movs), not for strcpy.

They also have instructions for strlen (i.e. rep scasb), so you could implement strcpy with very few instructions by finding the length and then copying the string.

Executing first strlen, then validating the sizes and then copying with memcpy if possible is actually the recommended way for implementing a replacement for strcpy, inclusive in the parent article.

On modern Intel/AMD CPUs, "rep movs" is usually the optimal way to implement memcpy above some threshold of data size, e.g. on older AMD Zen 3 CPUs the threshold was 2 kB. I have not tested more recent CPUs to see if the threshold has diminished.

On the old AMD Zen 3 there was also a certain size range above 2 kB at sizes comparable with the L3 cache memory where their implementation interacted somehow badly with the cache and using "non-temporal" vector register transfers outperformed "rep movs". Despite that performance bug for certain string lengths, using "rep movs" for any size above 2 kB gave a good enough performance.

More recent CPUs might be better than that.

reply
amluto
10 minutes ago
[-]
Whoops, this proves I’m not really a userspace assembly programmer…

But you can indeed safely read past the end if a buffer if you don’t cross a page boundary and you aren’t bound by the rules of, say, C.

reply
gizmo686
3 hours ago
[-]
X86-64 has the REP prefix for string operation. When combined with the MOVS instruction, that is pretty much an instruction for strcpy.
reply
messe
2 hours ago
[-]
No, it's an instruction for memcpy. You still need to compute the string length first, which means touching every byte individually because you can't use SIMD due to alignment assumptions (or lack thereof) and the potential to touch uninitialized or unmapped memory (when the string crosses a page boundary).
reply
ncruces
1 hour ago
[-]
You do aligned reads, which can't crash.

Not even musl uses a scalar loop, if it can do aligned reads/writes: https://git.musl-libc.org/cgit/musl/tree/src/string/stpcpy.c

And you don't need to worry about C UB if you do it in ASM.

reply
manwe150
16 hours ago
[-]
The spec and some sanitizers use a scalar loop (because they need to avoid mistakenly detecting UB), but real world libc seem unlikely to use a scalar loop.
reply
t43562
21 hours ago
[-]
I've always wondered at the motivatons of the various string routines in C - every one of them seems to have some huge caveat which makes them useless.

After years I now think it's essential to have a library which records at least how much memory is allocated to a string along with the pointer.

Something like this: https://github.com/msteinert/bstring

reply
Someone
18 hours ago
[-]
> I've always wondered at the motivatons of the various string routines in C

This idiom:

    char hostname[20];
    ...
    strncpy( hostname, input, 20 );
    hostname[19]=0;
exists because strncpy was invented for copying file names that got stored in 14-byte arrays, zero terminated only if space permitted (https://stackoverflow.com/a/1454071)
reply
masklinn
18 hours ago
[-]
Technically strncpy was invented to interact with null-padded fixed-size strings in general. We’ve mostly (though not entirely) moved away from them but fixed-size strings used to be very common. You can see them all over old file formats still.
reply
messe
2 hours ago
[-]
I've always assumed that the n in strncpy was meant to signify a max length N. Now I'm wondering if it might have stood for NUL padding.
reply
BobbyTables2
12 hours ago
[-]
It’s also horrible because each project ends up reinventing their own abstractions or solutions for dealing with common things.

Destroys a lot of opportunity for code reuse / integration. Especially within a company.

Alternatively their code base remains a steaming pile of crap riddled with vulnerabilities.

reply
jerf
19 hours ago
[-]
Yes, not having a length along with the string was a mistake. It dates from an era where every byte was precious and the thought of having two bytes instead of one for length was a significant loss.

I have long wondered how terrible it would have been to have some sort of "varint" at the beginning instead of a hard-coded number of bytes, but I don't have enough experience with that generation to have a good feel for it.

reply
CupricTea
15 hours ago
[-]
>every one of them seems to have some huge caveat which makes them useless

They were added into C before enough of the people designing it knew the consequences they would bring. Another fundamentally broken oversight is array-to-pointer demotion in function signatures instead of having fat pointer types.

reply
lesuorac
20 hours ago
[-]
It's from a time before computer viruses no?

But also all of this book-keeping takes up extra time and space which is a trade-off easily made nowadays.

reply
rini17
20 hours ago
[-]
Yes, in the old times if you crashed a program or whole computer with invalid input, it was your fault.

Viruses did exist, and these were considered users' fault too.

reply
formerly_proven
21 hours ago
[-]
strncpy is fairly easy, that's a special-purpose function for copying a C string into a fixed-width string, like typically used in old C applications for on-disk formats. E.g. you might have a char username[20] field which can contain up to 20 characters, with unused characters filled with NULs. That's what strncpy is for. The destination argument should always be a fixed-size char array.

A couple years ago we got a new manual page courtesy of Alejandro Colomar just about this: https://man.archlinux.org/man/string_copying.7.en

reply
Cyph0n
21 hours ago
[-]
strncpy doesn’t handle overlapping buffers (undefined behavior). Better to use strncpy_s (if you can) as it is safer overall. See: https://en.cppreference.com/w/c/string/byte/strncpy.html.

As an aside, this is part of the reason why there are so many C successor languages: you can end up with undefined behavior if you don’t always carefully read the docs.

reply
formerly_proven
18 hours ago
[-]
> strncpy doesn’t handle overlapping buffers (undefined behavior).

It would make little sense for strncpy to handle this case, since, as I pointed out above, it converts between different kinds of strings.

reply
Asooka
20 hours ago
[-]
Back when strncpy was written there was no undefined behaviour (as the compiler interprets it today). The result would depend on the implementation and might differ between invocations, but it was never the "this will not happen" footgun of today. The modern interpretation of undefined behaviour in C is a big blemish on the otherwise excellent standards committee, committed (hah) in the name of extremely dubious performance claims. If "undefined" meaning "left to the implementation" was good enough when CPU frequency was measured in MHz and nobody had more than one, surely it is good enough today too.

Also I'm not sure what you mean with C successor languages not having undefined behaviour, as both Rust and Zig inherit it wholesale from LLVM. At least last I checked that was the case, correct me if I am wrong. Go, Java and C# all have sane behaviour, but those are much higher level.

reply
Cyph0n
19 hours ago
[-]
The problem isn't undefined behavior per se; I was using it as an example for strncpy. Rust is a no - in fact, the goal of (safe) Rust is to eliminate undefined behavior. Zig on the other hand I don't know about.

In general, I see two issues at play here:

1. C relies heavily on unsized pointers (vs. fat pointers), which is why strncpy_s had to "break" strncpy in order to improve bounds checks.

2. strncpy memory aliasing restrictions are not encoded in the API and can only be conveyed through docs. This is a footgun.

For (1), Rust APIs of this type operate on sized slices, or in the case of strings, string slices. Zig defines strings as sized byte slices.

For (2), Rust enforces this invariant via the borrow checker by disallowing (at compile-time) a shared slice reference that points to an overlapping mutable slice reference. In other words, an API like this is simply not possible to define in (safe) Rust, which means you (as the user) do not need to pore over the docs for each stdlib function you use looking for memory-related footguns.

reply
loeg
11 hours ago
[-]
> For (2), Rust enforces this invariant via the borrow checker by disallowing (at compile-time) a shared slice reference that points to an overlapping mutable slice reference.

At least the last time I cared about this, the borrow checker wouldn't allow mutable and immutable borrows from the same underlying object, even if they did not overlap. (Which is more restrictive, in an obnoxious way.)

reply
Cyph0n
10 hours ago
[-]
Do you mean borrows for different fields of a struct? If so, that’s handled today - it’s sometimes called “splitting borrows”: https://doc.rust-lang.org/nomicon/borrow-splitting.html
reply
loeg
10 hours ago
[-]
Not exactly -- independent subranges of the same range (as would be relevant to something like memcpy/memmove/strcpy). E.g.,

https://godbolt.org/z/YhGajnhEG

It's mentioned later in the same article you shared above.

reply
oneshtein
1 hour ago
[-]

  fn f() {
    let mut v = vec![1, 2, 3, 4, 5];
    let (header, tail) = v.split_at_mut(1);
    b(&header[0], &mut tail[0]);
  }
reply
Cyph0n
10 hours ago
[-]
Gotcha. There is a split_at_mut method that splits a mutable slice reference into two. That doesn’t address the problem you had, but I think that’s best you can do with safe Rust.
reply
loeg
9 hours ago
[-]
Yeah. It just isn't something the borrow checker natively understands.
reply
tialaramex
11 hours ago
[-]
Rust safe subset doesn't have UB. At all. So long as you never write the "unsafe" keyword you're fine, the compiler will check you are obeying all of the language rules at all times.

Whereas in C, oops, sorry, you broke a rule you didn't even know existed and so that's Undefined Behaviour left and right. Some of it you could argue falls into the category you're describing, where in a better world it should have been made Implementation Defined, not UB, and too bad. However lots of it is just because the language was designed a very long time ago and prioritized ease of implementation.

If you wish the language was properly defined, you should use (safe) Rust. If you just wish that when you write nonsense the compiler should somehow guess what you meant and do that, you're not actually a programmer, find a practice which suits you better - take up knitting, learn to paint, something like that.

reply
dundarious
20 hours ago
[-]
Yes, these were also common in several wire formats I had to use for market data/entry.

You would think char symbol[20] would be inefficient for such performance sensitive software, but for the vast majority of exchanges, their technical competencies were not there to properly replace these readable symbol/IDs with a compact/opaque integer ID like a u32. Several exchanges tried and they had numerous issues with IDs not being "properly" unique across symbol types, or time (restarts intra-day or shortly before the open were a common nightmare), etc. A char symbol[20] and strncpy was a dream by comparison.

reply
ufo
21 hours ago
[-]
A big footgun with strncpy is that the output string may not be null terminated.
reply
kccqzy
21 hours ago
[-]
Yeah but fixed width strings don’t need null termination. You know exactly how long the string is. No need to find that null byte.
reply
ninkendo
21 hours ago
[-]
Until you pass them as a `char *` by accident and it eventually makes its way to some code that does expect null termination.

There’s languages where you can be quite confident your string will never need null termination… but C is not one of them.

reply
kccqzy
20 hours ago
[-]
You don’t do that by accident. Fixed-width strings are thoroughly outdated and unusual. Your mental model of them is very different from regular C strings.
reply
arka2147483647
18 hours ago
[-]
Sadly, all the bug trackers are full of bugs relating to char*. So you very much do those by accident. And in C, fixed width strings are not in any way rare or unusual. Go to any c codebase you will find stuff like:

   char buf[12];
   sprintf(buf, "%s%s", this, that); // or
   strcat(buf, ...) // or
   strncpy(buf, ...) // and so on..
reply
snickerbockers
16 hours ago
[-]
Thats only really a problem if this and that are coming from an external source and have not been truncated. I really don't see this as any more significant of a problem than all the many high level scripting languages where you can potentially inject code into a variable and interpret it.

There are certainly ways in which the c library could've been better (eg making strncpy handle the case where the source string is longer than n) but ultimately it will always need to operate under the assumption that the people using it are both competent and acting in good faith.

reply
kccqzy
15 hours ago
[-]
When you write such code your mental model is C strings, not fixed-width strings, the intended use case for strncpy.
reply
ninkendo
18 hours ago
[-]
The mental model doesn’t matter, it’s the compiler’s model that is going to bite you. If the compiler doesn’t reject it, it will happen eventually.
reply
Sharlin
21 hours ago
[-]
Good luck though remembering not to pass one to any function that does expect to find a null terminator.
reply
kevin_thibedeau
20 hours ago
[-]
Ignore the prefix and always treat strncpy() as a special binary data operation for an era where shaving bytes on storage was important. It's for copying into a struct with array fields or direct to an encoded block of memory. In that context you will never be dependent on the presence of NUL. The only safe usage with strings is to check for NUL on every use or wrap it. At that point you may as well switch to a new function with better semantics.
reply
masklinn
4 hours ago
[-]
> an era where shaving bytes on storage was important

Fixed size strings don’t save bytes on storage tho, when the bank reserves 20 bytes for first name and you’re called Jon that’s 17 bytes doing fuckall.

What they do is make the entire record fixed size and give every field a fixed relative position so it’s very easy to access items, move record around, reuse allocations (or use static allocation), … cycles is what they save.

reply
integralid
19 hours ago
[-]
That's not a problem with strncpy, right? Fixed width records are a thing of the past, and even then it was only used for on-disk storage.
reply
andrepd
20 hours ago
[-]
Seriously. We have type systems and compilers that help us to not forget these things. It's not the 70s anymore!
reply
dingi
20 hours ago
[-]
Isn't strlcpy the safer solution these days?
reply
jandrese
18 hours ago
[-]
I don't think anybody in this thread read the article.

Strlcpy tries to improve the situation but still has problems. As the article points out it is almost never desirable to truncate a string passed into strXcpy, yet that is what all of those functions do. Even worse, they attempt to run to the end of the string regardless of the size parameter so they don't even necessarily save you from the unterminated string case. They also do loads of unnecessary work, especially if your source string is very long (like a mmaped text file).

Strncpy got this behavior because it was trying to implement the dubious truncation feature and needed to tell the programmer where their data was truncated. Strlcpy adopted the same behavior because it was trying to be a drop in replacement. But it was a dumb idea from the start and it causes a lot of pain unnecessarily.

The crazy thing is that strcpy has the best interface, but of course it's only useful in cases where you have externally verified that the copy is safe before you call it, and as the article points out if you know this then you can just use memcpy instead.

As you ponder the situation you inevitably come to the conclusion that it would have been better if strings brought along their own length parameter instead of relying on a terminator, but then you realize that in order to support editing of the string as well as passing substrings you'll need to have some struct that has the base pointer, length, and possibly a substring offset and length and you've just re-invented slices. It's also clear why a system like this was not invented for the original C that was developed on PDP machines with just a few hundred KB of RAM.

Is it really too late for the C committee to not develop a modern string library that ships with base C26 or C27? I get that they really hate adding features, but C strings have been a problem for over 50 years now, and I'm not advocating for the old strings to be removed or even deprecated at this time. Just that a modern replacement be available and to encourage people to use them for new code.

reply
cyberpunk
17 hours ago
[-]
Do they really need to at this point? Just include bstrlib and stop thinking about it?
reply
jandrese
17 hours ago
[-]
Having an official replacement is the only thing that I think will motivate the majority C programmers to finally switch.
reply
alexfoo
19 hours ago
[-]
Yet software developed in C, with all of the foibles of its string routines, has been sold and running for years with trillions of USD is total sales.

A library that records how much memory is allocated to a string along with the pointer isn't a necessity.

Most people who write in C professionally are completely used to it although the footgun is (and all of the others are) always there lurking.

You'd generally just see code like this:-

    char hostname[20];
    ...
    strncpy( hostname, input, 20 );
    hostname[19]=0;
The problem obviously comes if you forget the line to NUL that last byte AND you have a input that is greater than 19 characters long.

(It's also very easy to get this wrong, I almost wrote `hostname[20]=0;` first time round.)

I remember debugging a problem 20+ years ago on a customer site with some software that used Sybase Open/Server that was crashing on startup. The underlying TDS communications protocol (https://www.freetds.org/tds.html) had a fixed 30 byte field for the hostname and the customer had a particularly long FQDN that was being copied in without any checks on its length. An easy fix once identified.

Back then though the consequences of a buffer overrun were usually just a mild annoyance like a random crash or something like the Morris worm. Nowadays such a buffer overrun is deadly serious as it can easily lead to data exfiltration, an RCE and/or a complete compromise.

Heartbleed and Mongobleed had nothing to do with C string functions. They were both caused by trusting user supplied payload lengths. (C string functions are still a huge source of problems though.)

reply
__float
19 hours ago
[-]
> Yet software developed in C, with all of the foibles of its string routines, has been sold and running for years with trillions of USD is total sales.

This doesn't seem very relevant. The same can be said of countless other bad APIs: see years of bad PHP, tons of memory safety bugs in C, and things that have surely led to significant sums of money lost.

> It's also very easy to get this wrong, I almost wrote `hostname[20]=0;` first time round.

Why would you do this separately every single time, then?

The problem with bad APIs is that even the best programmers will occasionally make a mistake, and you should use interfaces (or...languages!) that prevent it from happening in the first place.

The fact we've gotten as far as we have with C does not mean this is a defensible API.

reply
alexfoo
19 hours ago
[-]
Sure, the post I was replying to made it sound like it's a surprise that anything written in C could ever have been a success.

Not many people starting a new project (commercial or otherwise) are likely to start with C, for very good reason. I'd have to have a very compelling reason to do so, as you say there are plenty of more suitable alternatives. Years ago many of the third party libraries available only had C style ABIs and calling these from other languages was clumsy and convoluted (and would often require implementing cstring style strings in another language).

> Why would you do this separately every single time, then?

It was just an illustration or what people used to do. The "set the trailing NUL byte after a strncpy() call" just became a thing lots of people did and lots of people looked for in code reviews - I've even seen automated checks. It was in a similar bucket to "stuff is allocated, let me make sure it is freed in every code path so there aren't any memory leaks", etc.

Many others would have written their own function like `curlx_strcopy()` in the original article, it's not a novel concept to write your own function to implement a better version of an API.

reply
t43562
18 hours ago
[-]
I learned C in about 1989/1990 and have used it a lot since then. I have worked on a fair amount of rotten commercial C code, sold at a high price, in which every millimeter of extra functionality was bought with sweat and blood. I once spent a month finding a memory corruption issue that happened every 2 weeks with a completely different stack trace which, in the end, required a 1-line fix.

The effort was usually out of proportion with the achievement.

I crashed my own computer a lot before I got Linux. Do you remember far pointers? :-( In those days millions of dollars were made by operating systems without memory protection that couldn't address more than 640k of memory. One accepted that programs sometimes crashed the whole computer - about once a week on average.

Despite considering myself an acceptable programmer I still make mistakes in C quite easily and I use valgrind or the sanitizers quite heavily to save myself from them. I think the proliferation of other languages is the result of all this.

In spite of this I find C elegant and I think 90% of my errors are in string handling so therefore if it had a decent string handling library it would be enormously better. I don't really think pure ASCIIZ strings are so marvelous or so fast that we have to accept their bullshit.

reply
alexfoo
13 hours ago
[-]
> I learned C in about 1989/1990 and have used it a lot since then. I have worked on a fair amount of rotten commercial C code, sold at a high price, in which every millimeter of extra functionality was bought with sweat and blood. I once spent a month finding a memory corruption issue that happened every 2 weeks with a completely different stack trace which, in the end, required a 1-line fix.

That sums up one of my old roles where this kind of thing accounted for about 10% of my time over a 10 year period.

Heisenbug, mutating stack traces, weeks between occurrences, 1 line fix, do some other interesting work before the next weird thing comes along.

I think the longest running one I had (several years) was some weird interaction between pthread_cond_wait() and pthread_cond_broadcast(). Ugh.

reply
throw0101c
12 hours ago
[-]
> The underlying TDS communications protocol (https://www.freetds.org/tds.html) had a fixed 30 byte field for the hostname and the customer had a particularly long FQDN that was being copied in without any checks on its length. An easy fix once identified.

I had to file a bug with a vendor because their hostname handling had a similar issue: I think it was 64 max.

There was some pushback about if it was "really" a problem, so I ended up quoting the relevant RFCs to argue that they were not compliant with Internet standards, and eventually they fixed the issue.

reply
saghm
13 hours ago
[-]
> Yet software developed in C, with all of the foibles of its string routines, has been sold and running for years with trillions of USD is total sales.

Even with the premise that sales of software is a good metric for analyzing design of the language (which I think is arguable at best), we don't know that even more money might have been made with better strings in C. You coming justify pretty much anything with that argument. MongoDB (which indicentally is on C++ and presumably makes plenty of use of std::string) made millions of dollars despite having the bug you mention, so why bother fixing it?

reply
bluecalm
17 hours ago
[-]
>>(It's also very easy to get this wrong, I almost wrote `hostname[20]=0;` first time round.)

Impossible to get wrong with a modern compiler that will warn you on that or LSP that will scream the moment you type ; and hit enter/esc.

reply
swinglock
21 hours ago
[-]
I'm surprised curlx_strcopy doesn't return success. Sure you could check if dest[0] != '/0' if you care to, but that's not only clumsy to write but also error prone, and so checking for success is not encouraged.
reply
jutter
21 hours ago
[-]
This is especially bizarre given that he explains above that "it is rare that copying a partial string is the right choice" and that the previous solution returned an error...

So now it silently fails and sets dest to an empty string without even partially copying anything!?

reply
ahoka
12 hours ago
[-]
Yeah, thought the same. Expect some CVEs in the future.
reply
AlexeyBrin
21 hours ago
[-]
I guess the idea is that if the code does not crash at this line:

    DEBUGASSERT(slen < dsize);
it means it succeeded. Although some compilers will remove the assertions in release builds.

I would have preferred an explicit error code though.

reply
swinglock
20 hours ago
[-]
assert() is always only compiled if NDEBUG is not defined. I hope DEBUGASSERT is just that too because it really sounds like it, even more so than assert does.

But regardless of whether the assert is compiled or not, its presence strongly signals that "in a C program strcpy should only be used when we have full control of both" is true for this new function as well.

reply
ahoka
19 hours ago
[-]
"strncpy() is a weird function with a crappy API."

Well if you bother looking up that it's originally created for non null-terminated strings, then it kinda makes sense.

The real problem begun when static analyzers started to recommend using it instead of strcpy (the real alternative used to be snprintf, now strlcpy).

reply
manwe150
17 hours ago
[-]
strlcpy is a BSD-ism that isn't in posix. The official recommendation is stpecpy. Unfortunately, it is only implemented in the documentation, but not available anywhere unless you roll your own:

https://man7.org/linux/man-pages/man7/string_copying.7.html

reply
bentley
16 hours ago
[-]
reply
manwe150
16 hours ago
[-]
Ah, good point. I forgot it had just gotten added. Past context https://news.ycombinator.com/item?id=36765747
reply
tptacek
16 hours ago
[-]
Who cares? Just vendor it into your project. It's a tiny string manipulation function.

(I agree with the author of the piece that strlcpy doesn't actually solve the real problem.)

reply
tourist2d
18 hours ago
[-]
Your comment makes no sense. If it was designed for non-null terminated strings, why would it specifically pad after a null terminator?

I looked up the actual reason for its inception:

---

    Rationale for the ANSI C Programming Language", Silicon Press 1990.

    4.11.2.4 The strncpy function
    strncpy was initially introduced into the C library to deal with fixed-length name fields in structures such as directory entries. Such fields are not used in the same way as strings: the trailing null is unnecessary for a maximum-length field, and setting trailing bytes for shorter names to null assures efficient field-wise comparisons. strncpy is not by origin a "bounded strcpy," and the Committee has preferred to recognize existing practice rather than alter the function to better suit it to such use.
reply
masklinn
15 hours ago
[-]
> If it was designed for non-null terminated strings, why would it specifically pad after a null terminator?

Padded and terminated strings are completely different beasts. And the text you quote tells you black on white that strncpy deals in padded strings.

reply
bentley
16 hours ago
[-]
“fixed-length name fields in structures such as directory entries”

“the trailing null is unnecessary for a maximum-length field”

That is a non–null terminated string.

reply
loeg
21 hours ago
[-]
A weird Annex-K like API. The destination buffer size includes space for the trailing nul, but the source size only includes non-nul string bytes.

I don't really think this adds anything over forcing callers to use memcpy directly, instead of strcpy.

reply
Scubabear68
22 hours ago
[-]
From the article:

> It has been proven numerous times already that strcpy in source code is like a honey pot for generating hallucinated vulnerability claims

This closing thought in the article really stood out to me. Why even bother to run AI checking on C code if the AI flags strcpy() as a problem without caveat?

reply
CGamesPlay
21 hours ago
[-]
It's not quite as black and white as the article implies. The hallucinated vulnerability reports don't flag it "without caveat", they invent a convoluted proof of vulnerability with a logical error somewhere along the way, and then this is what gets submitted as the vulnerability report. That's why it's so agitating for the maintainers: it requires reading a "proof" and finding the contradiction.
reply
Sharlin
20 hours ago
[-]
Because these people who run AI checks on OSS code and submit bogus bug reports either assume that AIs don't make mistakes, or just don't care if the report is legit or not, because there's little to no personal cost to them even if it isn't.
reply
saagarjha
21 hours ago
[-]
Because people are stupid and use AI for things it is not good at.
reply
Tempest1981
21 hours ago
[-]
> people are stupid

people overestimate AI

reply
lesuorac
20 hours ago
[-]
Its weird though because looking through the hackone reports in the slop wiki page there aren't actually reproduction steps. It's basically always just a line of code and an explanation of how a function can be mis-used but not a "make a webserver that has this hardcoded response".

So like why doesn't the person iterate with the AI until they understand the bug (and then ultimately discover it doesn't exist)? Like have any of this bug reports actually paid out? It seems like quickly people should just give up from a lack of rewards.

reply
zahlman
19 hours ago
[-]
> So like why doesn't the person iterate with the AI until they understand the bug (and then ultimately discover it doesn't exist)? Like have any of this bug reports actually paid out? It seems like quickly people should just give up from a lack of rewards.

This sounds a bit like expecting the people who followed a "make your own drop-shipping company" tutorial to try using the products they're shipping to understand that they suck.

reply
amenhotep
20 hours ago
[-]
As long as the number of people newly being convinced that AI generated bounty demands are a good way to make money equals or exceeds the number of people realising it isn't and giving up, the problem remains.

Not helped, I imagine, that once you realise it doesn't work, an easy pivot is to start convincing new people that it'll work if they pay you money for a course on it.

reply
zahlman
19 hours ago
[-]
Apparently FOSS developers have been getting this kind of slop report even though they clearly don't offer a bug bounty.
reply
pixl97
17 hours ago
[-]
There are no shortage of people wanting to be able to say they found CVE-XXXX-XXX or a bug in product X.
reply
Waterluvian
19 hours ago
[-]
> Enforce checks close to code

This makes a lot of sense but one time I find this gets messy is when there’s times I need to do checks earlier in a dataset’s lifetime. I don’t want to pay to check multiple times, but I don’t want to push the check up and it gets lost in a future refactor.

I’m imagining a metadata for compile time that basically says, “to act on this data it must have been first checked. I don’t care when, so long as it has been by now.” Which I’m imagining is what Rust is doing with a Result type? At that point it stops mattering how close to code a check is, as long as you type distinguish between checked and unchecked?

reply
masklinn
4 hours ago
[-]
> Which I’m imagining is what Rust is doing with a Result type?

Result only carries information about the success / failure of an unspecified operation, it is not a long term signal and furthermore is not resistant to tampering (so a mistake processing the Result can undo the validation).

What you want in this case is a new separate type, which can only be constructed through the check operation. This is the ethos of "parse, don't validate".

And you're correct that in that case you don't need the check to be close to the consumer, in fact you want the opposite, for the check to be as close to the software edge as possible such that tainted data has limited to no presence inside the system and it's difficult or impossible to unwittingly interact with it.

But of course the farther into that direction you head the more expressive a type system you need. And some constraints are not so easily checked as there's a multitude of consumers each with their own foibles, or as in this case you need to check the interaction of multiple runtime objects.

reply
deepsun
18 hours ago
[-]
I'd use different types for those. Like Java's String vs. CharSequence.
reply
rf15
2 hours ago
[-]
it feels like the arguments' off-by-one buffer size vs string length is horrible ergonomics and will probably lead to further usage errors in the future.

Yes I have a degree in bike shedding, why am I always getting this particular question

reply
zahlman
20 hours ago
[-]
> To make sure that the size checks cannot be separated from the copy itself we introduced a string copy replacement function the other day that takes the target buffer, target size, source buffer and source string length as arguments and only if the copy can be made and the null terminator also fits there, the operation is done.

... And if the copy can't be made, apparently the destination is truncated as long as there's space (i.e., a null terminator is written at element 0). And it returns void.

I'm really not sold on that being the best way to handle the case where copying is impossible. I'd think that's an error case that should be signaled with a non-zero return, leaving the destination buffer alone. Sure, that's not supposed to happen (hence the DEBUGASSERT macro), but still. It might even be easier to design around that possibility rather than making it the caller's responsibility to check first.

reply
stabbles
21 hours ago
[-]
Apart from Daniel Sternberg's frequent complaints about AI slop, he also writes [1]

> A new breed of AI-powered high quality code analyzers, primarily ZeroPath and Aisle Research, started pouring in bug reports to us with potential defects. We have fixed several hundred bugs as a direct result of those reports – so far.

[1] https://daniel.haxx.se/blog/2025/12/23/a-curl-2025-review/

reply
molf
21 hours ago
[-]
reply
p2detar
21 hours ago
[-]
So? Those are automated analysis tools and by "slop" he seems to refer to careless reports crafted using AI, solely for collecting bounties:

https://gist.github.com/bagder/07f7581f6e3d78ef37dfbfc81fd1d...

reply
pama
22 hours ago
[-]
Congrats on the completion of this effort! C/C++ can be memory safe but take some effort.

IMHO the timeline figure could benefit in mobile from using larger fonts. Most plotting libraries have horrible font size defaults. I wonder why no library picked the other extreme end: I have never seen too large an axis label yet.

reply
saagarjha
21 hours ago
[-]
Removing strcpy from your code does not make it memory safe.
reply
pama
16 hours ago
[-]
Apologies. I never meant to imply that of course. It is a long and arduous process, and this is but a single tiny step.
reply
kjjfnkeknrn
20 hours ago
[-]
Removing strcpy from your code does make it a little memory safer.
reply
alexfoo
19 hours ago
[-]
Removing strcpy from your code makes it a little less memory unsafe.

(Depends on what you replace it with obviously...)

reply
Tempest1981
21 hours ago
[-]
Yes, the graph font-sizes seem intended for printing them on a single sheet of paper, vs squeezed into a single column in a blog.
reply
self_awareness
4 hours ago
[-]
Bikeshedding.
reply
snvzz
22 hours ago
[-]
The AI chatbot vulnerability reports part sure is sad to read.

Why is this even a thing and isn't opt-in?

I dread the idea of starting to get notifications from them in my own projects.

reply
trollbridge
21 hours ago
[-]
Making a strcpy honeypot doesn’t sound like a bad idea…

  void nobody_calls_me(const char *stuff) {
          char *a, *b;
          const size_t c = 1024;

          a = calloc(c);
          if (!a) return;
          b = malloc(c);
          if (!b) {
                  free(a);
                  return;
          }
          strncpy(a, stuff, c - 1);
          strcpy(b, a);
          strcpy(a, b);
          free(a);
          free(b);
  }
Some clever obfuscation would make this even more effective.
reply
snvzz
19 hours ago
[-]
That got those Core SDI abo vibes.

Flashback of writing exploits for these back in high school.

reply
easterncalculus
20 hours ago
[-]
It's a symptom of complete failure of this industry that maintainers are even remotely thinking about, much less implementing changes in their work to stave off harassment over false security impact from bots.
reply
Y_Y
22 hours ago
[-]
Because humans generate and relay the slop-reports in the hopes of being helpful
reply
nottorp
19 hours ago
[-]
There is or was a cash bug bounty.

And even if not, the motivation is building a reputation as a security “expert”.

reply
captn3m0
22 hours ago
[-]
s/being helpful/making money.
reply
senthil_rajasek
22 hours ago
[-]
Title is :

No strcpy either

@dang

reply
Snild
22 hours ago
[-]
I don't see a problem with that, but for the record, the title on the site is lower-case for me (both browser tab title, and the header when in reader mode).
reply
1f60c
21 hours ago
[-]
I think the submission originally had a typo ("strpy", with no C)
reply
Snild
20 hours ago
[-]
Ah.
reply
TZubiri
21 hours ago
[-]
LMAO

After all this time the initial AI Slop report was right:

https://hackerone.com/reports/2298307

reply
lesuorac
20 hours ago
[-]
?

Nonce and websockets don't appear at all in the blog post. The only thing the ai slop got right is that by removing strcpy curl will get less issues [submitted about it].

reply