Hacker News new | past | comments | ask | show | jobs | submit login
Spawn your shell like it's the 90s again (akat1.pl)
141 points by mulander on July 22, 2016 | hide | past | favorite | 43 comments



The posix file system API invites these race conditions. but when I want to access the fs from my app, it often feels like the only choice I have is to just accept it.

What can I do to change it? Is there a reliable, widespread and usable successor to the posix API that offers transactions, or "compare and swap"-type operations? Or are we forever caught in this catch 22 between OS writers and application programmers ?

I wouldn't mind an API that can be (unsafely) simulated on regular posix for compatibility, while being forward compatible with the next gen of file IO on systems that do support it, for example.


> that offers transactions, or "compare and swap"-type operations?

"Things UNIX can do atomically" https://rcrowley.org/2010/01/06/things-unix-can-do-atomicall...


This depends entirely on what you want to do. You can open files with O_EXCL. You can atomically rename() them. You can use libsqlite3 which provides an abstraction for many operations.


in this situation you can do a stat on the file descriptor after you have opened the file to ensure that it is not a symlink. some of the races with the filesystem can be fixed using the file descriptor family of functions instead of the path family of functions.


How do you get a fd for the link and not the linked file?


There is the non-standard O_NOFOLLOW.


Why do you say "non-standard"? O_NOFOLLOW is specified by POSIX.


The Linux manual page is a little misleading, and I didn't dig further into POSIX. Mea culpa.

       O_NOFOLLOW
              If pathname is a symbolic link, then the open fails.  This is a FreeBSD extension, which was added  to  Linux  in  version
              2.1.126.  Symbolic links in earlier components of the pathname will still be followed.  See also O_PATH below.
...

       The O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW flags are not specified in POSIX.1-2001, but are  specified  in  POSIX.1-2008.


Can you not do an ordinary open without O_NOFOLLOW and then lstat the filename?


Race condition again. The file could be unlinked. fstat is the only safe way of getting stat information for an open file.


Right. The right thing to do is open() with NOFOLLOW, then fstat the resulting fd. Then check for S_ISLNK(st_mode).


> Is there a reliable, widespread and usable successor to the posix API that offers transactions, or "compare and swap"-type operations?

https://msdn.microsoft.com/en-us/library/windows/desktop/aa3...


the problem is, you can't really avoid this kind of race condition. Not in a pre-emptive system. You can't stat a file and open it at the same time: the whole point of the stat is to see if you SHOULD open it. The best you could do is maybe stat the file after you open it. But I'm not sure what other problems that would cause.


There are many possible solutions:

Obtain a "session ID" through stat and always use that to access the file:

  id := stat("/a")
  read(id)
  write(id, ..) // doesn't protect race access to a file,
                // but at least you're not using the
                // pathname twice, which already solves a
                // lot of problems
Use transactions:

  transid := ftransopen()
  f := stat("/a")
  read("/a")
  write("/a", ..)
  ftransend(transid) // fails if clashes with other fs changes
Have "last modification ID" counters that change for every modification to a file, and allow CAS-style operations:

  mod := stat_mod("/a")
  open_mod("/a", mod)
  read_mod("/a", mod)
  mod2 := write_mod("/a", ..., mod) // fails if /a modified since mod
  mod3 := chown_mod("/a", ..., mod2) // fails if modified since mod2
etc.

All with their own pros and cons, of course, but definitely possible. I'm just curious if there's a specific implementation that's gaining momentum.


okay. neat.

why did you use Go syntax?


you call fstat on the opened file descriptor and compare device IDs and inodes, as was done in the referenced OpenBSD patch from -96: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/libexec/mail.lo...

EDIT: corrected year


What's so strange about stat() on the file after you open it? It's a security check against a known and abused exploit and it's not even hard to do. This seems like a simple fix to the problem.

Just make sure you fstat() the file descriptor and not stat() the file to avoid a second race condition where a malicious actor undoes their attack immediately after you open() the file to hide the evidence.


fstat is a more recent system call than stat, so old code bases do not use it, and mail.local is quite ancient.

Although a quick lookup tells me that it appeared in 4.3BSD-Tahoe (June 1988) and SysVR4 (October 1988), so one would have expected all reasonable distributions to have gotten with the program by now.


A little digging suggests that mail.local appeared in Version 7 Unix from 1979, so it is not a surprise that it doesn't include a syscall invented 9 years later.

Still, that syscall is 28 years old now. It's kind of embarrassing that nobody has gone though and checked for ancient and obvious privilege escalation issues like this. Or I guess they have, but on different OSes. This is one big downside to fragmentation, getting fixes distributed to all of the fragments.


fstat() isn't the complete fix here - it doesn't protect you against opening/creating an unintended file through a symlink, for which you need O_NOFOLLOW (which is a bit more recent).


I had no idea if it was common or not, which is why I said I was unsure.


I could think of multiple ways of fixing this problem changing the API. For instance they could have an api that opens the file only if it is not a synlink.


Like open(O_NOFOLLOW)?


Author is correct, this is an old bug. I can confirm it was at least present in SunOS 4.1.3, because I worked for an ISP back in the 90s that offered shell accounts and we found evidence of attempts all over the place (when you "lose" the race a root-owned file is created, so unless you ever "win" and clean up after yourself there's evidence of your attempt).


Why Korn shell? Is there something special about the way it handles being run w/ a setuid bit?


It's one of the shells in the NetBSD base system. bash, for example, is not guaranteed to be installed.


Bash drops privileges by default.


Waiting for someone to say this wouldn't be possible in rust.

Spoiler alert: it would be.


It wouldn't be possible if the filesystem were not recklessly-shared (essentially global) mutable state, and the danger of shared mutable state is one of the high-level lessons that learning Rust will teach you.


I don't think anyone's claiming you can't use your system's API in Rust, to do things like gain root privileges. Just that Rust makes memory bugs and race conditions near impossible.


The vulnerability described in the C program (race condition in filesystem which leads an attacker to write to a file they should have no access to) is absolutely possible to write in rust.

I was attempting to parody the line that you see around here every time a vulernable C program is shown, that rust is magic pixie dust that removes all security issues. (This is exactly the type of bug that memory safe languages are still vulnerable to.)

(One could argue setuid is the real problem though.)


Yes safe systems programming languages are still open to logic errors, but getting rid of the memory corruption bugs that C spread into the world thanks to the adoption of UNIX is already an improvement over the daily CVE entries.

Had AT&T charged UNIX at the same prices as the other OSes, instead of giving it for free to universities and I hardly believe C would matter.


It's likely C, or something like it, would still matter. C, despite, and because, of its faults, fills an important niche: It's at a higher level of abstraction than assembler, but still provides the control you need for that very low-level code that you need to write things like operating systems. Yes, it isn't as safe as the Wirthian languages that you yourself prefer, and I will say that it certainly isn't ideal for most of the purposes it's put to, but if you want a high-level assembly language, than it works well, because that's pretty much what it is.


C has stopped being a high level Assembly for quite a while now.

Most of the features people associate with C are compiler specific language extensions that any compiler writer can bother to add to his favourite language and not part of ANSI C.

Inline Assembly, CPU intrisics, SIMD, calling conventions, out of order execution, hardware transaction memory, cache line control, GPU execution, attaching functions to interrupt handlers and so on.

Also the pile of UB patterns that C compilers have accumulated throughout the years, and the differences between compilers, makes it actually easier to reason about code at the Assembly level than looking at C code.

The only thing has going for it, us that thanks to the economics of free, we now have UNIX flavours everywhere.

So we can only get rid of C when UNIX is not an option, and that is almost impossible.

Only now almost 30 years later has C++ surpassed C in many use cases, but it still carries C with it.

C could have been made so much safer with a few tiny changes, like having optional bounds checking and not decaying arrays into pointers.


>C has stopped being a high level Assembly for quite a while now.

...No, not especially. It may not expose CPU-specific functionality in the spec, which is why it is somewhat portable, but it still is effectively good at the same things asm is good at. That's why C is sometimes dubbed "portable assembly"

>Also the pile of UB patterns that C compilers have accumulated throughout the years, and the differences between compilers, makes it actually easier to reason about code at the Assembly level than looking at C code.

Well, yeah. Asm is rigorously defined. It's perhaps the only language that has absolutely zero UB in practice (many languages CLAIM to have no UB, but if you're on multiple architectures, it's almost unavoidable, although few have as much as C): because it's totally unportable, it doesn't have to lean on UB the way C does for optimization.

>Only now almost 30 years later has C++ surpassed C in many use cases, but it still carries C with it.

Yeah, but C++ has... other problems.

>C could have been made so much safer with a few tiny changes, like having optional bounds checking and not decaying arrays into pointers.

Now that's something I wish happened.


Rust isn't "magic pixie dust" to remove all security issues. It's just a massive improvement over what we already have -- look at how many security vulnerabilities in web browsers (which largely use Modern C++ techniques) are due to Use-after-Free bugs. Just because it doesn't prevent all bugs doesn't mean it's worthless.


If someone has the mindset that "the programming language will make it so I don't have to think about bugs" then it is guaranteed that you will have more bugs.

If you want good code, you need to think, in every language.


Bingo.

And I would go further and say: the people who prefer to leave details to others are exactly the people who will write filesystem race conditions like this bug and not understand what they have done.


You need to worry about less things in Rust.


Rust doesn't prevent race conditions–it prevents data races.

https://doc.rust-lang.org/nomicon/races.html


True. The indeterminism provided by letting the OS and cache coherence reorder thread execution and memory accesses in response to external factors is one of the reasons parallel execution can provide speedups in practice, so benign races are important to permit.

But this is a data race: the mail program performs a check on some data (in the filesystem) to establish some precondition, and then a concurrent thread of execution (another process) mutates that data before the mail program acts on its (now invalidated) belief.

LLVM wouldn't call it a data race, because to LLVM all system calls are essentially opaque. But if the filesystem only existed within your process, and were written in idiomatic Rust style, using structs, borrowing, mutability, and lifetimes, then the analogous bug (two threads racing and mutating the filesystem at once) would be prevented. The real villain here is shared mutable state; if UNIX had been written by skilled Rust progammers, it would with any luck have some abstraction that provides more isolation and less potential for interference than the filesystem.


> if UNIX had been written by skilled Rust progammers, it would with any luck have some abstraction that provides more isolation and less potential for interference than the filesystem.

I think this is kind of absurd and my guess is you haven't worked on a filesystem driver. At a certain point there is value in admitting that race conditions are a part of the universe and developing strategies to deal with them, rather than waste a bunch of overhead trying to isolate a program from the real world or from itself. The semantics you seem to want would be really crazy for a filesystem.


  > on some data (in the filesystem) 
Traditionally, "data race" only refers to memory, not other forms of resources. So I agree with you that this is like a data race, but would dispute that it's actually a data race.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: