[go: up one dir, main page]

|
|
Log in / Subscribe / Register

Losing the magic

Losing the magic

Posted Dec 13, 2022 6:02 UTC (Tue) by tytso (subscriber, #9993)
Parent article: Losing the magic

The use of magic numbers is something that I learned from Multics. One advantage of structure magic numbers is that it also provides protection against use-after-free bugs, since you can zero the magic number before you free the structure, and even if you don't, when it gets reused, if everyone uses the magic number scheme where the first four bytes contain a magic number, then it becomes a super-cheap defense a certain class of bugs without needing to rely on things like KMSAN, which (a) is super-heavyweight and so won't be used on production kernels, and (b) didn't exist in the early days of Linux.

Like everything, it's a trade-off. Yes, there is overhead associated with magic numbers. But it's not a lot of overhead (and it's certainly cheaper than KMSAN!) and the ethos of "trying to eliminate an entire set of bugs" which is something is well accepted for making the kernel more secure, is someting that could be applied for magic numbers as well.

I still use magic numbers in e2fprogs, where the magic number is generated using the com_err library (another Multicism; where the top 24-bits identify the subsystem, and the low 8-bits is the error code for that subsystem). This means it's super easy to do things like this:

In lib/ext2fs/ext2fs.h:

#define EXT2_CHECK_MAGIC(struct, code) \
	  if ((struct)->magic != (code)) return (code)

In lib/ext2fs/ext2_err.et.in:

	error_table ext2

ec	EXT2_ET_BASE,
	"EXT2FS Library version @E2FSPROGS_VERSION@"

ec	EXT2_ET_MAGIC_EXT2FS_FILSYS,
	"Wrong magic number for ext2_filsys structure"

ec	EXT2_ET_MAGIC_BADBLOCKS_LIST,
	"Wrong magic number for badblocks_list structure"

The compile_et program generates ext2_err.h and ext2_err.c, for which ext2_err.h will have definitions like this:

#define EXT2_ET_BASE                             (2133571328L)
#define EXT2_ET_MAGIC_EXT2FS_FILSYS              (2133571329L)
#define EXT2_ET_MAGIC_BADBLOCKS_LIST             (2133571330L)
...

Then in various library functions:

errcode_t ext2fs_dir_iterate2(ext2_filsys fs,
			      ext2_ino_t dir,
...
{
	EXT2_CHECK_MAGIC(fs, EXT2_ET_MAGIC_EXT2FS_FILSYS);
        ...

And of course:

void ext2fs_free(ext2_filsys fs)
{
	if (!fs || (fs->magic != EXT2_ET_MAGIC_EXT2FS_FILSYS))
		return;
       ...
	fs->magic = 0;
	ext2fs_free_mem(&fs);
}

Callers of ext2fs library functions then will do things like this:

 	errcode_t    retval;

	retval = ext2fs_read_inode(fs, ino, &file->inode);
	if (retval)
		return retval;
or in application code:
		retval = ext2fs_read_bitmaps (fs);
		if (retval) {
			printf(_("\n%s: %s: error reading bitmaps: %s\n"),
			       program_name, device_name,
			       error_message(retval));
			exit(1);
		}

This scheme has absolutely found bugs, and given that there is a full set of regression tests that get run via "make check", I've definitely found that having this kind of software engineering practice increases developer velocity, and reduces my stress when I code since when I do make a mistake, it generally gets caught really quickly as a result.

Personally, I find this coding discipline easier to understand and write than Rust, and more performant than using things like valgrind and MSan. Of course, I use those tools too, but if I can catch bugs early, my experience is that it allows me to generate code much more quickly and reliably.

Shrug. Various programming styles go in and out of fashion. And structure magic numbers goes all the way back to the 1960's (Multics was developed as a joint project between MIT, GE, and Bell Labs starting in 1964).


to post comments

Losing the magic

Posted Dec 13, 2022 6:19 UTC (Tue) by Fowl (subscriber, #65667) [Link]

Is it still 'magic' if it's a vtable pointer? ;p

Losing the magic

Posted Dec 13, 2022 6:27 UTC (Tue) by tytso (subscriber, #9993) [Link]

And by the way.... the com_err library is not just used by e2fsprogs. It's also used by Kerberos, as well as a number of other projects that were developed at MIT's Project Athena[1] (including Zephyr[2], Moira[3], Hesiod[4], Discuss[5], etc.)

[1] http://web.mit.edu/saltzer/www/publications/atp.html
[2] http://web.mit.edu/saltzer/www/publications/athenaplan/e....
[3] http://web.mit.edu/saltzer/www/publications/athenaplan/e....
[4] http://web.mit.edu/saltzer/www/publications/athenaplan/e....
[5] http://www.mit.edu/afs/sipb/project/www/discuss/discuss.html

Losing the magic

Posted Dec 13, 2022 12:11 UTC (Tue) by mathstuf (subscriber, #69389) [Link] (9 responses)

I run my userspace under `MALLOC_CHECK_=3` and `MALLOC_PERTURB_=…` (updated occasionally by a user timer unit) to catch things like this. Is some kind of "memset-on-kfree" mechanism not suitable for debugging the entire kernel for use-after-free while also being far less heavy than KMSAN?

I ask because some day, a very smart compiler might see that dead write of `fs->magic = 0;` given the immediate free afterwards and optimize it out as UB to observe. Additionally, while it's also against UAF in ext2 code, non-ext2 code that gets its hands on the pointer that somehow that doesn't have the magic-checking logic is just as dead too (I have no gauge on how "likely" this is in the design's use of pointers).

Losing the magic

Posted Dec 13, 2022 13:08 UTC (Tue) by excors (subscriber, #95769) [Link] (8 responses)

> I ask because some day, a very smart compiler might see that dead write of `fs->magic = 0;` given the immediate free afterwards and optimize it out as UB to observe.

That day was at least 8 years ago. GCC 4.9 with -O1 will optimise away the writes, defeating this attempt at memory protection, because ext2fs_free_mem is an inline function so the compiler knows the object is passed to free() and can no longer be observed. See e.g. https://godbolt.org/z/nWYEa34a6

I guess the cheapest way to prevent that is to insert a compiler barrier (`asm volatile ("" ::: "memory")`) just after writing to fs->magic, to prevent the compiler making assumptions about observability of memory.

Losing the magic

Posted Dec 13, 2022 14:47 UTC (Tue) by adobriyan (subscriber, #30858) [Link] (7 responses)

This is the usecase for "volatile": *(volatile int *)&fs->magic = 0;

Losing the magic

Posted Dec 14, 2022 2:46 UTC (Wed) by nybble41 (subscriber, #55106) [Link] (6 responses)

Even then, couldn't the compiler just set it back after the volatile write, but before the memory is freed? Seeing as how it's unobservable and all. Perhaps it decides to use that "dead" memory as scratch space for some other operation.

Losing the magic

Posted Dec 14, 2022 16:47 UTC (Wed) by adobriyan (subscriber, #30858) [Link] (4 responses)

I don't think so. "volatile" means "load/store instruction must be somewhere in the instruction stream" which is what needed.

Losing the magic

Posted Dec 14, 2022 17:10 UTC (Wed) by mathstuf (subscriber, #69389) [Link] (3 responses)

It means that the value being represented is not trackable in the C abstract machine and therefore no assumptions can be made about it. Because no assumptions can be made, optimizers are hard-pressed to do much of anything about it because the "as-if" rule is likely impossible to track accurately.

However, given that this is trivially detectable as about-to-be-freed memory, I don't know what kind of rules exist around "volatile values living in C-maintained memory" might allow even this to still be seen as a dead store and unobservable via UAF == UB.

Losing the magic

Posted Dec 14, 2022 19:31 UTC (Wed) by farnz (subscriber, #17727) [Link] (2 responses)

If I'm reading the standard correctly, the compiler has to output the stores, because it is possible that the program has shared that memory with an external entity using mechanisms outside the scope of the standard. What the implementation does after the memory is freed is not specified (although the implementation is allowed to assume that the memory is no longer shared with an external entity at this point), and in theory a sufficiently malicious implementation could undo those final stores after you called free, but before the memory is reused.

In practice, I don't think this is a significant concern for tricks intended to help with debugging. It is for security-oriented code, but that's not the case here.

Losing the magic

Posted Dec 14, 2022 23:25 UTC (Wed) by mathstuf (subscriber, #69389) [Link] (1 responses)

Can the C abstract machine really say that memory it obtains through `malloc` has some other magical property? Wouldn't that require you to get "lucky" with what `malloc` gives you in the first place to have that address space "mean something" to some other part of the system?

Maybe the kernel gets away with it by "hiding" behind non-standard allocation APIs…

Losing the magic

Posted Dec 15, 2022 10:47 UTC (Thu) by farnz (subscriber, #17727) [Link]

It's more that you can have an external observer outside the abstract machine, but able to understand abstract machine pointers; I can, in theory, store a pointer from malloc in a way that allows the external observer to reach into the abstract machine and read the malloc'd block. I can also have the external observer be looking not at the addresses, but at the pattern of data written into the block (just as in hardware, it's not unknown to have chips only connected to the address bus, and to rely on the pattern of address accesses to determine what to do).

The compiler is not allowed to make assumptions about what the external environment can, or cannot, see, and thus has to assume that any write to a volatile is visible in an interesting fashion.

Losing the magic

Posted Dec 14, 2022 18:33 UTC (Wed) by excors (subscriber, #95769) [Link]

> Even then, couldn't the compiler just set it back after the volatile write, but before the memory is freed? Seeing as how it's unobservable and all. Perhaps it decides to use that "dead" memory as scratch space for some other operation.

It probably could, but I don't think it's particularly fruitful to consider what the compiler 'could' do, because the goal of the magic numbers here is to detect use-after-free bugs, i.e. we're interested in the practical behaviour of a situation that the standard says is undefined behaviour. We're outside the scope of the standard, so all we can do is look at what GCC/Clang actually will do.

If there is no barrier or volatile, and some optimisations are turned on, they demonstrably will delete the write-before-free. With barrier or volatile, it appears (in my basic testing) they don't delete it, so the code will behave as intended - that doesn't prove they'll never delete it, but I can't immediately find any examples where that trick fails, and intuitively I think it'd be very surprising if it didn't work, so I'd be happy to make that assumption until shown a counterexample.

(The same issue comes up when trying to zero a sensitive buffer before releasing it, to reduce the risk of leaking its data when some other code has a memory-safety bug - you need to be very careful that the compiler doesn't remove all the zeroing code, and you can't look to the C standard for an answer.)


Copyright © 2026, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds