11 ms·
Love C, hate C: Web framework memory problems
- jacquesm 11mo agoThere are many, many more such issues with that code. The person that posted it is new to C and had an AI help them to write the code. That's a recipe for disaster, it means the OP does not actually understand what they wrote. It looks nice but it is full of footguns and even though it is a useful learning exercise it also is a great example of why it is better run battle tested frame works than to inexpertly roll your own. As a learning exercise it is useful, but it should never see production use. What is interesting is that the apparent cleanliness of the code (it reads very well) is obscuring the fact that the quality is actually quite low. If anything I think the conclusion should be that AI+novice does not create anything that is useable without expert review and that that probably adds up to a net negative other than that the novice will (hopefully) learn something. It would be great if someone could put in the time to do a full review of the code, I have just read through it casually and already picked up a couple of problems, I'm pretty sure that if you did a thorough job of it there would be many more.
- drnick1 11mo ago> What is interesting is that the apparent cleanliness of the code (it reads very well) is obscuring the fact that the quality is actually quite low. I think this is a general feature and one of the greatest advantages of C. It's simple, and it reads well. Modern C++ and Rust are just horrible to look at.
- messe 11mo agoI slightly unironically believe that one of the biggest hindrances to rust's growth is that it adopted the :: syntax from C++ rather than just using a single . for namespacing.
- jacquesm 11mo agoI believe that the fanatics in the rust community were the biggest factor. They turned me off what eventually became a decent language. There are some language particulars that were strange choices, but I get that if you want to start over you will try to get it all right this time around. But where the Go authors tried to make the step easy and kept their ego out of it, it feels as if the rust people aimed at creating a new temple rather than to just make a new tool. This created a massive chicken-and-the-egg problem that did not help adoption at all. Oh, and toolchain speed. For non-trivial projects for the longest time the rust toolchain was terribly slow. I don't remember any other language's proponents actively attacking the users of other programming language.
- imtringued 11mo agoSoftware vulnerabilities are an implicit form of harassment.
- messe 11mo agoI'm hoping that's meant to satirise the rust community, because it's horseshit like this that makes a sizeable subset of rust evangelists unbearable.
- 01HNNWZ0MV43FF 11mo ago> I don't remember any other language's proponents actively attacking the users of other programming language. I just saw someone on Hacker News saying that Rust was a bad language because of its users
- jacquesm 11mo agoYawn. Really, if you have nothing to say don't do it here.
- LexiMax 11mo agoGotcha hypocrisy might be a really cheap thing to point out, but they're not wrong. I have noticed my fair share of Rust Derangement Syndrome in C++ spaces that seems completely outsized from the series of microaggressions that they eventually point out when asked "Why?"
- citbl 11mo agoThe safer the C code, the more horrible it starts looking though... e.g. my_func(char msg[static 1])
- deleted 11mo ago[deleted]
- uecker 11mo agoCompared to other languages, this is still nice.
- jacquesm 11mo agoIt is - like everything else - nice because you, me and lots of others are used to it. But I remember starting out with C and thinking 'holy crap, this is ugly'. After 40+ years looking at a particular language it no longer looks ugly simply because of familiarity. But to a newcomer C would still look quite strange and intimidating. And this goes for almost all programming languages. Each and every one of them has warts and issues with syntax and expressiveness. That holds true even for the most advanced languages in the field, Haskell, Erlang, Lisp and more so for languages that were originally designed for 'readability'. Programming is by its very nature more akin to solving a puzzle than to describing something. The puzzle is to how to get the machine to do something, to do it correctly, to do it safely and to do it efficiently, and all of those while satisfying the constraint of how much time you are prepared (or allowed) to spend on it. Picking the 'right' language will always be a compromise on some of these, there is no programming language that is perfect (or even just 'the best' or 'suitable') for all tasks, and there are no programming languages that are better than any other for any subset of all tasks until 'tasks' is a very low number.
- uecker 11mo agoI agree that the first reaction usually is only about what one is used to. I have seen this many times. Still, of course, not all syntax is equally good. For example, the problem with Vec<Vec<T>> for a 2D array is not that one is not used to it, but that the syntax is just badly designed. Not that C would not have problematic syntax, but I still think it is fairly good in comparison.
- OneLessThing 11mo agoI agree that it reads really well which is why I was also surprised the quality is not high when I looked deeper. The author claims to have only used AI for the json code, so your conclusion may be off, it could just be a novice doing novice things. I suppose I was just surprised to find this code promoted in my feed when it's not up to snuff. And I'm not hating, I do in fact love the project idea.
- lifthrasiir 11mo agoYeah, I recently wrote a moderate amount of C code [1] entirely with Gemini and while it was much better than what I initially expected I needed a constant steering to avoid inefficient or less safe code. It needed an extensive fuzzing to get the minimal amount of confidence, which caught at least two serious problems---seriously, it's much better than most C programmers, but still. [1] https://github.com/lifthrasiir/wah/blob/main/wah.h https://github.com/lifthrasiir/wah/blob/main/wah.h
- jacquesm 11mo agoI've been doing this the better part of a lifetime and I still need to be careful so don't feel bad about it. Just like rust has an 'unsafe' keyword I realize all of my code is potentially unsafe. Guarding against UB, use-after-free, array overruns and so on is a lot of extra work and you only need to slip up once to have a bug, and if you're unlucky something exploitable. You get better at this over the years. But if I know something needs to be bullet proof the C compiler would not be my first tool of choice. One good defense is to reduce your scope continuously. The smaller you make your scope the smaller the chances of something escaping your attention. Stay away from globals and global data structures. Make it impossible to inspect the contents of a box without going through a well defined interface. Use assertions liberally. Avoid fault propagation, abort immediately when something is out of the expected range.
- uecker 11mo agoI strategy that helps me is just not use open-coded pointer arithmetic or string manipulation but encapsulate those behind safe bounds-checked interfaces. Then essentially only life-time issues remain and for those I usually do have a simple policy and clearly document any exception. I also use signed integers and the sanitizer in trapping mode, which turns any such issue I may have missed into a run-time trap.
- OneLessThing 11mo agoThis is why I love C. You can build these guard rails at exactly the right level for you. You can build them all the way up to CPython and do garbage collection and constant bounds checking. Or keep them at just raw pointer math. And everywhere in between. I like your approach. The downside being that there are probably 100,000+ bespoke implementations of similar guard rails where python users for example all get them for free.
- citbl 11mo agoThe irony is also that AI could have been used to audit the code and find these issues. All the author had to do was to question.
- nurettin 11mo ago> should never see production use. I have an issue with high strung opinions like this. I wrote plenty of crappy delphi code while learning the language that saw production use and made a living from it. Sure, it wasn't the best experience for users, it took years to iron out all the bugs and there was plenty of frustration during the support phase (mostly null pointer exceptions and db locks in gui). But nobody would be better off now if that code never saw production use. A lot of business was built around it.
- zdragnar 11mo agoBuggy code that just crashes or produces incorrect results are a whole different category. In C a bug can compromise a server and your users. See the openssl heart bleed vulnerability as a prime example. Once upon a time, you could put up a relatively vulnerable server, and unless you got a ton of traffic, there weren't too many things that would attack it. Nowadays, pretty much anything Internet facing will get a constant stream of probes. Putting up a server requires a stricter mindset than it used to.
- jacquesm 11mo agoThere are minimum standards for deployment to the open web. I think - and you're of course entirely free to have a different opinion - that those are not met with this code.
- nurettin 11mo agoYes, I have lots of opinions! I guess the question at spotlight is: At what point would your custom server's buffer overflow when reading a header matter and would that bug even exist at that point? Could a determined hacker get to your server without even knowing what weird software you cooked up and how to exploit your binary? We have a lot of success stories born from bad code. I mean look at Micro$oft. Look at all the big players like discord leaking user credentials. Why would you still call out the little fish? Maybe I should create a form for all these ahah.
- frumplestlatz 11mo ago
- lelanthran 11mo agoI can't completely blame the language here: anyone "coding" in a language new to them using an LLM is going to have real problems.
- OneLessThing 11mo agoIt's funny the author says this was 90% written without AI, and that AI was mostly used for the json code. I think they're just new to C. Trust me I love C. Probably over 90% of my lifetime code has been written in C. But python newbies don't get their web frameworks stack smashed. That's kind of nice.
- lelanthran 11mo ago> But python newbies don't get their web frameworks stack smashed. That's kind of nice. Hah! True :-) The thing is, smashed stacks are difficult to exploit deterministically or automatically. Even heartbleed, as widespread as it was, was not a guaranteed RCE. OTOH, an exploit in a language like Python is almost certainly going to be easier to exploit deterministically. Log4j, for example, was a guaranteed exploit and the skill level required was basically "Create a Java object". This is because of the ease with which even very junior programmers can create something that appears to run and work and not crash.
- alfiedotwtf 11mo ago> The thing is, smashed stacks are difficult to exploit deterministically or automatically. Even heartbleed, as widespread as it was, was not a guaranteed RCE. That’s like driving without a seatbelt - it’s not safe, but it would only matter on that very rare chance you have a crash. I would rather just wear a seatbelt!
- uyzstvqs 11mo agoIt's a double-sided coin. LLMs are probably the best way to learn programming languages right now. But if you vibecode in a programming language that you don't understand, it's going to be a disaster sooner or later. This is also the reason why AI will not replace any actual jobs with merit.
- messe 11mo ago> Another interesting choice in this project is to make lengths signed: There are good reasons for this choice in C (and C++) due to broken integer promotion and casting rules. See: "Subscripts and sizes should be signed" (Bjarne Stroustrup) https://open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1428r0.pdf https://open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1428r0... As a nice bonus, it means that ubsan traps on overflow (unsigned overflows just wrap).
- uecker 11mo agoI do not agree that the integer promotion or casting (?) rules are broken in C. That some people make mistakes because they do not know them is a different problem. The reason you should make length signed is that you can use the sanitizer to find or mitigate overflow as you correctly observe, while unsigned wraparound leads to bugs which are basically impossible to find. But this has nothing to do with integer promotion and wraparound bugs can also create bugs in - say - Rust.
- OneLessThing 11mo agoIt's interesting to hear these takes. I've never had problems catching unsigned wrap bugs with plain old memory sanitizers, though I must admit to not having a lot of experience with ubsan in particular. Maybe I should use it more.
- jacquesm 11mo agoI've had some fun reviewing some very old code I wrote (1980's) to see what it looked like to me after such a long time of gaining experience. It's not unlike what the OP did here, it reads cleanly but I can see many issues that escaped my attention at the time. I always compared C with a very fast car: you can take some corners on two wheels but if you make a habit of that you're going to end up in a wall somewhere. That opinion has not changed.
- uecker 11mo agoI think the correct comparison is a sharp knife. It is extremely useful and while there is a risk it is fully acceptable. The idea that we should all use plastic knifes because there are often accidents with knifes is wrong and so is the idea that we use should abandon C because of memory safety. I follow computer security issues for several decades, and while I think we should have memory safety IMHO the push and arguments are completely overblown - and they are especially not worth the complexity and issues of Rust. I never was personally impacted by a security exploit caused by memory safety or know anybody in my personal vicinity who was. I know many cases where people where affected by other kinds of security issues. So I think those are what we should focus on first. And having timely security updates is a hell lot more important than memory safety, so I am not happy that Rust now makes this harder.
- bluetomcat 11mo agoGood C code will try to avoid allocations as much as possible in the first place. You absolutely don’t need to copy strings around when handling a request. You can read data from the socket in a fixed-size buffer, do all the processing in-place, and then process the next chunk in-place too. You get predictable performance and the thing will work like precise clockwork. Reading the entire thing just to copy the body of the request in another location makes no sense. Most of the “nice” javaesque XXXParser, XXXBuilder, XXXManager abstractions seen in “easier” languages make little sense in C. They obfuscate what really needs to happen in memory to solve a problem efficiently.
- 01HNNWZ0MV43FF 11mo agoCan you do parsing of JSON and XML without allocating?
- bluetomcat 11mo agoYes, you can do it with minimal allocations - provided that the source buffer is read-only or is mutable but is unused later directly by the caller. If the buffer is mutable, any un-escaping can be done in-place because the un-escaped string will always be shorter. All the substrings you want are already in the source buffer. You just need a growable array of pointer/length pairs to know where tokens start.
- gritzko 11mo agoYep, no problem. In place parsing only requires a stack. Stack length is the maximum JSON nesting allowed. I have a C dialect exactly like that.
- veqq 11mo agoOf course. You can do it in a single pass/just parse the token stream. There are various implementations like: https://zserge.com/jsmn/ https://zserge.com/jsmn/
- andrepd 11mo agoIt requires manual allocation of an array of tokens. So it needs a backing "stack vector" of sorts. And what about escapes?
- jqpabc123 11mo agoReads like an indictment of vibe coding. LLMs are fundamentally probabilistic --- not deterministic. This basically means that anything produced this way is highly suspect. And this framework is an example.
- erichocean 11mo agoGive Fil-C a try, the speed hit is pretty minimal and you get full memory safety. https://fil-c.org/ https://fil-c.org/
- Karrot_Kream 11mo agoWow this is really cool, I'd never seen this before. Thanks!
- adhamsalama 11mo agoWhy isn't this used more?
- dang 11mo agoRecent and related: Show HN: I built a web framework in C - https://news.ycombinator.com/item?id=45526890 https://news.ycombinator.com/item?id=45526890 - Oct 2025 (208 comments)
- yipikaya 11mo agoAs an aside, it's amusing that it took 25 years for C coders to embrace the C99 named struct designator feature: HttpParser parser = { .isValid = true, .requestBuffer = strdup(request), .requestLength = strlen(request), .position = 0, }; All the kids are doing it now!
- 1718627440 11mo agoThis is nice for constant data, but strdup can return NULL here, which is again never checked. > it took 25 years for C coders to embrace the C99 named struct designator feature Not sure if this actually true, but this is kind of the feature of C, 20 years old code or compiler is supposed to work just fine, so you just wait for some time to settle things. For fast and shiny, there is Javascript.
- davemp 11mo agoI’m still regularly getting on projects and moving C89 variable declarations from the start of functions to where they’re initialized, but I guess it’s not the kids doing it.
- mkfs 11mo ago> C89 variable declarations from the start of functions Technically it's the start of a block.
- davemp 11mo agoTechnically but I don’t think folks ever really bothered.
- 1718627440 11mo agoI only declare variables at the begin of a block, not because I would need C89 compatibility, but because I find it clearer to establish the working set of variables upfront. This doesn't restrict me in anyway, because I just start a new block, when I feel the need. I also try to keep the scope of a variable as small as possible.
- ge96 11mo agoLong as you allocate me, it's alright
- acidx 11mo agoOne thing to note, too, is that `atoi()` should be avoided as much as possible. On error (parse error, overflow, etc), it has an unspecified return value (!), although most libcs will return 0, which can be just as bad in some scenarios. Also not mentioned, is that atoi() can return a negative number -- which is then passed to malloc(), that takes a size_t, which is unsigned... which will make it become a very large number if a negative number is passed as its argument. It's better to use strtol(), but even that is a bit tricky to use, because it doesn't touch errno when there's no error but you need to check errno to know if things like overflow happened, so you need to set errno to 0 before calling the function. The man page explains how to use it properly. I think it would be a very interesting exercise for that web framework author to make its HTTP request parser go through a fuzz-tester; clang comes with one that's quite good and easy to use (https://llvm.org/docs/LibFuzzer.html https://llvm.org/docs/LibFuzzer.html), especially if used alongside address sanitizer or the undefined behavior sanitizer. Errors like the one I mentioned will most likely be found by a fuzzer really quickly. :)
- MathMonkeyMan 11mo agoUnspecified, really? cppreference's [C documentation][1] says that it returns zero. The [OpenGroup][2] documentation doesn't specify a return value when the conversion can't be performed. This recent [draft][3] of the ISO standard for C says that if the value cannot be represented (does that mean over/underflow, bad parse, both, neither?), then it's undefined behavior. So three references give three different answers. You could always use sscanf instead, which tells you how many values were scanned (e.g. zero or one). [1]: https://en.cppreference.com/w/c/string/byte/atoi.html https://en.cppreference.com/w/c/string/byte/atoi.html [2]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/atoi.html https://pubs.opengroup.org/onlinepubs/9799919799/functions/a... [3]: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2310.pdf https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2310.pdf
- acidx 11mo agoThe Linux man page (https://man7.org/linux/man-pages/man3/atoi.3.html#VERSIONS https://man7.org/linux/man-pages/man3/atoi.3.html#VERSIONS) says that POSIX.1 leaves it unspecified. As you found out, it's really something that should be avoided as much as possible, because pretty much everywhere disagrees how it should behave, especially if you value portability. sscanf() is not a good replacement either! It's better to use strtol() instead. Either do what Lwan does (https://github.com/lpereira/lwan/blob/master/src/lib/lwan-config.c#L156-L174 https://github.com/lpereira/lwan/blob/master/src/lib/lwan-co...), or look (https://cvsweb.openbsd.org/src/lib/libc/stdlib/strtonum.c?rev=1.8&content-type=text/x-cvsweb-markup https://cvsweb.openbsd.org/src/lib/libc/stdlib/strtonum.c?re...) at how OpenBSD implemented strtonum(3). For instance, if you try to parse a number that's preceded by a lot of spaces, sscanf() will take a long time going through it. I've been hit by that when fuzzing Lwan. Even cURL is avoiding sscanf(): https://daniel.haxx.se/blog/2025/04/07/writing-c-for-curl/ https://daniel.haxx.se/blog/2025/04/07/writing-c-for-curl/
- AdieuToLogic 11mo agoWhile the classic "Parse, don’t validate"[0] paper uses Haskell instead of C as its illustrative programming language, the approach detailed is very much applicable in these scenarios. 0 - https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- lelanthran 11mo ago> While the classic "Parse, don’t validate"[0] paper uses Haskell instead of C as its illustrative programming language, the approach detailed is very much applicable in these scenarios. Good thing someone (i.e. me) took the time to demonstrate PdV in C: https://www.lelanthran.com/chap13/content.html https://www.lelanthran.com/chap13/content.html
- nxobject 11mo agoI appreciate that link - now I see the parallels between “consolidate allocation in C to the extent that the rest of your code doesn’t have to worry”, and “consolidate validation in C” to the extent that…”.
- pizlonator 11mo agoJust compile it with Fil-C
- kazinator 11mo agoI definitely don't love C that does atoi on a Content-Length value that came from the network and passes that to malloc. Even before we get to how a malicious would interact with malloc, there is this: > The functions atof, atoi, atol, and atoll are not required to affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undefined. [ISO C N3220 draft] That includes not only out-of-range values by garbage that cannot be converted to a number at all. atoi("foo") can behave in any manner whatsoever and return anything. Those functions are okay to use on something that has been validated in a way that it cannot cause a problem. If you know you have a nonempty sequence of nothing but digits, possibly with a minus sign, and the number digits is small enough that the value will fit into int, you are okay. > A malicious user can pass Content-Length of 4294967295 But why would they when it's fewer keystrokes to use -1, which will go to 4294967295 on a 32 bit malloc, while scaling to 18446744073709551615 on 64 bit?
- noobermin 11mo agoThis seems more like a coding problem than a C problem. Most people would at least validate input before allocating anything.
- kazinator 11mo agoA HTTP request processing layer would typically have some configuration of the maximum content size that can be received in one request and reject anything larger. That's why, e.g., you have to mess with a "php.ini" file to get your self-hosted webmail to handle large attachments (which are uploads from the browser UI to the back-end).
- 1718627440 11mo ago> But why would they when it's fewer keystrokes to use -1, which will go to 4294967295 on a 32 bit malloc, while scaling to 18446744073709551615 on 64 bit? If that user wants to exploit your application it's better not to pass such a high value, since malloc typically detects size > SIZE_MAX/2. But then this code also doesn't check for malloc to return NULL, so this might also what leads to an exploit.
- qalmakka 11mo agoInteger operations, the one thing in computers where basically there's no non-annoying way to do them right except being over pedantic with checks
- qalmakka 11mo agoOT: using the `strcasecmp` family of functions is basically asking for trouble - unless you've previously set the locale to "C", which is basically the only locale with a defined behaviour. Otherwise you're basically bound to run onto very funny internationalisation issues you'd rather know nothing about (and fail the Turkey Test)