Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert common syscalls to safe I/O types #1863

Closed
wants to merge 3 commits into from

Conversation

SUPERCILEX
Copy link
Contributor

Works towards #1750. Fixes #1850.

I went the lazy route and pulled the plug... trying to feature gate everything seemed way too complicated.

@asomers
Copy link
Member

asomers commented Dec 3, 2022

There's a lot of stuff here. Could you please annotate #1750 to say which modules you're converting?

@SUPERCILEX SUPERCILEX mentioned this pull request Dec 4, 2022
29 tasks
src/unistd.rs Outdated Show resolved Hide resolved
SUPERCILEX added a commit to SUPERCILEX/nix that referenced this pull request Dec 4, 2022
SUPERCILEX added a commit to SUPERCILEX/nix that referenced this pull request Dec 4, 2022
bors bot added a commit that referenced this pull request Dec 4, 2022
1908: Move some pure formatting changes out of #1863 r=asomers a=SUPERCILEX



Co-authored-by: Alex Saveau <[email protected]>
@SUPERCILEX SUPERCILEX force-pushed the io_safety branch 13 times, most recently from 88b6ba9 to 760ca08 Compare December 5, 2022 00:17
@SUPERCILEX
Copy link
Contributor Author

Wow that was painful. 😅 I think this is finally ready for review.

Copy link
Member

@asomers asomers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't reviewed the tests yet. But the src changes look mostly good. The one big problem I see is that you removed the Option from all of the *at functions. I don't think we want to do that.

src/dir.rs Show resolved Hide resolved
src/fcntl.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
src/pty.rs Outdated Show resolved Hide resolved
src/unistd.rs Outdated
@@ -438,37 +452,43 @@ pub fn dup(oldfd: RawFd) -> Result<RawFd> {
/// specified fd instead of allocating a new one. See the man pages for more
/// detail on the exact behavior of this function.
#[inline]
pub fn dup2(oldfd: RawFd, newfd: RawFd) -> Result<RawFd> {
let res = unsafe { libc::dup2(oldfd, newfd) };
pub fn dup2<Fd: AsFd>(oldfd: &Fd, newfd: &mut OwnedFd) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever, taking OwnedFd by borrow. But what if the user wants to supply an integer that doesn't correspond to any currently open file? That was possible with the old interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang, you're right. Will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out how to let you both pass in an fd by reference and by value, so I ended up adding dup*_raw variants.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I ended up adding dup*_raw variants.

This is something unfortunate, but I think this unfortunate approach is the only way to do it, rustix does not provide something similar to unsafe fn dup2_raw(), so one cannot specify a fd that is still not open.

And interestingly, rustix provides 3 helper functions to use dup2 with Stdin, Stdout and Stderr: https://docs.rs/rustix/latest/rustix/stdio/fn.dup2_stdin.html

src/unistd.rs Show resolved Hide resolved
src/unistd.rs Outdated Show resolved Hide resolved
src/unistd.rs Show resolved Hide resolved
@SUPERCILEX SUPERCILEX force-pushed the io_safety branch 5 times, most recently from a56ddf2 to 3a11e20 Compare December 8, 2022 06:44
@SUPERCILEX SUPERCILEX force-pushed the io_safety branch 3 times, most recently from 676185c to 4da7e7f Compare December 8, 2022 18:18
@SUPERCILEX
Copy link
Contributor Author

Also at @asomers any chance this PR can be prioritized over the other I/O safety ones? Otherwise I'm going to be stuck constantly resolving merge conflicts.

Copy link
Member

@asomers asomers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another possibility for the *at functions. The cap-std crate treats them as methods on an already-created file descriptor object, like this:

let etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
let passwd = etc.open("passwd")?;

I think that's better than any of the proposals here. The only problems with cap-std are that it's too big. It pulls in too many dependencies, and it tries to provide a platform-independent abstraction layer. Should we do something similar?

src/lib.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
bors bot added a commit that referenced this pull request Dec 9, 2022
1913: feat: I/O safety for 'sys/inotify' r=asomers a=SteveLauC

#### What this PR does:

1. Changes the `fd` field of `struct Inotify` from `RawFd` to `OwnedFd`
2. Changes the interfaces of functions in the `impl Inotify {}`
   
    > The type of `self` changes from `Self` to `&mut Self`. 
    
    From:

   ```rust
   pub fn add_watch<P: ?Sized + NixPath>(
         self,
         path: &P,
         mask: AddWatchFlags,
   ) -> Result<WatchDescriptor> 

   pub fn rm_watch(self, wd: WatchDescriptor) -> Result<()>

   pub fn read_events(self) -> Result<Vec<InotifyEvent>>
   ```

   To:

   ```rust
   pub fn add_watch<P: ?Sized + NixPath>(
        &mut self,
        path: &P,
        mask: AddWatchFlags,
    ) -> Result<WatchDescriptor>

   pub fn rm_watch(&mut self, wd: WatchDescriptor) -> Result<()>

   pub fn read_events(&mut self) -> Result<Vec<InotifyEvent>>
   ```
  

   In the previous implementation, these functions can take `self` by value as `struct Inotify` [was `Copy`](https://docs.rs/nix/latest/nix/sys/inotify/struct.Inotify.html#impl-Copy-for-Inotify). With the changes in `1` applied, `struct Inotify` is no longer `Copy`, so we have to take `self` by reference.

-------

Blocks until the merge of #1863 as this PR needs `read(2)` to be I/O-safe.


1926: feat: I/O safety for 'sys/sendfile' r=asomers a=SteveLauC

#### What this PR does:
1. Adds I/O safety for module `sys/sendfile`.

1927: feat: I/O safety for 'sys/statvfs' r=asomers a=SteveLauC

#### What this PR does:
1. Adds I/O safety for module `sys/statvfs`.

1931: feat: I/O safety for 'sys/uid' & 'sched' r=asomers a=SteveLauC

#### What this PR does:
Adds I/O safety for modules:

1. `sys/uio`
2. `sched`

1933: feat: I/O safety for 'sys/timerfd' r=asomers a=SteveLauC

#### What this PR does:
1. Adds I/O safety  for module `sys/timerfd`.

Co-authored-by: Steve Lau <[email protected]>
SUPERCILEX added a commit to SUPERCILEX/nix that referenced this pull request Dec 9, 2022
@SUPERCILEX SUPERCILEX force-pushed the io_safety branch 2 times, most recently from fa23312 to db93263 Compare December 10, 2022 04:50
@SUPERCILEX
Copy link
Contributor Author

There's another possibility for the *at functions. The cap-std crate treats them as methods on an already-created file descriptor object

I had considered this a while back and think the conclusion was that I didn't like it because it'd pollute the i32 namespace. Now it would pollute AsFds (which is definitely better than all i32s, but still). Over at rustix, we're talking about potentially having a DirFd type to also prevent using FDs opened for files instead of directories, but I need to do some more research. That would combine nicely with extension methods.

Not sure where that leaves this PR... maybe we can keep AsDirFd and punt the discussion for now or I could try pulling the *at changes out of this PR?

bors bot added a commit that referenced this pull request Dec 10, 2022
1935: Formatting only changes for #1928 and #1863 r=asomers a=SUPERCILEX



Co-authored-by: Alex Saveau <[email protected]>
@asomers
Copy link
Member

asomers commented Dec 10, 2022

There's another possibility for the *at functions. The cap-std crate treats them as methods on an already-created file descriptor object

I had considered this a while back and think the conclusion was that I didn't like it because it'd pollute the i32 namespace. Now it would pollute AsFds (which is definitely better than all i32s, but still). Over at rustix, we're talking about potentially having a DirFd type to also prevent using FDs opened for files instead of directories, but I need to do some more research. That would combine nicely with extension methods.

Not sure where that leaves this PR... maybe we can keep AsDirFd and punt the discussion for now or I could try pulling the *at changes out of this PR?

Yeah, I wasn't thinking about adding a method to AsFd, but about adding a newtype for open directories. However, that would be suboptimal on Illumos, where you can use openat on a regular file to edit extended attributes. I see that you're a Rustix contributor too. So I've got to ask, why use both Nix and Rustix? I've generally thought of them as providing equivalent functionality. And also, why bother with both?

@SUPERCILEX
Copy link
Contributor Author

Yeah, I wasn't thinking about adding a method to AsFd, but about adding a newtype for open directories.

Gotya, cool. I'm planning on playing around with that tomorrow. Regarding Illumos, I'd prefer doing the nice thing for everybody else and adding a cfg exception for them.

So I've got to ask, why use both Nix and Rustix?

A month ago, I just used Nix, but over the past few weeks, I've fully migrated over to rustix for the perf improvements of raw syscalls. So why stick around? Originally, it was because I had open PRs here and it felt wrong to close everything and be like, "So long, suckers!" 😆 Anyway, now I'm sticking around because between you and Dan we're generating some interesting ideas that I don't think would have come about without the cross pollination.

@notgull
Copy link
Contributor

notgull commented Dec 11, 2022

This smells like it should be a newtype to me. Make it implement AsFd or From and it should work.

}

#[inline]
pub fn dup2_raw<Fd1: AsFd, Fd2: AsRawFd + IntoRawFd>(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this operates on a RawFd it should be unsafe. Otherwise it could be passed a fd corresponding to a file open elsewhere, and break soundness assumptions in the code using it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's why it requires IntoRawFd. If the call fails, you had to pass in your FD by value which means it gets dropped inside dup2_raw. If it succeeds, we give you back the same fd. Of course if you don't want the safety stuff, then you pass in a RawFd and IntoRawFd will do nothing.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the current Rust version RawFd implements AsRawFd and IntoRawFd, so this API can easily be used with an arbitrary integer. So you could for instance call close(dup2_raw(some_fd, 0).unwrap()). Allowing safe code to close stdin or any other fd. This shouldn't be possible in safe code without an OwnedFd.

This function is only safe if the caller either owns fd2, or knows the fd isn't used.

So I think the function should be an unsafe fn, with a doc comment describing when it is safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? When to require unsafe is unclear IMO. BTW, your example can just be close(0)—no need for dup. I think our goal should be to not break BorrowedFd or OwnedFd. If you want to do weird stuff with RawFd, then I feel like that's up to you.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://rust-lang.github.io/rfcs/3128-io-safety.html

Functions accepting arbitrary raw I/O handle values (RawFd, RawHandle, or RawSocket) should be unsafe if they can lead to any I/O being performed on those handles through safe APIs.

Basically any function that can operate on a RawFd should be unsafe. So to conform to IO safety, close should either be unsafe, or it should take a OwnedFd and consume it.

This is definitely awkward for a crate like nix though, since conforming to this necessitates breaking changes to basically all of the API. Perhaps these functions in nix should have been unsafe already, but ideally Rust would have had clear rules about IO safety before 1.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely awkward for a crate like nix though

Yeah, I think that's the crux of the matter. Nix is trying to be more low-level than the stdlib, so requiring unsafe is probably going to be annoying. I also think you can argue that we're following the definition. It says "can lead to" which to me means that if you store the RawFd somewhere and then use it later (like in a BorrowedFd or File), the function that does the storing should be marked unsafe, but if you immediately use the RawFd then you're good. Otherwise anytime a RawFd is even mentioned you'd need unsafe which doesn't make sense to me.

All of that said, I wouldn't be opposed to marking close as unsafe. Interestingly enough, dup is perfectly safe according to "Rust code has I/O safety if it's not possible for that code to cause invalid I/O operations" because anything you throw at dup will work (if the fd doesn't exist, it'll be allocated, if it does exist, it'll be closed).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the breaking API changes, and interop with other things using RawFd, it seems like it should be fairly ergonomic to work with a safe API that uses OwnedFd/BorrowedFd/AsFd and mostly doesn't expose RawFd anywhere in the API (dup2_raw is a special case here).

Interestingly enough, dup is perfectly safe according to "Rust code has I/O safety if it's not possible for that code to cause invalid I/O operations"Interestingly enough, dup is perfectly safe according to "Rust code has I/O safety if it's not possible for that code to cause invalid I/O operations"

This is subtle, but a safe dup taking a RawFd and returning an OwnedFd could be used to trigger memory unsafety with the sort of things envisioned in the IO safety RFC. In particular:

And in specialized circumstances, violating I/O safety could even lead to violating memory safety. For example, in theory it should be possible to make a safe wrapper around an mmap of a file descriptor created by Linux's memfd_create system call and pass &[u8]s to safe Rust, since it's an anonymous open file which other processes wouldn't be able to access. However, without I/O safety, and without permanently sealing the file, other code in the program could accidentally call write or ftruncate on the file descriptor, breaking the memory-safety invariants of &[u8].

So safe code shouldn't be able to write or truncate an arbitrary fd, which could break immutability assumptions of something mmapping that fd. dup is also a problem, because safe code could write or truncate given an OwnedFd, which will have the same issue as write or truncate to the original fd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but then everything is unsafe because you can just open /proc/self/mem. I think we have to take a pragmatic approach here and making dup unsafe because someone might be messing around with mmap is overkill.

@SUPERCILEX
Copy link
Contributor Author

@asomers see bytecodealliance/rustix#478 (comment). I want to try your idea, but not in this PR.

Regarding this PR: please let me know what needs to change for it to be merged (I'll resolve conflicts then). I think AsDirFd should be left in, though I'd be willing to temporarily rip it out. I can also drop the dup commit, but I think as long as the commits are merged as is this PR is fine and we can just do fixups before release if necessary.

Regarding AsDirFd, I'll make a new PR after this one is merged that replaces it with the idea discussed here and in rustix. Regarding making openat an extension method, I think that's a bad idea: there's no symmetry to be found. If we make openat an extension method, why stop there? Open can be an extension on paths. Close can be an extension on FDs. But then what do you do about methods that have multiple paths/fds or mix the two? The whole thing becomes a mess, so let's just keep it simple and use top-level functions.

@SteveLauC SteveLauC mentioned this pull request Oct 4, 2023
3 tasks
copybara-service bot pushed a commit to chromeos/adhd that referenced this pull request Jun 5, 2024
This gives us file descriptor types.
nix-rust/nix#1863

BUG=b:233174528
TEST=CQ

Change-Id: Ida81204a41d9e2f8b81afa3d0eb729e81a853f95
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/adhd/+/5599982
Tested-by: [email protected] <[email protected]>
Commit-Queue: Li-Yu Yu <[email protected]>
Reviewed-by: Chih-Yang Hsia <[email protected]>
@SteveLauC SteveLauC modified the milestones: 0.27.0, 0.30.0 Jun 9, 2024
@SteveLauC
Copy link
Member

Close as this has been superseded by other I/O safety PRs: https://github.com/nix-rust/nix/tree/d60f710c31394b3ee7af2a1be0f37a713a9565bd/changelog

@SteveLauC SteveLauC closed this Jun 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Change the interface of openat(dirfd: RawFd, ...) to openat(dirfd: Option<RawFd>, ...)
5 participants