Ushering out strlcpy()
In the beginning, copying strings in C was simple. Your editor's dog-eared, first-edition copy of The C Programming Language provides an implementation of strcpy() on page 101:
strcpy(s, t) char *s, *t; { while (*s++ = *t++) ; }
This function has a few shortcomings, the most obvious of which is that it will overrun the destination buffer if the source string is too long. Developers working in C eventually concluded that this could be a problem, so other string-copying functions were developed, starting with strncpy():
char *strncpy(char *dest, char *src, size_t n);
This function will copy at most n bytes from src to
dest, so, if n is no larger than the length of
dest, then that array cannot be overrun. strncpy() has a
couple of quirks, though. It is defined to NUL-fill dest if
src is shorter than n, so it ends up always writing the
full array. If src is longer than n, then dest
will not be NUL-terminated at all — an invitation to trouble if the caller
does not carefully check the return value. That return value is the
address of the first NUL character written to dest unless
src is too long, in which case strncpy() returns
&dest[n] — an address beyond the actual array
dest regardless of whether truncation occurs or not. As a result,
checking for truncation is a bit tricky and often not done. [Thanks to
Rasmus Villemoes for pointing out the error in our earlier description of
the strncpy() return value.]
strlcpy() and strscpy()
The BSD answer to the problems with strncpy() was to introduce a new function called strlcpy():
size_t strlcpy(char *dest, const char *src, size_t n);
This function, too, will copy a maximum of n bytes from src to dest; unlike strncpy(), it will always ensure that dest is NUL-terminated. The return value is always the length of src regardless of whether it was truncated in the copy or not; developers must compare the returned length against n to determine whether truncation has occurred.
The first uses of strlcpy() in the kernel entered briefly during the 2.4 stable series — sort of. The media subsystem had a couple of implementations defined as:
#define strlcpy(dest,src,len) strncpy(dest,src,(len)-1)
As one might imagine, there was not a lot of checking of return values going on at that point. That macro disappeared relatively quickly, but a real strlcpy() implementation appeared in the 2.5.70 release in May 2003; that release also converted many callers in the kernel over to this new function. Everything seemed good for quite some time.
In 2014, though, criticism of strlcpy() started to be heard, resulting in, among other things, an extended discussion over whether to add an implementation to the GNU C library; to this day, glibc lacks strlcpy(). Kernel developers, too, started to feel disenchanted with this API. In 2015, yet another string-copy function was added to the kernel by Chris Metcalf:
ssize_t strscpy(char *dest, const char *src, size_t count);
This function, like the others, will copy src to dest without overrunning the latter. Like strlcpy(), it ensures that the result is NUL-terminated. The difference is in the return value; it is the number of characters copied (without the trailing NUL byte) if the string fits, and -E2BIG otherwise.
Reasons to like strscpy()
Why is strscpy() better? One claimed advantage is the return value, which makes it easy to check whether the source string was truncated or not. There are a few other points as well, though; to get into those, it is instructive to look at the kernel's implementation of strlcpy():
size_t strlcpy(char *dest, const char *src, size_t size) { size_t ret = strlen(src); if (size) { size_t len = (ret >= size) ? size - 1 : ret; memcpy(dest, src, len); dest[len] = '\0'; } return ret; }
One obvious shortcoming is that this function will read the entire source string regardless of whether that data will be copied or not. Given the defined semantics of strlcpy(), this inefficiency simply cannot be fixed; there is no other way to return the length of the source string. This is not just a question of efficiency, though; as recently pointed out by Linus Torvalds, bad things can happen if the source string is untrusted — which is one of the intended use cases for this function. If src is not NUL-terminated, then strlcpy() will continue merrily off the end until it does find a NUL byte, which may be way beyond the source array — if it doesn't crash first.
Finally, strlcpy() is subject to a race condition. The length of src is calculated, then later used to perform the copy and returned to the caller. But if src changes in the middle, strange things could happen; at best the return value will not match what is actually in the dest string. This problem is specific to the implementation rather than the definition, and could thus be fixed, but nobody seems to think it's worth the effort.
The implementation of strscpy() avoids all of these problems and is also more efficient. It is also rather more complex as a result, of course.
The end of strlcpy() in the kernel?
When strlcpy() was first introduced, the intent was to replace all of the strncpy() calls in the kernel and get rid of the latter function altogether. In the 6.0-rc2 kernel, though, there are still nearly 900 strncpy() call sites remaining; that number grew by two in the 6.0 merge window. At the introduction of strscpy(), instead, Torvalds explicitly did not want to see any sort of mass conversion of strlcpy() calls. In 6.0-rc2, there are just over 1,400 strlcpy() calls and nearly 1,800 strscpy() calls.
Nearly seven years later, the attitude seems to have changed a bit;
Torvalds now says that "strlcpy() does need to go
". A number of
subsystems have made conversion passes, and the number of
strlcpy() call sites has fallen by 85 since 5.19. Whether it will
ever be possible to remove strlcpy() entirely is unclear;
strncpy() is still holding strong despite its known hazards and a
decision to get rid of it nearly 20 years ago. Once something gets
into the kernel, taking it out again can be a difficult process.
There may be hope, though, in this case. As Torvalds observed in response to a set of conversions from Wolfram Sang, most of the callers to strlcpy() never use the return value; those could all be converted to strscpy() with no change in behavior. All that would be needed, he suggested, was for somebody to create a Coccinelle script to do the work. Sang rose to the challenge and has created a branch with the conversions done. That work, obviously, won't be considered for 6.0, but might show up in a 6.1 pull request.
That would leave relatively few strlcpy() users in the kernel.
Those could be cleaned up one by one, and it might just be possible to get
rid of strlcpy() entirely. That would end a 20-year sporadic
discussion on the best way to do bounded string copies in the kernel — all
of those remaining strncpy() calls notwithstanding — at
least until some clever developer comes up an even better function and
starts the whole process anew.
Index entries for this article | |
---|---|
Kernel | String processing |
Posted Aug 25, 2022 16:16 UTC (Thu)
by sub2LWN (subscriber, #134200)
[Link] (1 responses)
/still holding string/still holding strong/? strngcpy
Posted Aug 30, 2022 6:51 UTC (Tue)
by milesrout (subscriber, #126894)
[Link]
Posted Aug 25, 2022 16:23 UTC (Thu)
by IanKelling (subscriber, #89418)
[Link] (4 responses)
Funny.
Posted Aug 25, 2022 18:08 UTC (Thu)
by adobriyan (subscriber, #30858)
[Link] (3 responses)
Posted Aug 25, 2022 19:47 UTC (Thu)
by Sesse (subscriber, #53779)
[Link] (2 responses)
Of course, this makes the function much less useful than the average C programmer assumes.
Posted Aug 25, 2022 22:03 UTC (Thu)
by tialaramex (subscriber, #21167)
[Link] (1 responses)
But, on the other hand notice the newer APIs are still awful. The language just doesn't give us much to work with by having this terrible string type. I think we might have been better off if C hadn't admitted to a string type at all (ie there are no string functions, and no "" literal syntax) and then presumably C89 might have introduced one but there would have been a war to decide how that works (since C89 mostly codifies popular real world C implementations) and I can't believe that 0-terminated wins such a war, even if Pascal-style (length-prefixed) wins that's a less awful world than our world.
Posted Aug 26, 2022 0:46 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link]
Posted Aug 25, 2022 17:30 UTC (Thu)
by iabervon (subscriber, #722)
[Link]
Posted Aug 25, 2022 18:15 UTC (Thu)
by adobriyan (subscriber, #30858)
[Link] (4 responses)
No, it doesn't count! It's like saying that strdup() has race condition because it calls strlen() inside. Of course, string must be stable during strlcpy().
Posted Aug 25, 2022 22:04 UTC (Thu)
by roc (subscriber, #30627)
[Link] (3 responses)
Posted Aug 25, 2022 22:42 UTC (Thu)
by tialaramex (subscriber, #21167)
[Link] (2 responses)
Mechanically, reading a byte and then writing the byte works, the data you get might have tearing, but it's still just bytes. Byte 14 of a 20 byte structure you copied might, unaccountably, be 0xF6 even though that's inconsistent with the other bytes, but it can't actually take some eldritch value outside of the 256 possible bit patterns. If you're writing a GTK+ mail app, you just shouldn't think this way, it's going to get you into trouble - but if you're writing an operating system kernel this is far from the shakiest proposition you're relying on.
Anyway, I think the race condition is a practical problem for untrusted input. If I know the kernel will read my program's buffer, find there's a NUL byte in position 814, and then cheerfully copy 814 bytes into some other structure expecting the result to be a 814 byte C-style string, then I can cause some real mischief, which might have been impossible to pull off if the kernel page-copies my entire data structure instead.
Posted Aug 26, 2022 2:12 UTC (Fri)
by dezgeg (subscriber, #92243)
[Link] (1 responses)
Yes, userspace trying to trick the kernel is a valid concern... but I do not get how that is related to strlcpy() topic at all as that function is not anyway safe to call on a userspace pointer!
This whole untrusted and/or racy source argument to strlcpy() is something I don't understand at all... if there is no guarantee on source string being NUL-terminated, then strscpy() will read past the source buffer if it happens to be smaller than the destination buffer.
Posted Aug 26, 2022 7:42 UTC (Fri)
by LtWorf (subscriber, #124958)
[Link]
Posted Aug 25, 2022 18:48 UTC (Thu)
by NYKevin (subscriber, #129325)
[Link] (31 responses)
* They call it String::replace_range(), and it's a method rather than a free function. It also takes an argument describing which range of the string you want to replace (which can be the entire string, if desired).
Posted Aug 25, 2022 20:31 UTC (Thu)
by tialaramex (subscriber, #21167)
[Link] (4 responses)
This won't be obvious to non-Rust people, and might not even be immediately obvious to new Rust programmers, but, in a sense all Rust's functions are/ can be written as free functions.
Here's some idiomatic Rust using a method call:
let mut news = "Linux Weekly News".to_owned();
But while it isn't idiomatic in this case, you could also just write:
String::replace_range(&mut news, ..5, "FreeBSD");
As well as moving news to be the first parameter, we also need to write &mut to show that we want to pass the mutable reference, not move news itself into the replace_range function as a parameter. The method call sugar does that work for us by examining the signature of the String::replace_range function and seeing how the self parameter is used.
It really is just sugar, the code emitted will be identical (modulo perturbing an optimiser somewhere).
This is also how Rust resolves ambiguities. If Goose implements both AirlinePassenger and Bird but both traits define a fly() method, that's no problem in Rust, I can write:
if goose.is_wealthy() { AirlinePassenger::fly(&goose); } else { Bird::fly(&goose); }
In contexts where the code knows about Bird but not AirlinePassenger, goose.fly() is unambiguous and does what you expect, and likewise vice versa.
Posted Aug 26, 2022 7:56 UTC (Fri)
by nyanpasu64 (guest, #135579)
[Link]
Posted Aug 28, 2022 23:45 UTC (Sun)
by KJ7RRV (subscriber, #153595)
[Link] (2 responses)
Posted Aug 30, 2022 17:10 UTC (Tue)
by tialaramex (subscriber, #21167)
[Link] (1 responses)
It's very similar, however whereas self in Python is just a convention, self in Rust is a keyword, using it signals that you mean for this function to be a method, and so the type is implied. Not all functions implemented for a type in Rust have to be methods. Consider these two functions in the implementation of Goose:
fn is_wealthy(&self) -> bool { self.net_worth > 1000 }
fn is_healthy(goose: &Self) -> bool { self.hit_points > 90 }
The is_wealthy function can be used as a method, the self keyword means if I have a Goose named jimmy I can write:
if jimmy.is_wealthy() { println!("Swimming in money"); } else { println!("Aww, poor Jimmy"); }
But the is_healthy function is not a method, it's only an associated function. That capitalized word "Self" is also a keyword, but it just means "my type" and so we could have written Goose instead. So I always need to write:
if Goose::is_healthy(&jimmy) { println!("Feeling good"); } else { println!("Jimmy needs a goose doctor"); }
I believe this distinction does not exist in Python. The reason it is used in Rust is that sometimes a type exists mostly as a "pass through" for some other type, and so by having only associated functions instead of methods it is hidden from accidental method invocations. For example Rc and Arc are smart pointers offering reference counting, but they sport associated functions not methods, so that you need to say e.g. Rc::downgrade(&thing) and thus if thing.downgrade() already means something else the reference counted type doesn't get in your way.
Posted Aug 30, 2022 17:12 UTC (Tue)
by tialaramex (subscriber, #21167)
[Link]
fn is_healthy(goose: &Self) -> bool { goose.hit_points > 90 }
There is no "self" parameter on is_healthy because it isn't a method, and so the version I wrote above can't work since it refers to a self variable which doesn't exist.
Posted Aug 26, 2022 6:43 UTC (Fri)
by wtarreau (subscriber, #51152)
[Link] (21 responses)
Posted Aug 26, 2022 10:30 UTC (Fri)
by tialaramex (subscriber, #21167)
[Link] (20 responses)
However, the byte-oriented code you've written can indeed be expressed in Rust, from Rust's point of view these are just bytes (type u8) in a mutable slice of some sort. Rust knows how big the slice is, (the unforgivable mistake in C is that it doesn't know how big whatever char *hdr is pointing at is) and so we can safely change the bytes within it.
I assume you don't consider the behaviour if hdr is in fact pointing at something that is not a zero-terminated string (ie a buffer overflow with undefined behaviour) to be desirable, and so we don't need Rust's unsafe which is the only way to duplicate that.
https://gist.github.com/rust-play/59883fd0aecbfa0c988f2bf...
[ I have not tested this code, but I believe it does what your C does ]
One very obvious difference is that Rust doesn't have pointer arithmetic, so we're writing the Rust in terms of indexing into the slice. The compiler, of course, is free to implement this with identical machine code.
Rust doesn't think that "any slice of bytes" is a string, but, on the other hand, it also thinks most of C's "string" features are reasonable things to do to a slice of bytes, so this is mostly a matter of terminology. You can for example call make_ascii_lowercase() on a slice. If it wasn't actually ASCII text before then what this does is well defined but probably not very useful.
Posted Aug 26, 2022 15:48 UTC (Fri)
by wtarreau (subscriber, #51152)
[Link] (19 responses)
Yep absolutely, but I thought about it because it was a perfectly valid real-world example of something that can be done extremely efficiently when you manipulate bytes and that one cannot afford to process as individual strings using allocations nor doing memmove() etc.
> I assume you don't consider the behaviour if hdr is in fact pointing at something that is not a zero-terminated string (ie a buffer overflow with undefined behaviour) to be desirable, and so we don't need Rust's unsafe which is the only way to duplicate that.
Absolutely. These are final implementation details. Just like it would be fine to require to know the length upfront and use strnchr() if needed.
> https://gist.github.com/rust-play/59883fd0aecbfa0c988f2bf...
Thanks! [ I have not tested mine either :-) ]
> One very obvious difference is that Rust doesn't have pointer arithmetic, so we're writing the Rust in terms of indexing into the slice
I'm fine with this, I'm used to both forms in C as well, it's just a matter of preference. a[b] is exactly the same as b[a] and *(a+b) or *(b+a) in C, so writing -1[ptr] is valid but only useful to confuse the reader :-)
> Rust doesn't think that "any slice of bytes" is a string, but, on the other hand, it also thinks most of C's "string" features are reasonable things to do to a slice of bytes, so this is mostly a matter of terminology. You can for example call make_ascii_lowercase() on a slice.
Makes sense. The only thing you'll miss then is the machine-specific optimizations that went into a number of C libraries for byte search, fill or move (which can be significant for strchr() or memset()).
> If it wasn't actually ASCII text before then what this does is well defined but probably not very useful.
Sure but the point of such protocols precisely is that you don't care as they're byte-oriented and very fast to process when done right.
Thanks!
Posted Aug 26, 2022 16:59 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (5 responses)
Posted Aug 26, 2022 18:54 UTC (Fri)
by wtarreau (subscriber, #51152)
[Link] (4 responses)
Posted Aug 26, 2022 20:47 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (3 responses)
> The only thing you'll miss then is the machine-specific optimizations that went into a number of C libraries for byte search, fill or move (which can be significant for strchr() or memset()).
strchr and memset are libc functions, so I interpreted "a number of C libraries" as meaning "libc, and possibly other libraries, but mostly libc." If libc has machine-specific optimizations, and Rust can emit libc calls into the LLVM IR (which can then be inlined if it is advantageous to do so), then what exactly is the problem? Most of the interesting libc functions have some sort of safe wrapper in the Rust stdlib, and the language also provides a full FFI for calling arbitrary C code, so I really don't see what disadvantage you're talking about here.
Posted Aug 26, 2022 21:05 UTC (Fri)
by wtarreau (subscriber, #51152)
[Link] (2 responses)
Posted Aug 26, 2022 22:00 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
[1]: https://doc.rust-lang.org/std/primitive.str.html#method.find
Posted Aug 29, 2022 9:37 UTC (Mon)
by wtarreau (subscriber, #51152)
[Link]
Posted Aug 26, 2022 22:37 UTC (Fri)
by tialaramex (subscriber, #21167)
[Link] (12 responses)
Specifically this is by BurntSushi, who also wrote ripgrep the very fast grep implementation. So yes, you certainly can (at least amortized over huge input slices) go significantly faster than the naive loop I wrote if you are willing to work a lot harder than I did, or to use somebody else's work.
Posted Aug 29, 2022 9:57 UTC (Mon)
by wtarreau (subscriber, #51152)
[Link] (11 responses)
Posted Aug 29, 2022 11:42 UTC (Mon)
by Wol (subscriber, #4433)
[Link] (8 responses)
Aren't these constraints? Just different ones?
> than about how to find their way through strict types and constraints, otherwise these constraints can quickly reduce the quality of what they're doing. At least that's my feeling.
I think you're over-thinking it.
The question is, are those artificial constraints, or are they real constraints that need to be addressed somewhere. I'll give you an example - from databases of course :-)
In Pick, the underlying datatype is string. The database is built around handling random-length strings. So when I'm designing my database, because Pick separates the underlying value from the display format, THERE IS NO CONCEPT OF LENGTH in the database itself. So the only time I have to concern myself with "how long is a piece of string" is when I'm trying to output it. Compare that with eg C, which is a plentiful source of buffer over-runs because programmers have no clue how long a piece of string is, or Fortran which iirc is a plentiful source of truncated data because programmers get it wrong ...
Same thing with numbers. Why should the programmer be forced to address "what sort of number is this?". Who cares whether it's integer / float / double / whatever, and it's far too easy to make the wrong choice and end up with precision errors etc. ffs it's a number!
Bear in mind, I don't know Rust, but I get the impression most / all of these constraints are questions which MUST be answered - like "who owns this variable, and when?". Not knowing the answer to that is a serious bug. Whereas on the other hand "is this number a float or a double?" most of the time is both a nuisance and an irrelevance. And mathematically meaningless to boot, but get it wrong and you've just corrupted your data.
Cheers,
Posted Aug 29, 2022 14:11 UTC (Mon)
by mpr22 (subscriber, #60784)
[Link] (5 responses)
*hollow mathematical laughter*
Once you put it in your digital computer, it's a digital computer number.
The mathematics of the many kinds of digital computer number are their own distinctive branches of mathematics, which diverge from the "natural" rules of "everyday" arithmetic in a variety of ways.
When you are operating in contexts such as, say, frivolous Javascript programs for the entertainment of the idle, where enormous performance costs are routinely accepted (if they weren't, you wouldn't be using Javascript in the first place) and correctness is an afterthought, "ffs it's a number!" is an unremarkable stance.
(Javascript has two native numeric types. "number" is IEEE 754 double precision floating point (but you are allowed to use bitwise ops on its purported integer value!), and "bigint" is an "arbitrary-precision" computer integer type for holding integer values outside the precise integer range of IEEE 754 double precision floating point.)
When you are operating in the contexts that C or Rust are intended for, where correctness, speed of execution, runtime memory footprint, etc. all matter, it is not.
Posted Aug 29, 2022 14:34 UTC (Mon)
by Wol (subscriber, #4433)
[Link] (4 responses)
When you are operating in the contexts that Pick is intended for, where correctness (or the lack of it) can land you in jail, and speed is all important - because lack of it means that the job needed for tomorrow won't be ready until next week ...
STEP BACK AND LOOK AT THE FOREST!
I'm not saying your concerns aren't valid IN SOME CIRCUMSTANCES. But forcing the programmer to decide what sort of number, when the problem at hand doesn't give a shit, is not wise. Equally, not bothering the programmer to worry about things like buffer over-runs, use-after-free, and all that stuff when the problem at hand lends itself to making those mistakes, is equally stupid.
YOU NEED TO WORRY ABOUT THE THINGS THAT MATTER. And my worry is that wtarreau is missing the big picture. What's the difference between a good design woefully implemented, and a crap design that works as designed? I don't know, but both are probably useless for the intended purpose ...
I see it time and time again, people can't see the big picture, and when the resulting brown stuff hits the rotating blades, they're completely stunned by something that was blatantly obvious to anyone who took a step back. Maybe it helps that I'm pretty new in my current work role (and I'm coming at it from a completely different angle to most of colleagues), but the amount of poor design / fire-fighting fixes / technical debt I see is horrendous. Luckily, I'm being left alone to do what I think is right to fix all this - I have to explain what and why I'm doing it, but my manager sees the benefits (and I'm new extra resource), so I leave the rest of them to carry on while I try and fix the technical debt.
> "ffs it's a number!" is an unremarkable stance.
It's also an unremarkable stance for big accountancy systems managing huge projects - I worked on Sellafield THORP.
Cheers,
Posted Aug 29, 2022 17:07 UTC (Mon)
by wtarreau (subscriber, #51152)
[Link] (1 responses)
But you wrote the correct term: "programmer". That person got some training to learn that the machine they were going to program for uses discrete numbers, doesn't have infinite memory and so on. Otherwise it's just a human. Programmers know that no machine can accurately represent Pi. Programmers know that it's extremely dangerous to store numeric identifiers in Javascript because by default they're stored as floats and that if their absolute value is larger than 2^52 they will lose precision and possibly confuse some people, sessions, digital signatures or whatever. Nowadays a lot of people think that after having hacked in a text area on an online training site they are programmers. And the day they write code like: if (a < b) do_something(); else if (a == b) do_domething() else if (a > b) do_something_else(), they don't understand why there are some entire classes of values for a and b which result in no function being called. But this is taugh in computer courses, that remain an absolute necessity when writing programs aimed at running on real computers. Otherwise, as you're saying, some bugs can put you to jail (or even put someone else to jail).
Processing protocols requires to handle them using the types they were designed with. Most of the time the common type is the byte and protocol elements are just sequences of bytes. In this case it makes sense to be able to handle them as naturally as possible. That's why C strings are overly used in that area even though they don't shine for their efficiency in this specific purpose, but they're wonderful for many other cases.
And I really *do* want programmers to care for the machine they're writing code for. The languages can help, the compilers as well, and in that regard C is not your ally with its absurd precedence, annoying shifts and unclear type promotion, but it remains extremely reliable, so much that it still runs almost the entierety of the internet's core services and operating systems, many systems which would probably not exist if the only choices by then were much more lenient in terms of resource usage, or too constraining in terms of development. Some will say it's not user-friendly, others will say it is user-friendly, it's just selective about who its friends are. My personal feeling is that it could be way better but that I'm not willing to deal with caprices from another language that constantly puts itself in my way when I'm trying to do something simple for the machine. When I can't do something in C I do it in asm, and I grumble that the interface between the two is ugly and not very transparent (though it improved lately in gcc by allowing to return flags).
> amount of poor design / fire-fighting fixes / technical debt I see is horrendous
That's true and it seems to speed up with newer languages sadly, where developers seem to prefer to throw away everything and restart again and again, without rethinking the design. When your language of choice requires a bit of thinking, you're becoming very stingy about your code and you prefer to be certain to get the design right. You end up drawing stuff on paper before writing code and that significantly helps keeping code running well for decades with an acceptable level of maintenance.
Posted Aug 29, 2022 17:54 UTC (Mon)
by mpr22 (subscriber, #60784)
[Link]
I firmly agree with the general thrust of your post, but I do feel the need to point out that even in Javascript, with its historical dedication to Doing Something That Isn't Showing An Error And Refusing To Run when presented with ill-formed code, that code gets an ingestion-time syntax error at the first 'else'.
Posted Aug 29, 2022 17:08 UTC (Mon)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
You can't just say "it's a number" and call it a day. The math doesn't work out. You have to make some sort of compromise, and operate on some computable subset of the real numbers, which in practice is going to be a great deal smaller than the computable numbers. You can shout in ALL CAPS about how programmers should not be required to think about that compromise, but clearly somebody has to, at some point. If you just want to make the accountants happy, then use some sort of decimal fixed point (like SQL's DECIMAL type) or something similar. That's not "just a number" because it still does intermediate rounding, but at least it's doing the rounding that accountants probably want it to do.
Posted Aug 29, 2022 18:26 UTC (Mon)
by Wol (subscriber, #4433)
[Link]
:-)
Pick mostly uses decimal fixed point (and makes the programmer think about it), but it really is "a number is 14sf to 4dp unless you tell me otherwise", which is sufficient for most purposes. It's a case of "if the sort of number matters, then I need to do something about it, but "number" covers most use cases".
Apologies if I'm maligning him, but wtarreau came across as complaining about Rust's memory checking and borrow checking and all the minutiae of system programming that causes real grief it it goes wrong.
People get too bogged down in the minutiae of what they're doing, and it can be very hard to step back and ask yourself "does this REALLY matter?". Quite often the answer is "no". If the computable subset is larger than your problem space (as it usually is), do you really want to be forced to pay strict attention to it? :-) And when the answer is "yes" then you do have to pay attention.
Cheers,
Posted Aug 29, 2022 23:09 UTC (Mon)
by notriddle (subscriber, #130608)
[Link]
No, it just doesn’t compile.
Posted Aug 30, 2022 7:03 UTC (Tue)
by milesrout (subscriber, #126894)
[Link]
There is no such thing as "just a number" in mathematics, even. An integer is a completely different sort of thing from a real number, or a rational number, or a complex number.
What is the result of calling the sine function on "a number"? What is the result of taking the square root of "a number"?
"Bigint" is a _very_ good default for a high-level programming or scripting language, for integers. But most of them keep using floating-point numbers despite their obvious flaws for a good reason: it's very hard to come up with anything else that will survive the kinds of operations people want to do on them.
You could use rational numbers, but there are lots of numbers people want to use that aren't rationals, and the fractional representations of rationals get huge very quickly when you use them. And even if you want to keep them only within a certain user-configured precision, knowing when to perform that reduction is not trivial if you want it to be performant (it shouldn't be after every operation).
Now there are other possible representations. You could represent numbers as functions that compute the value of the number to a given precision. You could represent numbers in many ways. What exactly a "number" is really isn't clear.
Posted Aug 29, 2022 19:26 UTC (Mon)
by tialaramex (subscriber, #21167)
[Link] (1 responses)
This might be more compelling with experimental results but I'd be astonished if it made a discernible difference actually. Without such results I don't see any reason I shouldn't argue the exact opposite, that your solution is needlessly complicated and dangerous.
BurntSushi did this work because of ripgrep which is processing rather more data than a HTTP User-agent header. I can well believe it's worth doing all that for running grep over an entire monorepo or a few weeks log files or whatever but I have my doubts for Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0 which is, as I count it, 70 bytes.
Posted Aug 31, 2022 3:51 UTC (Wed)
by wtarreau (subscriber, #51152)
[Link]
Optimized grep like those relying on the Boyer-Moore algorithm are extremely effective to look for patterns in large blocks (a complete file), because the initialization cost is very quickly amortized (often after just a few lines).
HTTP headers are painful because they're rarely large enough to amortize many optimizations, need to be processed at very high frequency and as such often suffer from even a function call or the construct of a likely()/unlikely().
I wrote a log parser quite a long time ago, that I'm still using, that processes between 2 and 4 GB of logs per second, looking for some fields or producing statistics on output. It shared similar principles, and all the time was spent in fgets() looking for the LF! I reimplemented this in asm for i386 by then, then for x86_64, then added an option to choose whether or not to use memchr() instead when glibc improved and could sometimes be faster and compensate for the extra cost of the function call, and the differences can be significant. I then adopted similar mechanisms for HTTP headers and gained ~10% global performance probably indicating +50-100% on the parsing alone, so that does count quite a lot.
For the HTTP header example above, among the possibilities to be faster are taking into account the abysmally low probability to meet the pattern. On average sized headers it could be faster to use the fast grep twice over the block if you know it's fit in L1 cache, searching for "\n " then "\n\t" in the whole block at once. Since most headers will be at least ~100 bytes long, this could amortize the cost of initializing the lookup function.
But I definitely agree with you that in any case this must be measured, confronted to various pathological patterns (and that's what I'm doing all day long, trashing lots of apparently smart code changes that don't pass the performance test on real data).
Posted Aug 30, 2022 14:11 UTC (Tue)
by andy_shev (subscriber, #75870)
[Link] (3 responses)
Posted Aug 30, 2022 15:26 UTC (Tue)
by mathstuf (subscriber, #69389)
[Link] (1 responses)
Posted Aug 30, 2022 17:33 UTC (Tue)
by tialaramex (subscriber, #21167)
[Link]
https://rust-for-linux.github.io/docs/alloc/string/struct.String.html
Here's the equivalent Rust standard String (well, alloc::string::String, userspace Rust programmers would use it from std but it's equivalent)
https://doc.rust-lang.org/alloc/string/struct.String.html
You'll notice that the standard String has a lot of stuff that the kernel doesn't. And the kernel only has try_reserve() so you can't just go around reserve()ing memory which might not exist as userspace programmers often do.
The kernel has (or will eventually get) all of core, the Rust library where core language features that are not just inside the compiler live, so that's stuff like an Option type, a Result type, most of the methods you think about for built-in types. But it doesn't have std, the library where OS depending features like File I/O and Networking live, and it has its own rather different alloc, a library where the allocator and related features (such as a growable String) live.
Posted Aug 30, 2022 15:43 UTC (Tue)
by excors (subscriber, #95769)
[Link]
Posted Aug 25, 2022 21:32 UTC (Thu)
by unixbhaskar (guest, #44758)
[Link] (4 responses)
I certainly did learn about that topic by going over his response a few times.
Posted Aug 26, 2022 1:13 UTC (Fri)
by rcampos (subscriber, #59737)
[Link] (2 responses)
Posted Aug 26, 2022 10:08 UTC (Fri)
by unixbhaskar (guest, #44758)
[Link]
You might try this link for the subject ..then look for that specific mail in that listing :
Search in all mail
Posted Aug 26, 2022 13:35 UTC (Fri)
by patrick_g (subscriber, #44470)
[Link]
I think a link to this email is provided in this LWN article.
Posted Aug 25, 2022 22:12 UTC (Thu)
by kees (subscriber, #27264)
[Link] (6 responses)
Replacing strncpy has some special corner cases, but I really hope we can get it purged too. I wrote up the per-caller conversion flow chart in the issue for it:
Posted Aug 26, 2022 6:53 UTC (Fri)
by wtarreau (subscriber, #51152)
[Link] (3 responses)
Posted Aug 30, 2022 7:10 UTC (Tue)
by milesrout (subscriber, #126894)
[Link] (2 responses)
These people should not be programming in C.
"If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written."
Right there in the man page. If you can't read the most basic documentation, don't use the language!
Posted Aug 30, 2022 14:58 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
(Incidentally, snprintf is just about the only portable function that DTRT in this context.)
Posted Aug 30, 2022 14:59 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link]
Posted Aug 27, 2022 6:42 UTC (Sat)
by ruscur (guest, #104891)
[Link] (1 responses)
Posted Aug 28, 2022 11:39 UTC (Sun)
by willy (subscriber, #9762)
[Link]
Posted Aug 26, 2022 9:11 UTC (Fri)
by joib (subscriber, #8541)
[Link] (1 responses)
This has apparently been in POSIX forever, and is also supposedly going to be part of C2X.
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2349.htm
Posted Aug 26, 2022 10:43 UTC (Fri)
by bluss (subscriber, #47454)
[Link]
Posted Sep 2, 2022 11:16 UTC (Fri)
by mtodorov (subscriber, #158788)
[Link] (4 responses)
What if strlcpy() was fixed instead of changing 1,000s of occurrences?
Changing the strlen(src) to strnlen(src, size) should suffice. This would both prevent segfaults for long unterminated strings and preserve function semantics. The fix is simple:
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
strncpy
strncpy
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
* Most of the actual work is in Vec::splice(), which works for arbitrary vectors (dynamic arrays).
* They resize the string if the sizes don't match.
* In general, this whole operation is a lot easier in Rust than in C, because Rust's strings are heap-allocated dynamic arrays, which can be resized in the first place. So it's not solving the same problem (e.g. in some contexts, heap allocation might be seriously problematic).
* If you don't want to use heap-allocated strings and "just want a dumb array," you can use the copy_from_slice() method on primitive slices instead, which is functionally equivalent to memcpy. It panics if the slices have different lengths, but slices know their lengths so at least it's cheap to check for that in advance.
* It is illegal to have two mutable references (or one mutable and one immutable reference) to the same slice at the same time, so you can't copy_from_slice with overlapping arguments (hence they use memcpy instead of memmove). They provide copy_within() for the possibly-overlapping use case, or if the arguments wouldn't actually overlap, you can use split_at_mut() to break it apart into two separate slices.
* There are similar memcpy/memmove-like functions for raw pointers, but Rust considers raw pointers to be inherently unsafe, and so those functions are unsafe as well.
* Rust doesn't have a notion of null-terminated u8 arrays (that I could find), and so the exact "copy a C string" problem is not really a thing that exists in Rust. There is std::ffi::CString, but it doesn't have any methods for directly manipulating its contents; it looks like it's "just" an intermediary for converting between Rust strings and raw pointers.
Ushering out strlcpy()
news.replace_range(..5, "FreeBSD");
Ushering out strlcpy()
Is this the Rust equivalent of Ushering out strlcpy()
str.strip(" string with padding ")
as opposed to (" string with padding ").strip()
in Python?
Ushering out strlcpy()
OR
if Goose::is_wealthy(&jimmy) { println!("Swimming in money"); } else { println!("Aww, poor Jimmy"); }
Ushering out strlcpy()
With all these design constraints explained, it seems quite complicated to efficiently fold HTTP headers in a request, which simply consists in replacing the optional CR and the LF preceeding a space or tab, with a space or tab (i.e. overwrite two bytes in a buffer at known positions). In C that could be as simple as:
Ushering out strlcpy()
Here your description makes it sounds like you'd have to deal with complicated blocks of known lengths which would be error-prone, or even worse, dynamic allocation which is a total no-go in protocol parsers. Or maybe there's a way to still keep the simple solutions such as above (possibly in unsafe mode) ?
void fold(char *hdr)
{
char *lf;
for (lf = hdr; (lf = strchr(lf, '\n')); lf++) {
if (*(lf + 1) != ' ' && *(lf + 1) != '\t')
continue;
if (lf > hdr && *(lf - 1) == '\r')
*(lf - 1) = ' ';
*lf = ' ';
}
}
Ushering out strlcpy()
Ushering out strlcpy()
> [ I have not tested this code, but I believe it does what your C does ]
So overall once presented as a byte array like this it looks similar and should be of equivalent complexity. It may miss some optimizations that can be done for strchr() for example, that would allow to skip 8 bytes at once (or possibly even more using vector instructions), but overall it looks similar.
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Wol
Ushering out strlcpy()
Ushering out strlcpy()
Wol
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Wol
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
https://github.com/KSPP/linux/issues/90
Ushering out strlcpy()
based on strscpy()'s return would work too).
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
Ushering out strlcpy()
memccpy()
memccpy()
Just occurred to me, maybe it is a stupid idea ...
Ushering out strlcpy()
size_t strlcpy(char *dest, const char *src, size_t size)
{
size_t ret = strnlen(src, size);
if (size) {
size_t len = (ret >= size) ? size - 1 : ret;
memcpy(dest, src, len);
dest[len] = '\0';
}
return ret;
}
Am I making any sense to you? strnlen (src, size) is said to be POSIX-2008 compliant ...
Posted Sep 2, 2022 12:08 UTC (Fri)
by mathstuf (subscriber, #69389)
[Link] (3 responses)
```
to 1-stepping the buffer increment instead of just doing a single realloc-enough loop. The return value is "the size that would have been attempted", not "something bigger than".
Posted Sep 2, 2022 13:33 UTC (Fri)
by mtodorov (subscriber, #158788)
[Link] (2 responses)
Yes, I see indeed. Anyway, it seemed too simple and obvious so no one wouldn't have thought of it earlier.
Thanks, @mathstuf.
However, I believe that attempting and unbound strlen(src) on a fixed-size buffer is inherently and semantically wrong, as the size can never be greater than the size of the buffer unless you're performing a scan beyond buffer's end on a potentially unfinished string, which as Mr. Torvalds said can bring disaster (SEGFAULT in kernel mode).
IMHO.
Posted Sep 2, 2022 13:38 UTC (Fri)
by mathstuf (subscriber, #69389)
[Link] (1 responses)
The problem is that `sizeof (buf)` is not known in a lot of contexts (e.g., filenames passed in from userspace). At that point, you're starting to carry around the allocated size around and you may as well amortize the length calculation as well…and oh, what do you know, you have a C++ std::string with no relevant established API.
Posted Sep 2, 2022 13:50 UTC (Fri)
by mtodorov (subscriber, #158788)
[Link]
No argument on that.
Posted Feb 19, 2024 21:06 UTC (Mon)
by pepsiman (guest, #22382)
[Link]
It was eventually added in glibc 2.38.
Ushering out strlcpy()
size_t ret = bufsz;
size_t sz = bufsz;
while (sz <= ret) {
ret = strlcpy(buf, in, bufsz);
if (sz <= ret) {
buf = realloc(buf, ret);
sz = ret;
}
}
```
I see what you're trying to do. My presumption was obviously wrong: changing the return value amounts to a change of semantics, which makes the premise false. And requires another function not to break the existing sources.
Ushering out strlcpy()
strlen (buf)
should thereof always be done as strnlen (buf, sizeof (buf))
.
Beyond the end of the buffer there can be beasts, nasty animals and hidden pits.
Ushering out strlcpy()
Ushering out strlcpy()
However, in most cases there seems to be an implementation-specific limit, like `strnlen (buf, MAXPATHLEN)`.
Attempting to process more than that which makes sense usually only brings a risk of buffer overflow anyway. IMHO.
Carefully crafted input could place the first NULL byte at the beginning of the next memory page that is not allocated and trigger SIGBUS. Probably difficult to exploit, but still possible?
Ushering out strlcpy()