[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
|
|
Subscribe / Log in / New account

Ushering out strlcpy()

By Jonathan Corbet
August 25, 2022
With all of the complex problems that must be solved in the kernel, one might think that copying a string would draw little attention. Even with the hazards that C strings present, simply moving some bytes should not be all that hard. But string-copy functions have been a frequent subject of debate over the years, with different variants being in fashion at times. Now it seems that the BSD-derived strlcpy() function may finally be on its way out of the kernel.

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
KernelString processing


to post comments

Ushering out strlcpy()

Posted Aug 25, 2022 16:16 UTC (Thu) by sub2LWN (subscriber, #134200) [Link] (1 responses)

> Whether it will ever be possible to remove strlcpy() entirely is unclear; strncpy() is still holding string despite its known hazards and a decision to get rid of it nearly 20 years ago.

/still holding string/still holding strong/? strngcpy

Ushering out strlcpy()

Posted Aug 30, 2022 6:51 UTC (Tue) by milesrout (subscriber, #126894) [Link]

> Please do NOT post typos in the article as comments, send them to lwn@lwn.net instead.

Ushering out strlcpy()

Posted Aug 25, 2022 16:23 UTC (Thu) by IanKelling (subscriber, #89418) [Link] (4 responses)

> strncpy() is still holding string despite its known hazards

Funny.

Ushering out strlcpy()

Posted Aug 25, 2022 18:08 UTC (Thu) by adobriyan (subscriber, #30858) [Link] (3 responses)

"n" in "strncpy" stands for "never".

Ushering out strlcpy()

Posted Aug 25, 2022 19:47 UTC (Thu) by Sesse (subscriber, #53779) [Link] (2 responses)

I think it's just catastrophically named. It should be something like strtofixed() or strpad() or something; what it does is fundamentally to copy a null-terminated string into a fixed-size, non-null-terminated, buffer. Like if you have a struct of fixed-width string fields, ISAM-style. So the destination isn't a C-style string, and shouldn't be treated as such.

Of course, this makes the function much less useful than the average C programmer assumes.

strncpy

Posted Aug 25, 2022 22:03 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (1 responses)

As I understand it, this function exists because it's how you do exactly this operation (copy a C-style string into some fixed width buffer) somewhere in early Unix, maybe the filesystem. Its authors knew exactly what they were doing, and this function makes sense for their intended application, but as you say the name suggests somebody might find it useful in very different circumstances.

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.

strncpy

Posted Aug 26, 2022 0:46 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

Raymond Chen reports that this was the case at least as far back as System V (and yes, it was in the filesystem): https://devblogs.microsoft.com/oldnewthing/20050107-00/?p...

Ushering out strlcpy()

Posted Aug 25, 2022 17:30 UTC (Thu) by iabervon (subscriber, #722) [Link]

Is checkpatch capable of complaining about new uses of deprecated functions? That seems like a good way of getting a gradual change with minimal noise, which could then be followed by an eventual cleanup in code that nobody's touched for a few years.

Ushering out strlcpy()

Posted Aug 25, 2022 18:15 UTC (Thu) by adobriyan (subscriber, #30858) [Link] (4 responses)

> Finally, strlcpy() is subject to a race condition.

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().

Ushering out strlcpy()

Posted Aug 25, 2022 22:04 UTC (Thu) by roc (subscriber, #30627) [Link] (3 responses)

Yes. In fact any C code reading memory without using atomics, where that memory is concurrently modified, is undefined behaviour. So all C implementations of string copying will be "subject to a race condition" in that sense.

Ushering out strlcpy()

Posted Aug 25, 2022 22:42 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (2 responses)

I will guess that while what you wrote is literally true, the same reasoning applies for almost everything Linux (as an operating system kernel written largely in C) does and so Linus will pronounce it stupid and decide it doesn't apply.

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.

Ushering out strlcpy()

Posted Aug 26, 2022 2:12 UTC (Fri) by dezgeg (subscriber, #92243) [Link] (1 responses)

> 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.

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.

Ushering out strlcpy()

Posted Aug 26, 2022 7:42 UTC (Fri) by LtWorf (subscriber, #124958) [Link]

I think it will read past regardless of the size of the destination buffer.

Ushering out strlcpy()

Posted Aug 25, 2022 18:48 UTC (Thu) by NYKevin (subscriber, #129325) [Link] (31 responses)

Out of curiosity, I decided to look up how Rust handles string copying/overwriting, and found that:

* 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).
* 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()

Posted Aug 25, 2022 20:31 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (4 responses)

> it's a method rather than a free function

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();
news.replace_range(..5, "FreeBSD");

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.

Ushering out strlcpy()

Posted Aug 26, 2022 7:56 UTC (Fri) by nyanpasu64 (guest, #135579) [Link]

I thought that method calls on &[mut] dyn T variables (trait object references, fat pointers) couldn't be rewritten as free function calls, since you're calling a function pointer stored behind the fat pointer itself, and I thought free function calls are statically resolved. But it turns out that `Trait::func(obj)` can actually invoke a dynamic dispatch, if `obj` is a trait object fat pointer.

Ushering out strlcpy()

Posted Aug 28, 2022 23:45 UTC (Sun) by KJ7RRV (subscriber, #153595) [Link] (2 responses)

Is this the Rust equivalent of str.strip(" string with padding ") as opposed to (" string with padding ").strip() in Python?

Ushering out strlcpy()

Posted Aug 30, 2022 17:10 UTC (Tue) by tialaramex (subscriber, #21167) [Link] (1 responses)

I have used Python but I'm definitely not an expert so the rest of this may be wrong, especially in subtle ways.

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"); }
OR
if Goose::is_wealthy(&jimmy) { 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.

Ushering out strlcpy()

Posted Aug 30, 2022 17:12 UTC (Tue) by tialaramex (subscriber, #21167) [Link]

Gah, I should always check these compile before hitting submit.

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.

Ushering out strlcpy()

Posted Aug 26, 2022 6:43 UTC (Fri) by wtarreau (subscriber, #51152) [Link] (21 responses)

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:

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 = ' ';
   }
}
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) ?

Ushering out strlcpy()

Posted Aug 26, 2022 10:30 UTC (Fri) by tialaramex (subscriber, #21167) [Link] (20 responses)

You'll notice that in the newer protocols HTTP/2 and HTTP/3 the headers are no longer expressed this way. Today this folding is also considered obsolete in HTTP/1.1 and servers are entitled to just return a 400 error for requests with folded headers.

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.

Ushering out strlcpy()

Posted Aug 26, 2022 15:48 UTC (Fri) by wtarreau (subscriber, #51152) [Link] (19 responses)

> You'll notice that in the newer protocols HTTP/2 and HTTP/3 the headers are no longer expressed this way. Today this folding is also considered obsolete in HTTP/1.1 and servers are entitled to just return a 400 error for requests with folded headers.

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...
> [ I have not tested this code, but I believe it does what your C does ]

Thanks! [ I have not tested mine either :-) ]
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.

> 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!

Ushering out strlcpy()

Posted Aug 26, 2022 16:59 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (5 responses)

I believe Rust is perfectly capable of emitting libc calls in contexts where it is safe to do so, because many of the slice primitives are described with phrases like "using memcpy" in the documentation. That suggests to me that they really do emit a memcpy(3) function call, or at least something which LLVM will transform into a memcpy call during compilation. So that's probably going to perform just as well as C, because under the hood, it is C.

Ushering out strlcpy()

Posted Aug 26, 2022 18:54 UTC (Fri) by wtarreau (subscriber, #51152) [Link] (4 responses)

Here it's different, it's not a matter of relying on libc calls for other calls but rather a question of ability for a compiler to detect a well-known pattern from something that doesn't look similar. Just like we constantly have to help the C compiler catch what we're trying to do by sometimes making ugly constructs, any other language's compiler will have the same problem, and it's not necessarily trivial to match an strchr() pattern in the loop above. And that's actually part of the things I often don't like in more abstract or stricter languages, which is less freedom to tell the compiler what you're trying to do.

Ushering out strlcpy()

Posted Aug 26, 2022 20:47 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (3 responses)

I don't understand. You said:

> 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.

Ushering out strlcpy()

Posted Aug 26, 2022 21:05 UTC (Fri) by wtarreau (subscriber, #51152) [Link] (2 responses)

I'm not speaking about being able or not to emit libc calls. I can understand that it can emit them when the call is obvious. What I'm saying is that just like any compiler it's extremely unlikely to spot situations where this is possible based on a loop like above. Even for a human it's not trivial to spot this. If it had strings mapped to arrays for example, like in C, I would easily imagine that we'd have an equivalent of strchr() applying to that array that would rely on the libc call. But a loop like this with tests in the middle is extremely unlikely to be turned to a series of strchr() on one given value. And by the way in such a parser it should be left to the developer's choice, because short headers would cost more with an strchr() call while long ones would benefit from a fast strchr(). Thus it would be nice to be able to call the equivalent of strchr() on positions in this array. But overall it looks like reimplementing the C string model that many people dislike.

Ushering out strlcpy()

Posted Aug 26, 2022 22:00 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (1 responses)

Well, sure, but you wouldn't write that loop (in Rust) in the first place. You'd call str::find[1] with suitable arguments, and then it's the implementation's problem to emit the most efficient way to do this. Since one possible set of arguments to str::find can be statically proven to be exactly equivalent to strchr (purely in terms of the type system, so this can be done in the monomorphization pass), the implementation should emit strchr if it is optimal to do so. If the implementation does not in fact emit strchr, and you think this is suboptimal, then you should file a bug with them. But don't tell me it's impossible. It's clearly possible.

[1]: https://doc.rust-lang.org/std/primitive.str.html#method.find

Ushering out strlcpy()

Posted Aug 29, 2022 9:37 UTC (Mon) by wtarreau (subscriber, #51152) [Link]

To be honest, I think you lost me, especially since initially I understood that it had to be handled as an array of bytes and not a string, but that sort of comforts me in the idea that it can quickly become very complicated to write simple yet efficient functions for a number of low-level use cases. And my fear with languages that are complicated to use is that either the developer will introduce logic bugs when trying to hack around constraints, or that they will give up and fall back to the default, much less efficient solution (which is often what we see in practice).

Ushering out strlcpy()

Posted Aug 26, 2022 22:37 UTC (Fri) by tialaramex (subscriber, #21167) [Link] (12 responses)

https://docs.rs/memchr/latest/memchr/ is the uh, extremely complicated result of somebody who really cares about the sort of optimizations you're discussing attacking this problem. It is of course named after C's memchr() function which is exactly like strchr() except not specialized for C-style 0-terminated strings.

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.

Ushering out strlcpy()

Posted Aug 29, 2022 9:57 UTC (Mon) by wtarreau (subscriber, #51152) [Link] (11 responses)

Thanks! But indeed as I said above, that's the limit, i.e. it seems it can quickly become quite complicated to achieve C-like efficiency for real-world use cases that are routinely found in low-level programming. I mean, we both spent less than 5 minutes writing each of our versions, and they're probably as easy to read for a developer of each language so at this point that's fine. However the rust one will be significantly less efficient on medium sized headers (user-agent, cookie, referer etc) and suddenly it seems to become quite a bit more difficult to try to recover that last part because there are different ways to hack around this that one must explore. And for me, ease of use is a concern because I want to see developers spend more time thinking about design, reliability and security 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.

Ushering out strlcpy()

Posted Aug 29, 2022 11:42 UTC (Mon) by Wol (subscriber, #4433) [Link] (8 responses)

> And for me, ease of use is a concern because I want to see developers spend more time thinking about design, reliability and security

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,
Wol

Ushering out strlcpy()

Posted Aug 29, 2022 14:11 UTC (Mon) by mpr22 (subscriber, #60784) [Link] (5 responses)

> ffs it's a number!

*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.

Ushering out strlcpy()

Posted Aug 29, 2022 14:34 UTC (Mon) by Wol (subscriber, #4433) [Link] (4 responses)

> 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.

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,
Wol

Ushering out strlcpy()

Posted Aug 29, 2022 17:07 UTC (Mon) by wtarreau (subscriber, #51152) [Link] (1 responses)

> But forcing the programmer to decide what sort of number, when the problem at hand doesn't give a shit, is not wise.

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.

Ushering out strlcpy()

Posted Aug 29, 2022 17:54 UTC (Mon) by mpr22 (subscriber, #60784) [Link]

> 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()

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'.

Ushering out strlcpy()

Posted Aug 29, 2022 17:08 UTC (Mon) by NYKevin (subscriber, #129325) [Link] (1 responses)

The problem is, the real numbers are uncountable, which means that there is no encoding of the real numbers in (finite-length) bit strings (even allowing for variable-length bit strings, arbitrary precision encodings, etc.). Furthermore, there exist non-computable real numbers (real numbers for which there does not exist any algorithm that can output their digits), and even if you restrict yourself to the computable reals, there are many computable numbers for which basic arithmetic is problematic (for example, zero and "2^-n, if Turing machine M halts in exactly n steps, or else zero if it never halts" are both computable numbers, but deciding whether they are equal is equivalent to the halting problem and hence undecidable).

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.

Ushering out strlcpy()

Posted Aug 29, 2022 18:26 UTC (Mon) by Wol (subscriber, #4433) [Link]

> 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.

:-)

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,
Wol

Ushering out strlcpy()

Posted Aug 29, 2022 23:09 UTC (Mon) by notriddle (subscriber, #130608) [Link]

> Not knowing the answer to that [ownership question] is a serious bug.

No, it just doesn’t compile.

Ushering out strlcpy()

Posted Aug 30, 2022 7:03 UTC (Tue) by milesrout (subscriber, #126894) [Link]

> 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!

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.

Ushering out strlcpy()

Posted Aug 29, 2022 19:26 UTC (Mon) by tialaramex (subscriber, #21167) [Link] (1 responses)

> However the rust one will be significantly less efficient on medium sized headers (user-agent, cookie, referer etc)

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.

Ushering out strlcpy()

Posted Aug 31, 2022 3:51 UTC (Wed) by wtarreau (subscriber, #51152) [Link]

It actually does matter when you have to deal with DDoS for example, or when processing logs/traces.

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).

Ushering out strlcpy()

Posted Aug 30, 2022 14:11 UTC (Tue) by andy_shev (subscriber, #75870) [Link] (3 responses)

Good explanation why Rust sucks... No, thanks, we do not need page faults and sleeping context in the critical sections in the kernel.

Ushering out strlcpy()

Posted Aug 30, 2022 15:26 UTC (Tue) by mathstuf (subscriber, #69389) [Link] (1 responses)

I don't see the connection. The general purpose `std::string::String` might not work in all contexts, but there is a *lot* of code that doesn't work in such contexts anyways. The kernel is certainly free to make its own String which can be used from interrupts and such (with appropriate error handling of course); there's nothing magic about String in Rust and is "just" library code.

Ushering out strlcpy()

Posted Aug 30, 2022 17:33 UTC (Tue) by tialaramex (subscriber, #21167) [Link]

The kernel does have its own String:

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.

Ushering out strlcpy()

Posted Aug 30, 2022 15:43 UTC (Tue) by excors (subscriber, #95769) [Link]

The Rust standard library isn't fully suitable for use in a kernel (or many other embedded environments), but that's okay, you can find or implement your own equivalent types that meet your requirements (just like you'd have to when using C). E.g. there's heapless::String (https://docs.rs/heapless/latest/heapless/struct.String.html) as a fixed-capacity string without dynamic allocation, where functions that might exceed the string's capacity (like String::push) return a Result<> so it's always safe and the caller has to explicitly decide how to handle that case.

Ushering out strlcpy()

Posted Aug 25, 2022 21:32 UTC (Thu) by unixbhaskar (guest, #44758) [Link] (4 responses)

...also, I was bumped onto a mail, where Linus was explaining it to Steve Fench about the whole story...fascinating ....from time to time Linus wrote loooong response to someone about some particular topic ...that mail is one of them ...if anyone curious enough to find it out, please read that response and explanations too.

I certainly did learn about that topic by going over his response a few times.

Ushering out strlcpy()

Posted Aug 26, 2022 1:13 UTC (Fri) by rcampos (subscriber, #59737) [Link] (2 responses)

I'm just curious to ask for a link :)

Ushering out strlcpy()

Posted Aug 26, 2022 10:08 UTC (Fri) by unixbhaskar (guest, #44758) [Link]

If you go to lore.kernel.org and look for "strlcpy note" as the subject line ..then you should get it.

You might try this link for the subject ..then look for that specific mail in that listing :

Search in all mail

https://lore.kernel.org/all/?q=strlcpy

Ushering out strlcpy()

Posted Aug 26, 2022 10:18 UTC (Fri) by unixbhaskar (guest, #44758) [Link]

Ushering out strlcpy()

Posted Aug 26, 2022 13:35 UTC (Fri) by patrick_g (subscriber, #44470) [Link]

> I was bumped onto a mail, where Linus was explaining it to Steve Fench about the whole story...fascinating

I think a link to this email is provided in this LWN article.

Ushering out strlcpy()

Posted Aug 25, 2022 22:12 UTC (Thu) by kees (subscriber, #27264) [Link] (6 responses)

Replacing strlcpy is pretty straightforward. Here the issue tracker item for it: https://github.com/KSPP/linux/issues/89

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:
https://github.com/KSPP/linux/issues/90

Ushering out strlcpy()

Posted Aug 26, 2022 6:53 UTC (Fri) by wtarreau (subscriber, #51152) [Link] (3 responses)

Agreed, I too would like to see strncpy() disappear, as it tends to lie to the reader. I've seen it be entirely responsible for major slowdowns a few times in different projects by the way, where users did not realize that it was padding the whole target buffer. And the number of times you see it called in series to concatenate path elements, you can't avoid thinking "OK I have an overflow after the first one". The only valid case I know for it in userland is to write a UNIX socket path because sun_path is of fixed length and not necessarily 0-terminated :-/ But that clearly demonstrates that str_anything_pad() would be a more suitable name for this thing (though memset(end, 0, size-len)
based on strscpy()'s return would work too).

Ushering out strlcpy()

Posted Aug 30, 2022 7:10 UTC (Tue) by milesrout (subscriber, #126894) [Link] (2 responses)

>Agreed, I too would like to see strncpy() disappear, as it tends to lie to the reader. I've seen it be entirely responsible for major slowdowns a few times in different projects by the way, where users did not realize that it was padding the whole target buffer.

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!

Ushering out strlcpy()

Posted Aug 30, 2022 14:58 UTC (Tue) by NYKevin (subscriber, #129325) [Link] (1 responses)

IMHO it is not totally unreasonable to expect strncpy(src) to be equivalent to snprintf("%s", size, src), because strcpy(src) is actually equivalent to sprintf("%s", src).

(Incidentally, snprintf is just about the only portable function that DTRT in this context.)

Ushering out strlcpy()

Posted Aug 30, 2022 14:59 UTC (Tue) by NYKevin (subscriber, #129325) [Link]

(Oops, I put the arguments in the wrong order, but you know what I mean.)

Ushering out strlcpy()

Posted Aug 27, 2022 6:42 UTC (Sat) by ruscur (guest, #104891) [Link] (1 responses)

Replacing stray strlcpy() cases is some great low-hanging fruit for anyone looking to get their first commits into the kernel.

Ushering out strlcpy()

Posted Aug 28, 2022 11:39 UTC (Sun) by willy (subscriber, #9762) [Link]

Did you follow kees's link? It's literally tagged "Good first issue"

memccpy()

Posted Aug 26, 2022 9:11 UTC (Fri) by joib (subscriber, #8541) [Link] (1 responses)

No love for memccpy() in the kernel (note, two 'c's, this is not memcpy())?

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

memccpy()

Posted Aug 26, 2022 10:43 UTC (Fri) by bluss (subscriber, #47454) [Link]

And the nice documentation here says "To avoid the risk of buffer overflow" and produces an example that uses the returned pointer p without checking if p was NULL, which is may reasonably be. :)

Ushering out strlcpy()

Posted Sep 2, 2022 11:16 UTC (Fri) by mtodorov (subscriber, #158788) [Link] (4 responses)

Just occurred to me, maybe it is a stupid idea ...

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:

    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 ...

Ushering out strlcpy()

Posted Sep 2, 2022 12:08 UTC (Fri) by mathstuf (subscriber, #69389) [Link] (3 responses)

You've broken compatibility for callers which do something like:

```
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;
}
}
```

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".

Ushering out strlcpy()

Posted Sep 2, 2022 13:33 UTC (Fri) by mtodorov (subscriber, #158788) [Link] (2 responses)

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.

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).

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.

IMHO.

Ushering out strlcpy()

Posted Sep 2, 2022 13:38 UTC (Fri) by mathstuf (subscriber, #69389) [Link] (1 responses)

> 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.

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.

Ushering out strlcpy()

Posted Sep 2, 2022 13:50 UTC (Fri) by mtodorov (subscriber, #158788) [Link]

> 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.

No argument on that.
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()

Posted Feb 19, 2024 21:06 UTC (Mon) by pepsiman (guest, #22382) [Link]

> to this day, glibc lacks strlcpy()

It was eventually added in glibc 2.38.


Copyright © 2022, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds