Tearing apart printf()
maizure.org
maizure.org
I was expecting the article to start off by 'tearing apart' the source code, then dig into the algorithms with which diverse formats are rendered, then compare a few popular implementations and finally provide valuable insights, something on the lines of what WalterBright mentioned and the brief discussion it started. If not all this, the minimum I expected was an abridged, annotated source code.
This article left me underwhelmed.
What I did (Datalight, Zortech) was make large disk I/O buffers. It gave huge boosts to speed, and very few people cottoned on to that :-)
It was a technique I learned from Shal Farley.
Fewer round trips to the operating system almost always speeds things up, too, especially if it involves a context switch.
Things got even better when the switch to hard disks was underway. Hard disks had bigger sector sizes, but the buffer size sometimes remained at 512. This meant the driver had to read the sector, fold in the 512, then write it back out. Oops!
Later on, demand paged virtual memory and memory caching also favored big buffers.
It is true however that sequential access to hdds and floppies was always much faster than random access or even sequential in small buffers simply because the drive does not need to wait for the platter rotation to reach the right location again for each sector written but rather just write them all in one go.
Or more "robustly", an expression that comes from external input (such as getchar()). Compilers can be smart enough to evaluate expressions which are constant.
This reminds me, I briefly played around with JIT'ing printf() itself --- if you look at it the right way, it's basically a "bytecode" interpreter for the format string. I did get some pretty good performance gains, but it turned out simpler for the project it was intended to go in to just switch to a binary protocol instead of text for output.
I also think that writing a (subset of) printf() is a useful exercise for those beginning programming C in general; it's not too difficult (at least if you avoid the floating-point stuff...), but also not too easy either.
https://blapid.github.io/cpp/2017/10/31/llcpp-a-quest-for-fa...
Admittedly, the book is more than 25 years old now, but I'm sure it would still make an interesting read:
https://www.amazon.com/Standard-C-Library-P-J-Plauger/dp/013...
However, the "Draft Standard C++ Library" book was finished just before the standard was radically overhauled for the STL, and the STL itself and the language have evolved massively, so in my opinion, those two book have not aged as well as the C library book has.
> %% - complete form
Wait, what? %n writes the current number of characters outputted to the int * you specify, while %% just prints a percent sign.
macOS, for one, has changed printf to SIGILL when passed a dynamic string that contains %n, though, which mitigates most of the issues present with format string attacks.
If you're going to align anything, you will (probably) need to write some code to compute the right alignment. At that point, you will need to split your printf into pieces, so why not just use printf's return code, which tells you the number of characters written?
glibc on Linux started hardening printf by replacing calls to printf with calls to __printf_chk, which will abort if the format string contains %n and sits in a writable segment of memory. The implementation is somewhat horrific - printf opens /proc/self/maps and range-checks the format string's address, but it works and does provide the necessary security.
It's much less common to pass unsanitized input to scanf than to printf, in my experience, because the most common error case of printf(buf) (instead of printf("%s", buf)) is just not something that you can do with scanf.
----
By the way, which version of macOS did they introduce the SIGILL hardening, and is it documented? I'm on 10.12.6, and the following program:
#include <stdio.h>
#include <string.h>
int main() {
char buf[] = "Hi %d Bye\n";
strcpy(buf, "Hi %n Bye\n");
int foo = 424242;
printf(buf, &foo);
printf("foo was set to %d\n", foo);
}
runs without complaint (Apple LLVM version 9.0.0 (clang-900.0.39.2)).The child comment provides a use case for %n, so I won't go into this here.
> glibc on Linux started hardening printf by replacing calls to printf with calls to __printf_chk
Only with -D_FORTIFY_SOURCE=2 set when compiling. Swapping printf calls with __printf_chk is off by default.
> The implementation is somewhat horrific - printf opens /proc/self/maps and range-checks the format string's address, but it works and does provide the necessary security
Luckily, macOS has a nicer Mach API for this, namely the vm_region/vm_region_64 family of functions: https://opensource.apple.com/source/Libc/Libc-1244.30.3/stdi... (search for __printf_is_memory_read_only).
> By the way, which version of macOS did they introduce the SIGILL hardening, and is it documented? I'm on 10.12.6
You're off by a version; this was introduced in 10.13. Your program crashes nicely on with Apple LLVM version 9.1.0 (clang-902.0.39.2) on macOS High Sierra 10.13.5 (17F70a). Documented? Ha, I wish. It just ended up breaking a couple of programs that relied on this behavior, since it would just call os_crash with "%n used in a non-immutable format string".
Suppose you are aligning subsequent output lines to the current line, and want to know the number of characters at certain points in the string. In fact this is probably the exact use-case for %.* and %n used together.
This is part of what makes format string vulnerabilities so nasty - they hand you a free leak in addition to %n being capable of very flexible overwrites.
This is a luxury that not all of us have ;)
> you can then use %N$n to write to any pointer in memory - if you can find your input string in memory (dereference stuff until you find the heap, for example), you can stuff pointers into it
You first have to get a valid pointer to something, which can be pretty hard when the addresses are randomized. Yes: if you can satisfy all the requirements for a printf attack, there's a whole lot you can do: overflow the stack and write to the return address, find the address of system, perform arbitrary reads or writes, etc. But as you've mentioned yourself, a successful format string exploit requires more than just "someone passed a user-controlled string to printf", at least as far as I'm aware.
Can anyone explain this to me?
Anyone got thoughts on what it might be?
##*#**##****#*#**/\##*###*****#**#*#*#**#******#**#*#*####*#*##*In nand2tetris, they work from a hardware simulation that represents memory where a block of that memory determines what the pixels on the screen are set to. Then you write code that can turn the pixels on or off, then a character OS library that converts binary ascii values into a combination of pixel settings that display a character.
Then you implement your OS's program loop (the "OS" is based around a one-shot "here a bunch of functions, run from an entry point" that you have to specify each time you turn it on) which has a specified entrypoint from which the rest is called.
Obviously, modern systems are a lot faster, but it was a great illustration of what has to happen in between, which this article is doing for real OSes.
inline void printf(const char* const s) { puts(s); }
void printf(const char* const s, ...); /* non-trivial implementation */
but that's not possible in C.E.g. gcc docs on this: https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
A system call is expensive, but not as expensive as a context switch between two user processes.