size_t-to-int vulnerability in Linux’s filesystem layer
openwall.com
openwall.com
If there's one place other than the kernel where truly defensive programming should be applied, it is systemd.
My "systemK" joke was indeed implying what you said, that systemd is "a key place in the stack where you would need to be super careful." (Almost Kernel-like.)
And my question was legitimate, although poorly-researched. Answering myself: CVE-2021-33910 only affects systemd, not all FUSE in general.
This is likely because Unix semantics treat the init process specially: any process whose parent dies is re-parented to the init process. It's not clear what should happen to these processes if the init process itself went away, so the kernel just gives up.
[1] https://elixir.bootlin.com/linux/latest/source/kernel/exit.c...
In a server I generally want a crash immediately. But on a desktop I'd rather it limp along and give me a chance to finish writing my Hackernews post.
It depends really what your server does, and what the consequences of it doing the wrong thing are.
Keeping it small and simple to minimize bugs is perfectly viable and reasonable.
Fire an alert, remove that server from the load balancer, and fix the problem without your customers even noticing.
Or make sure if you're running a hobby-project architected platform that your customer expectations and SLAs are clear up front, and let it go down until Monday morning when you'll get around to fixing it.
I'd much rather _not_ have my desktop "limp along" in a poorly understood and probably exploitable fashion while the malware gets a chance to finish encrypting all my files...
If that costs the world the "benefit" of my shared wisdom in a half written Hackernews post, I'm good with that.
I call these the "already lost" situations. You've already lost, we're just arguing about how to distribute the lossage. While those discussions aren't completely pointless, it is important to keep it clear in our head we're arguing about how to pick up the bodies at a crash site and not how to prevent the crash in the first place; it's a different mindset.
Despite some moderately-justified mockery in the other messages in this thread, the answer really is "just don't crash and have secure code here", which is to say, "don't lose". It's exceeding hard to write and it's a very high bar, but at the same time, it's very difficult to imagine how to secure a single system when you can't even stipulate a core of trusted software exists. If you don't even have a foundation, you're not going to build a secure structure. In this case, by "secure" I don't just mean security, but also, functionality and everything else.
Systemd of course goes in the opposite direction: It assimilates as much functionality as possible from the OS into systemd (though to be fair, not all into PID1).
"Ummm, lets not do that, it's not such a great idea..." -- Windows Vista team, 2006
https://docs.microsoft.com/en-us/previous-versions//cc750820...
(Well, it came out late '96, so I suppose a bunch of that was actually done in '95.)
A proper init system similar to runit or s6 would be written in something safer (minimum unsafe) like Rust, be modular, simpler, follow UNIX philosophy, and not try to do everything in one process. Microkernel-style.
strdupa(input) without any length check
Fix is to replace it with unbounded malloc() instead of checking for sane length first.
And yes, I know that really that is only a limit of the system call path lenght, and in theory you can work with longer paths (by changing the current directory to a path and then opening a file from there), because filesystems does (stupidly in my opinion) support it.
But in reality, how many applications will break? Does it make sense to support them?
Also the code in question seems to be dealing with a filename more than a path. A file name shouldn't be longer than NAME_MAX, and that is an hard limit of many (possibly all?) filesystems, as far as I know. So why?
It would be simpler and more optimized to just truncate the name at PATH_MAX. Avoid the overflow and the crash but give an error. Why hard limits are considered that bad? We waste time supporting edge cases that no one would really use in a real system (no way someone needs a path longer than 4096 bytes...), for what? In Windows the limit is 260 characters, and nobody seems to be bothered by that, only in Windows 10 you can increase that.
$ strace -e trace=file perl -e 'open(FH, "<", "/" x 4096)'
…
openat(AT_FDCWD, "//////////…"..., O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENAMETOOLONG (File name too long)
+++ exited with 0 +++EDIT: And on Solaris PATH_MAX is 1024 and (AFAICT) Solaris also copies paths into kernel space. It seems I was confusing things with NL_TEXTMAX, which is INT_MAX on glibc (but not Solaris).
The Linux kernel defines upper limits for NAME_MAX (255) and PATH_MAX (4096).
The glibc doesn't enforce this limit because it was originally written to run on GNU HURD which I guess doesn't have these limits.
But systemd only runs on glibc on Linux. So I don't see why it doesn't at least sanity check the length of absolute paths with PATH_MAX...
Does the code in question ever run in a tight loop (e.g. on file operations after the filesystem is mounted), or just at mount time?
If it's just at mount time, "reducing performance" by one malloc vs a stack adjustment doesn't seem to me like it should be a primary concern.
If PID 1 does need to do that kind of stuff (as it seems), I would prefer it to fork a process and do the memory allocation in that, so if that process crashes because you are out of memory the kernel doesn't panic.
That almost sounds like the 260 character windows path limit constant used by some ancient APIs. I would assume that any API limited to that path length is dated and probably unreliable in various contexts as the wikipedia article on filesystems explicitly gives the limit as not defined for various Linux filesystems. Also given the recent talk about in kernel support for NTFS (path limit ~2^16) I assume that any historic code still relying on PATH_MAX needs to be fixed.
PATH_MAX is a limit of a path that you can pass to the various path manipulating functions, open(), unlink(), etc, or returned by getcwd() (that gives an error if path is longer than PATH_MAX, and yes there are non standard system call to go around this limit but... why?)
You can however use paths longer by PATH_MAX, how? Simply chdir() PATH_MAX, then you can chdir() another PATH_MAX, then count how many software breaks...
Imposing a limit on paths makes sense and should be done. 4096 bytes seems reasonable to me. Also, in that example, it wasn't even a matter of a path! They are parsing it seems only the file name, and that is defined to be NAME_MAX, that is 255 bytes, on every system and every filesystem!
I also do never understand why some libraries use "faster" methods everywhere, unless safer ones. it's not like all interfaces to systemd would need to be fast. but they should be secure.
https://capsule8.com/blog/exploiting-systemd-journald-part-1...
Consider that in Linux a path is defined to be a maximum length of PATH_MAX, that is defined to 4096 bytes, and a filename (and directory name) shouldn't be longer than FILE_MAX that is 255 bytes. This limits are defined in the headers and I use them always in writing my C programs (if it crashes... you are doing something really wrong!).
So how the hell do you have a directory that is more than 8Mb? You shouldn't! The filesystem doesn't support it. It's a matter of the filesystem driver that should reject a path that long in my opinion.
Systemd should be fast. It's at the base of the operating system. Also it should consume little memory. You can say, who cares about allocating dynamically a string, or allocating a static buffer of 16Mb, yes we should care, I use Linux computer with 16Mb of RAM, total. Of course they don't run systemd nowadays since it's too big, but in my opinion systemd is good, and I would like to see it more in the embedded world.
Remember that Linux supports hierarchical mounts! You can mount anything at any depth of directory nesting. Even if it were true that MAX_PATH were an FS limitation, you could still nest mounts and encounter absolute paths exceeding MAX_PATH. MAX_PATH is simply the length in bytes of the longest string you should expect system calls to accept as a path parameter.
> I use Linux computer with 16Mb of RAM, total. Of course they don't run systemd nowadays since it's too big, but in my opinion systemd is good, and I would like to see it more in the embedded world.
It sounds like using systemd is a terrible idea for memory-constrained devices, so you really don’t want to see it in the embedded world.
On the other hand, proper event-driven init system (instead of horrible shell scripts with all sorts of fragile "sleep"s and other hacks) sounds sexy for an embedded system. I sometimes get annoyed how home routers, NAS, etc. are slow to boot up
Though the embedded systems I refer to have much more than 16 MB of RAM, more like 128 and up.
ext4 doesn't limit directory depth; only filename length. A filename can be 255 bytes in ext4. How deep that lies in the filesystem isn't limited.
btrfs has the same filename limit, no underlying limit on directory depth.
And I would most likely guess most filesystems don't because the obvious ways to implement directories don't place limits on that depth.
I don't think you could even call strdupa through libc in rust. I would guess that strdupa is either a macro that uses the alloca compiler intrinsic or is itself a compiler intrinsic. Even if it isn't, it will break assumptions the rust compiler makes about the size of the stack frame.
For me it crashes into the fork_userns:177
PS: don't need to downvote. Sometimes managers want you to prove that there's a need to patch. It's dumb but it's what it is
Is this the type of things that could be caught by a linter or strict compilation rules? This seems to be to be a failure of the type system.
#include "stddef.h"
short foo(short a) { return a % 42; }
size_t bar(void) {
size_t sz = ~0UL;
return foo(sz);
}
https://godbolt.org/z/3ec9v8Pa4Personally I have never seen gcc spitting out a false positive. IMO it's always a good idea to explicitly downcast even if you know that it's 'safe'. That way someone else will see instantly what's going on. The fact that Rust requires it should tell us something.
You can find more cases in the bugtracker. To be fair, it seems many of them were fixed in recent releases.
warning C4267: 'argument': conversion from 'size_t' to 'short', possible loss of data
https://godbolt.org/z/nYeWT7zv6 (/W3 is the default warning level when creating a new project)I saw these warnings so often that I assumed that every compiler had them.
typedef struct {
unsigned value : 4;
} S;
void foo(S* s, unsigned value) {
// error: conversion from 'unsigned int' to 'unsigned char:4' may change value
s->value = value;
}
I mean, I guess I can see the rationale.. it's just annoying to have to resort to using pragmas to turn off -Wconversion whenever I need to assign to a bitfield.https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidel...
But strict compilation rules (eg, clang's -Weverything) mainly only work if you treat them as errors (so -Werror), and then some of those strict are then also questionable at best, and just outright annoyingly wrong at worst. For example, unused parameter warnings on virtual methods are a waste of time to deal with. It's not a symptom of a bug most of the time, so it being an error just generates workaround churn or you end up just disabling the warning and then maybe that bites you the few times it would have pointed out an actual issue.
Beyond the blanket ones like clang's -Weverything, it can otherwise be a job to keep up with compiler upgrades and the vast number of warning options they have.
Why is that even a warning? If at least one of the implementers use a parameter and a warning is shown the warning itself is wrong. That’s just broken implementation of the warning?
And it's because the kernel does a lot of non-standard things, mostly because it has to. It is not a normal program.
https://clang.llvm.org/docs/DiagnosticsReference.html#wshort...
The more general-purpose -Wconversion has many false positives, often around int to char conversion. Some functions like int toupper(int) have an unexpected int return type to deal with special out-of-bound values like EOF.
(Some Rust developers argue that even "size as i32" should be avoided, and "size.try_into()" should be used instead, since it forces the programmer to treat an overflow explicitly at runtime, instead of silently wrapping.)
class TimestampedObject implements Comparable<TimestampedObject> { long timestamp; int compareTo(TimestampedObject other) { return (int)(timestamp - other.timestamp); } ... }
ErrorProne catches this: https://errorprone.info/bugpattern/BadComparable
Adding .into() works though, which is the recommended method if the conversion can be statically guaranteed (otherwise try_into should be used, which will become easier in the 2021 edition as the TryInto trait will become part of the prelude).
All explicit conversions, more so fallible, are "frustrating in practice" especially when coming from a language without those foibles.
But given the semantics of usize/isize, it is perfectly reasonable, nay, a good thing, that they're considered neither widenings nor narrowings of other numeric types.
usize is already on that hook: `0x100000000usize` will fail to compile in 32 bit, so you already risk compile errors when switching archs.
As it is I'm just writing `as u64` which is clearly worse.
We don't even use full 64bit pointers today on x64.
push x // 256 bit constant
LOAD
// top of stack now contains storage[x]You could think of checked memory models where half of the 256 bit address is a 128 bit random key needed to access some allocation, or maybe even a decryption key. Similar things are done with the extra space of x64 as well.
Also, 128 bit numbers are still quite uncommon in Rust. Easy conversion of usize to them wouldn't be that useful if conversions of the other number types don't work.
People have said that about pretty much every memory size in the history of computing. The argument for 64-bit is not "2^64 bytes ought to be enough for anyone"; it's "you couldn't use more than 2^64 bytes even if you wanted to". Writing a full register worth of data every clock cycle at 4GHz works out to 32GB/s. (2^64B / 32GB/s) is just over seventeen years, to fill up a 64-bit address space, assuming you're doing no actual computation. Few computers even work for seventeen years without replacement, much less run single processes continually with no reboots.
It would have cause more bugs if they defined it the other way and people used usize to store addresses.
It's important to still have the option for efficient truncating semantics, though; some software (e.g. emulators) needs to chunk large integers into 2/4/8 smaller ones, and rotation + truncating assignment is usually the cheapest way to do that.
But, importantly, this is a rare case. Most software that does demoting casts does not mean to achieve these semantics.
So I wonder — are there any low-level/systems languages where a demoting cast with a generated runtime check gets the simple/clean syntax-sugared semantics (to encourage/favor its use), while truncating demotion requires a clumsier syntax (to discourage its use)?
edit: a cast will throw an exception if it fails, in case that was not clear from context.
Then safety is free...
I know Erlang's BEAM VM has a "fail jump pointer register", where instructions that can fail have relative-jump offsets encoded as immediates for those instructions, and if the instruction "fails" in whatever semantic sense, it takes the jump.
But most CPUs don't have anything like that.
Would you want it to trap, like with integer division by zero?
CPU traps are pretty hard to handle in most language runtimes, such that most compilers generate runtime checks to work around them, rather than attempting to handle them.
I always thought that overflows should be checked in hardware, I suppose it's not a stretch to extend that to truncation. It's controversial though, and obviously mostly a thought experiment anyway unless we manage to convince some CPU manufacturer to extend their ISA that way.
MIPS does have (optional) trapping overflow on signed add/sub overflow, so at least there's a small precedent for it.
It seems obvious that future Apple CPUs will have hardware support for this, if they don't already.
Given that Apple was one of the original founders of ARM it’s quite possible that their license allows much more latitude anyone else’s.
If AMX is allowed under their license, there is no reason why checked extensions would not be.
let x = size as i32;
... even if what you meant was closer to let x: i32 = size.try_into().expect("We are 100% sure size is small enough to fit into x");
But at least it isn't C or C++ where you might accidentally write x = size;
... and the compiler doesn't even warn you that size is bigger than x and you need to think about what you intended.It's really hard to fix this in C++. Some of the Epoch proponents want to do so using epochs to get there, basically you'd have a "new" epoch of C++ in which narrowing must be explicit, and old code would continue to have implicit narrowing so it doesn't break.
That's not true tho, compiler with reasonable flags set will definitely warn you and if you really don't like this kind of code you can force compiler to issue an error instead
int main(void) {
int some_int = 1234567;
char c = some_int;
return c;
}This is GNU's idea of "all".
Contrast to Clang's -Weverything, which will.
I can see the reason behind it, but I feel that this behavior is something you opt into when you use -Werror.
It feels like the "right thing" here would instead be for the compiler to allow build scripts to reference a specific point-in-time semantics for -Wall.
For example, `-Wall=9.3.0` could be used to mean "all the error checks that GCC v9.3.0 knew how to run".
Or better yet (for portability), a date, e.g. `-Wall=20210720` to mean "all the error checks built into the compiler as of builds up-to-and-including [date]."
To implement this, compilers would just need to know what version/date each of their error checks was first introduced. Errors newer than the user's specifier, could then be filtered out of -Wall, before -Wall is applied.
With such a flag, you could "lock" your CI buildscript to a specific snapshot of warnings, just like you "lock" dependencies to a specific set of resolved versions.
And just like dependency locking, if you have some time on your hands one day, you could "unlock" the error-check-suite snapshot, resolve all the new error-checks introduced, and then re-lock to the new error-check-suite timestamp.
[1]:https://docs.microsoft.com/en-us/cpp/error-messages/compiler... [2]:https://docs.microsoft.com/en-us/dotnet/fundamentals/code-an...
Personally i would vote for "Wall with Werror" means no guarantee for your build.
At least with -Werror on at all times, devs will tend to upgrade before the very-stable CI environment does, and thereby catch the problem at development time (usually less time-pressure) rather than release-cutting time (usually more time-pressure, esp. if the release is a hotfix.)
-----
Mind you, it does work to enable -Werror only in CI, if you lock your CI environment / compiler Docker image / etc. to a specific stable version, and treat that as the thing to re-lock in place of the "error-check suite snapshot version."
This has the disadvantage, though, that you can't take advantage of newly-stable/newly-unstable language features, or of newly-introduced compiler optimizations, without biting the bullet and taking on the work of fixing the errors introduced by re-locking the base-image.
With a separate flag for locking down the error-check-suite snapshot version, you could continue to upgrade the compiler — and thereby get access to new features / optimizations — while staying on a particular build regression "scope."
If you don't want your build to fail on warnings, don't use -Werror. If you want it to only fail on specific warnings, use -Werror=...
> with nobody sure why
Unless they look at the errors in the compiler output. What does it matter if it was brought on by a compiler update or a push?
> At least with -Werror on at all times, devs will tend to upgrade before the very-stable CI environment does
Nothing wrong with -Werror for devs - the problem is when you ship code to others and leave -Werror on by default.
And it is false. My default configuration C++ project created in Clion shows it very clearly, and even pesters to use int32/int64 over int/long.
But as usual the default fallback when you're wrong about C++ is "uh yeah but lotta footguns amirite"
As if there aren't enough that we need to start making them up...
0: https://developers.redhat.com/blog/2018/03/21/compiler-and-l...
Now that the default in the most beginner friendly of IDEs catches it, the goalpost is "my pet source of customization designed with C++98 in mind doesn't catch this"
Of course, even your pet source of customization caught up: https://developers.redhat.com/blog/2021/04/06/get-started-wi...
I mean MSVS uses Clang-tidy too, Clang-tidy integrates style guides provided by Mozilla and Google.
Most C++ Google projects have clang-tidy configs.
Clang-tidy is literally table-stakes for modern C++ tooling.
Github shows 970,000 commits related to setting up clang-tidy
But uh, yeah, let's see where the goalpost skitters to next.
-
The irony is I said above, C++ has enough footguns without sticking your fingers in your ears and ignoring boring, easy to setup, widely well known and well used tooling.
But in the war against C++ no stone must be left unturned.
C++ is a tiny fraction of all the code I've written in my life but it irks me to no end that people can't deal with the idea that language safety can improve, that tooling can be considered part of that safety. Or rather they can... unless they're talking about C/C++
I thought the point I had been making, as had others, is that by default this is an easy footgun.
There are all sorts of things that can be added on to all languages to help - if you know it’s a problem worth solving, etc. which is inevitably after you’ve footgunned yourself with it bad enough you felt the need to research how to prevent it.
Other languages just do the safer thing (or most compilers By default warn at least about common footguns) more - which is the whole point of this thread?
But tooling that is incredibly common, that beginners will run into even if they take the path of least resistance, and experts will use because it enforces standards at the very least, covers it.
Like Js without linters is a minefield, but everyone accepts you should lint your Js. Why does that change when C++ is involved?
The goalpost, since you're insistent on being explicit about it, was whether a C/C++ compiler "with reasonable flags" will catch the implicit wrap. GCC is a very popular compiler, and to be honest, I'm still not sure how to get it to warn on the above code, if doing so is possible.
Edit: Just read the rest of the thread, it's -Wconversion, which I suppose makes sense. Ignore me, point taken.
Unfortunately, over the years people baked the semantics of -Wall into their builds so new diagnostics could not be added to that flag.
And clang’s -Weverything shows how the opposite can fail as well
Also there are some warnings that won't be produced if you compile without optimization, because the needed analysis isn't performed.
-Wconversion
https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
This is common knowledge for ages. Any cursory Google search returns countless answers.
Take this post made over a decade ago.
https://stackoverflow.com/questions/1730255/gcc-shouldnt-a-w...
x = { size };
The "as" operator is often considered to have been a mistake. Both because of unchecked casts and because of "doing to much".
So I wouldn't be surprised if in the (very) long term there will be a rust edition deprecating `as` casts (after we have alternatives to all cast done with `as`, which are: Pointer casts, dyn casts/explicit coercion and truncating integer casts, for some we already have alternatives for on stable for other not).
And for all who want to not have `as` today you can combine extension traits (which internally still use `as`) + clippy lint against any usage of `as`.
EDIT: I forgot widening integer casts in the list above ;-).
Imagine there's a suitable narrow::<type>() function introduced which has the same consequence, always narrowing, if your data was too wide it may drop important stuff on the floor, and narrow() just says that's too bad.
Rust 2030 can introduce narrow::<type>(), warn for narrowing as usage and then Rust 2035 can error for as. The Rust 2030 -> 2035 conversion software can consume code that does { x as y } and write { x.narrow::<y>() } instead. This code is not better but it's still working in Rust 2035 and this explicit narrow() function is less tempting for new programmers than as IMO.
// a is a UInt64
let a = Int.random(in: 0..<Int.max)
// causes a fatal runtime error if out of range, halts execution
let b = UInt32(a)
// returns a UInt32?, which will be nil if out of range
let c = UInt32(exactly: a)
// another approach for exact conversion:
guard let d = UInt32(exactly: a) else {
// conversion failed
// handle error and return
return
}
// 'd' is a UInt32 without any bits lost
// always succeeds, will return a UInt32, either clamped or truncated. Truncation just cuts off the high bits.
let e = UInt32(clamping: a)
let f = UInt32(truncatingIfNeeded: a)Not exactly the same thing, but in a related area C++ does this a bit.
In C++ you can always still do a c-style cast `(int) some_var` (and the implicit casts obviously), but in general you're meant to use the C++ style explicit casts like `static_cast` and `const_cast`. These are generally tidy, but the most powerful and dangerous of these casts is deliberately awkwardly named as `reinterpret_cast<int>(some_var)` rather than something terse.
It's always easy to spot during a code review.
I thought about this very, very carefully when designing Virgil[1]'s numerical tower, which has both fixed-size signed and unsigned integers, as well as floating point. Like other new language designs, Virgil doesn't have any implicit narrowing conversions (even between float and int). Also, any conversions between numbers include range/representability checks that will throw if out-of-range or rounding occurs. If you want to reinterpret the bits, then there's an operator to view the bits. But conversions that have to do with "numbers" then all make sense in that numbers then exist on a single number line and have different representations in different types. Conversion between always preserve numbers and where they lie on the number line, whereas "view" is a bit-level operation, which generally compiles to a no-op. Unfortunately, the implications of this for floating point is that -0 is not actually an integer, so you can't cast it to an int. You must round it. But that's fine, because you always want to round floats to int, never cast them.
While some folks aren't too fussed about warnings like this, those folks generally aren't writing secure code like kernels. I'm very surprised that kind of conversion was permitted in the code.
https://rust-lang.github.io/rust-clippy/v0.0.212/#cast_possi...
We consider it best practice to try and organize the types such that explicit casts are minimized.
This os why in C it is a good practice to enable all compiler warnings and to have the compiler treat warnings as errors.
If you write for C compiler Foo 8, there's a decent chance Foo 9 will raise a warning which didn't exist before. Now you have to handle "why doesn't this compile" issues and distributions have to patch your sources to do future releases. And that's ignoring bugs like GCC in the past where in some versions you could not satisfy specific warnings.
But never ever leave -Werror enabled for building in Release mode. You'll be preventing your code from building as soon as a new compiler version goes out. Maintainers or code archeologist will have a much worse time than if this option was simply disabled to start with.
IMHO one should always be using C99 types instead of int, but Linux predates that.
Also, shouldn't that implicit conversion cause a compiler warning?
But of course getting C programmers to use these integer types rather than the ones they grew up with isn't easy.
That would lead to a hole in the type sequence (char <= short <= int <= long <= long long) for 64-bit targets (where int is the 32-bit type while size_t is 64 bits).
> IMHO one should always be using C99 types instead of int, but Linux predates that.
On the other hand, on Linux "long" has always been defined as the same size as size_t, so using "long" instead of "int" everywhere could also be an option.
"Always" is a strong way of putting it, there are often times where it makes sense to use the platform's "natural" word sizes (which is the entire point of having `int` `long` `long long` etc.)
In those cases we probably don't care about the full range of the larger types, so it doesn't hurt to use the smallest type for a range of expected values. If it does make a difference, the program will behave differently when compiled on a different arch or even a different compiler.
But maybe "generally" instead of "always". OTOH even I am guilty of using an int to loop over an array.
ILP64 causes a lot of problems, most notably needlessly-increased memory usage and, in C, the inconvenience of requesting a 32-bit type when int is 64-bit. It's rather uncommon to actually need the extra 64-bit range except when describing pointer addresses and memory/disk sizes, both of which benefit from an explicit intptr_t/size_t type for readability if nothing else.
In Haskell I would use an exception, and mark the function as unsafe, but the stdlib seems to disagree with me here.
I'm clueless about security: where does this fall on the scale of non-issue to critical? It strikes me as tending towards the latter, given that it enables unprivileged users to become root. Any insight into past Linux Kernel vulnerabilities that were severe?
If the attacker already has that level of unauthorized access, you're already doomed.
It also breaks sandboxing. To whatever extent you're trying to run programs that are somehow jailed, so you can download and run them without worrying about them taking over your system, kernel LPEs break those assurances.
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux...
Got unrootable old (but still fully working) phone there, might try to play with it.
m->size must be of type size_t. It's slightly mind-blowing to me that casting to a smaller unsigned int can cause a vulnerability. But I guess unintended behavior (not undefined) can do that.
Should we be afraid developers will stop caring? Did they even care in the first place?
...so basically 32-bit systems are totally unaffected (and I believe size_t and int are the same size there anyway), but I think bugs like this are easily prevented by simply imposing sane limits --- there is zero reason to even consider allowing a path more than a few K in length, and IMHO even that is overly generous.
While I know there's a lot of hate for Windows' traditional 260-char limit, I personally haven't run into it as a developer except by accident (e.g. runaway recursion) and it's very comforting to know that the limit is there so code will fail before it consumes the disk or memory entirely.
Clearly not a nodejs developer then! npm's insanely nested dependency graph caused me to hit the 260 character limit relatively regularly. (though this was several years ago, so maybe they have mitigations for that now)
C:/current/working/directory/that/is/reasonably/deep/testrun/YYYY_MM_DD_hh_mm_ss/code/moduleName/src/build/bin/../../../../../../outputs/moduleName/YYYY_MM_DD_hh_mm_ss/outputFolder/moduleName_testrun_results.txt
or some garbage like that (you get the picture). Yes, half the path was taken by descending into some directory that it went out of again straight away. Test runs would fail because of the 260 char limit unless we cut down the module name length. (Thankfully this did not need to be done in the code itself, just in the test run invocation.)In general, no. Attackers are clever and can usually find their way around arbitrary limits when a bug exists. Sometimes such a restriction might stop them, but more often than not they’ll bypass it some other way.
That's over 9MB, running in supervisor mode.
2021, and people are still surprised every time a kernel bug with security implications is found.
Maybe it is time to look at different OS designs. Large companies such as Google (Fuchsia) or Huawei (HarmonyOS) have begun to pick up on this.
Making the kernel as small as possible, and having components and drivers run unprivileged is the primary way to achieve this.
Some real examples that do this: Minix3, Haiku, Genode, Fuchsia, Harmony.
It’s like you sent a 1kg package through the postal service, and then the recipient gets an envelope containing a piece of cardboard from the original packaging.. And everyone involved is somehow A-OK with all of this.
If your programming language silently converts between types (in any direction), just to accommodate the programmer, instead of them specifying what they actually want to compute, you simply have failed as a programming language designer.
That's some hubris. The C language is 49 years old. Dennis Ritchie made reasonable design decisions for the time he found himself in. I think we should be understanding of that, and the network effects that lead to large parts of the world's critical software infrastructure being implemented in C. I don't think he failed at anything.
I used to be a C developer. I know how easy it is shoot your foot off in C. I think that, arguably, as an industry we should think twice before building more big and/or critical systems in C. There are better tools now.
But we are where we are and it's important to understand how we got here. Castigating our predecessors as failures does them a disservice.
That’s my point, no shade on K&R though.
age is orthogonal to good design.
I was just excusing K&R from the realization we have now in the 2020s, and I would obviously afford the same leniency to John McCarthy and lisp.
However with the painful experience that was the Python 2 to 3 debacle, it's clear to me that the only way to do such upgrade is with an all-in commitment. See Ruby: breaking compatibility hasn't ever been as discussed and polemic as in Python. You just upgrade and tell the world: here's the new version, and the old one will be supported for not a day further than 4 years.
People would complain but at the end of the day the world keeps turning. We could be already at C 3.0 and be much happier without all the old compatibility baggage that the language drags with it.
"Although we entertained occasional thoughts about implementing one of the major languages of the time like Fortran, PL/I, or Algol 68, such a project seemed hopelessly large for our resources: much simpler and smaller tools were called for. All these languages influenced our work, but it was more fun to do things on our own. "
How?
Or have you, completely on your own, decided that my criticism of programming language design in the 2020s is somehow applicable to languages “literally” designed in the 1970s?
Why not go one step further and deny Alan Turing and Alonzo Church, and their achievements…?
Also, tangentially related is the signed/unsigned business which tends to get in the way frequently. For example, OpenMP 2.0 (the only OpenMP version you get to use with MSVC) requires loop indices to be signed, but both std::vector::size and std::vector::operator[] deal with unsigned integers. Casts guaranteed!
Oh, I didn't know Linux supports GB long path name. On Windows it's limited to something like MAX_PATH_LENGTH which was defined as 200+ chars when I worked on it.
* the system must have LongPathsEnabled set (though it might be the default nowadays, not sure)
* the application itself must have `{http://schemas.microsoft.com/SMI/2016/WindowsSettings}longPa...` set in its manifest
IIRC, its a mess because it was lifted inconsistently for different access methods (APIs, and as a consequence UI/CLI methods that depend on them), and at least for some in ways which also are or were dependent on how paths are expressed.
So it is, or at least has historically been after it was first “lifted”, a minefield of inconsistent, surprising behaviors with plenty of gotchas if you didn’t treat it as if it were still a limit.
Since W10 Anniversary Update, there is a setting to disable the MAX_PATH limitation in various APIs, but applications still have to opt into long path awareness via a manifest key.
>PATCH 3.12 108/142] fs/seq_file: fallback to vmalloc allocation
>Signed-off-by: Linus Torvalds <torvalds@xxxxxxxxxxxxxxxxxxxx>
Oh maybe not this time :-)
I thought js "integers" are just floating point numbers?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
A newly designed OS kernel could perhaps take on this kind of feature. This would be the kind of OS that could be formally verified and would be willing to pay that runtime cost for arbitrary precision.
typedef struct { /* TODO */ } arb_int;
arb_int *new_arb_int(void);
void delete_arb_int(arb_int *);
arb_int *arb_int_add(arb_int *, arb_int *);
arb_int *arb_int_sub(arb_int *, arb_int *);
arb_int *arb_int_mul(arb_int *, arb_int *);
arb_int *arb_int_div(arb_int *, arb_int *);
bool arb_int_gt(arb_int *, arb_int *);
bool arb_int_lt(arb_int *, arb_int *);
bool arb_int_gte(arb_int *, arb_int *);
bool arb_int_lte(arb_int *, arb_int *);
bool arb_int_eq(arb_int *, arb_int *);Try this:
>> const x = 288230376151711740;
>> x == x + 1
true
Or this: >> 2\*1024
Infinity
JS doesn't even have integers. It only has floats. JS is by far the worst language I know of when it comes to integer support.If you want a better example of arbitrary precision integers, try Python, for example.
I also worked on a project once where unique IDs of objects could get quite large (because they were random u64 integers). Those IDs were serialized and sent to a browser. Sometimes two objects were viewed as "the same" in the browser application because their IDs were truncated by the floating point precision issue.
Math.pow(2, 53) == Math.pow(2, 53) + 1
This is a bit clearer I think. The addition just barely overflows the 53 mantissa bits of the IEEE 754 double precision floating point number.By the way, JS has BigInts these days, which are supported by all major browsers: https://caniuse.com/bigint
>> const x = 288230376151711740n;
>> x == x + 1n
false 2**53 == 2**53+1
^^ even more clear.That's why Number.isSafeInteger(a) exists.
Integers up to +/- 9007199254740991 (Number.MAX_SAFE_INTEGER) are fine.
Although, C does have an equivalent to that: the GNU MP library (and probably others as well).
The x86 assembly uses fixed width immediates. CPU registers are a fixed width. For any code to compile and run, it needs to make decisions about how large stack frames need to be, and how much heap memory to allocate.
This was the parent's point about abstractions. You can make a library that pretends to be a variable sized integer, but to implement such a library you need to make a decision about how much space to allocate and the width of variables in order to compile. There is no getting away from how the hardware works.
These can be implemented in any higher level language or within C without using fixed width numerals if correct abstractions were available. Only device control parts should use fixed with numerals in my opinion.
I for one am tired of the chicken & egg issue of "CPUs don't support efficient overflow checking because nobody uses it, so it's slow" and "Overflow checking is slow because the CPU doesn't support it, so nobody uses it". For all the other good security work done in both software and hardware, much for things far more complex than this, this seems like an absolutely batshit insane oversight considering the cost/benefits for fixing this.
Would it be possible to just silently replace with arbitrary sized integer and not break any code like safeintadd?
Null pointers are also handled by the kernel, not the CPU. Its called a segmentation fault because you are trying to access a memory segment that the OS doesn't want you to.
A NULL pointer in most (but not all) C implementations is address 0. Normally this address is not in a valid (mapped) page.
Any access to a virtual page that's not mapped by the HW page tables results in a page-fault exception. e.g. on x86, #PF.
This invokes the OS's page-fault exception handler to resolve the situation.
You may not be able to turn enforcement on for all code immediately. There's even the rare bits of code that depend on current overflow behavior. (Due to our human brains and the fact that we can easily name these bits of code, making the cognitively available, people often grotesquely overestimate the amount of code that operates this way. I'm sure it's only a matter of how many zeros belong in the 0.001%.) But we need this support to be available for code to be turn on easily and cheaply.
But what really boggles my mind, again given all the security work we've done, is that the reaction to this remains a combination of silence and "we can't do that!", when it seems to me the reaction ought to be "well duh jerf we all know that already." I don't get this. I don't get this attitude at all. This is a huge source of errors, a good fraction of which are security bugs, and nobody seems to care. Incomprehensible. This is, arguably, the number one thing that could be getting changed right now to fix security issues, and I just get slack-jawed "whaaaa?" in response to the idea.
One hundred plus one hundred is not negative fifty six! Here we are trying to hold together megabytes upon megabytes of security-critical code in a world where 100 + 100 = -56. Is it any wonder that writing any sort of code to maintain security invariants is tough in an environment where 100 + 100 = -56?