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

FreeBSD RESOLVE_BENEATH support #296

Merged
merged 4 commits into from
Dec 28, 2023

Conversation

valpackett
Copy link
Contributor

wheeeee! Overall the code is pretty simple but rustix::fs is getting a bit messier.

  • the support check I've implemented simply by probing AT_RESOLVE_BENEATH support because it seems the simplest, I don't want to copy version parsing code from the Android thing…
    • we technically could also support older FreeBSD versions in capability mode by checking that, even if AT_RESOLVE_BENEATH is not a thing, openat(fd, "/") results in ENOTCAPABLE, but this would also require making the AT_RESOLVE_BENEATH flag conditional on the result, the detection state would be like an atomic enum Unknown | FullSupport | OldSandboxed | NoSupport and aghhhh old versions aren't worth it at all
  • linkat could be supported too, but it only sandboxes fd1, i.e. linkat(fd1, "../one", fd2, "two") is rejected, but linkat(fd1, "one", fd2, "../two") just does it, so via_parent::open_parent would need to be used on the target, and that's currently pub(super) along with the MaybeOwnedFd type and everything :/
    • that escape wasn't getting caught by the tests when I tried to just use an implementation without doing that! I hope the test suite don't miss other possible escapes
  • yeah, openat(openat(AT_FDCWD, "/"), "..") does not return an error because we're already at the root and going .. doesn't actually escape anything, we're still at the root, haha

Waiting for: bytecodealliance/rustix#541

@sunfishcode
Copy link
Member

wheeeee! Overall the code is pretty simple but rustix::fs is getting a bit messier.

This is cool, thanks for working on this!

* the support check I've implemented simply by probing `AT_RESOLVE_BENEATH` support because it seems the simplest, I don't want to copy version parsing code from the Android thing…

Entirely reasonable.

* `linkat` could be supported too, but it only sandboxes `fd1`, i.e. `linkat(fd1, "../one", fd2, "two")` is rejected, but `linkat(fd1, "one", fd2, "../two")` just does it, so `via_parent::open_parent` would need to be used on the target, and that's currently `pub(super)` along with the `MaybeOwnedFd` type and everything :/
  * that escape wasn't getting caught by the tests when I tried to just use an implementation without doing that! I hope the test suite don't miss other possible escapes

Good catch; we should add more tests here.

* yeah, `openat(openat(AT_FDCWD, "/"), "..")` does not return an error because we're already at the root and going `..` doesn't actually escape anything, we're still at the root, haha

O_RESOLVE_BENEATH is a new feature in FreeBSD; I wonder if it would be possible to report this as a bug and get this fixed. In any RESOLVE_BENEATH use case I can think of, the caller of open shouldn't know
, for compatibility with Linux RESOVLVE_BENEATH, which does fail on "..".

Waiting for: bytecodealliance/rustix#541

I'm behind on my PR reviews at the moment, but I'll get to it!

@sunfishcode
Copy link
Member

I filed https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=269780 to see if FreeBSD would change the behavior of .. in /.

@valpackett valpackett marked this pull request as ready for review March 30, 2023 02:59
@sunfishcode
Copy link
Member

FreeBSD maintainers have not responded in a while in https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=269780, and in any case, the current release behavior of RESOLVE_BENEATH succeeds on /... My inclination is that cap-std should not pass this behavior on to its users, and that we can implement this by adding a #[cfg(target_os = "freebsd")] boolean field to Dir which records whether the directory could be root, and if it could be, we should use manually::open instead of open with RESOLVE_BENEATH. I can implement this is you'd like.

@valpackett
Copy link
Contributor Author

Hmm, I'm not sure that rather harmless and uncommon edge case is worth the effort tbh, I'm curious though, how would that tracking work?

@sunfishcode
Copy link
Member

I'm a little torn here. I agree that the /.. issue is pretty obscure. At the same time though, the concepts of "the root filesystem" and preventing applications from being aware of the configuration of the filesystem namespace outside of what they've been granted are fundamental to what cap-std is all about.

Thinking about this more though, adding a flag to Dir would probably get pretty cumbersome, especially with the current cap-std vs cap-primitives split. So what about the following:

In the beneath_supported function, before checking WORKING, try an open with start and ".." and O_RESOLVE_BENEATH, and if that succeeds (or maybe also if it fails with an error other than ENOTCAPABLE), then return false.

We could do something more complex if we care about the possibility of a chroot racing with this code from another thread, but chroot requires an effective uid of 0, and the idea of a multi-threaded program doing a chroot on one thread to a directory that another thread is doing cap-std calls on while also not tripping FreeBSD's kern.chroot_allow_open_directories check is extraordinarily obscure, so I'd be ok ignoring that for now.

That's admittedly not free; it is an extra system call. With a little cleverness, we could at least avoid doing it in the case where the provided path has exactly one component (unless it turns out to be a symlink), which is a pretty common case.

@valpackett
Copy link
Contributor Author

valpackett commented Apr 11, 2023

chroot requires an effective uid of 0

Ha—security.bsd.unprivileged_chroot might become enabled by default in the future :) But yes, I'm sure the allow-open-directories check completely disallows this kind of behavior. Races shouldn't be possible at all, as the chroot() and the VFS lookup used by openat() synchronize by exclusively locking the struct pwd belonging to the whole process—either the directory will be opened first and the chroot will be denied, or chroot will happen first and the directory will be searched inside of the chroot.


Waaaaait.

*facepalm*

What are we even talking about. The flag for checking whether the Dir is the root is trivial, isn't it? We can only acquire a Dir of / by requesting that from ambient authority; preventing any other way of getting that is kind of the whole point. I'll implement that :) Riiight, it's more the whole "adding a flag to Dir would probably get pretty cumbersome" thing

@valpackett
Copy link
Contributor Author

The change of the /.. behavior has landed in -STABLE and will be available in the next 13.x 🎉

@valpackett
Copy link
Contributor Author

Rebased. Had to fix a couple unrelated CI errors…

Copy link
Member

@sunfishcode sunfishcode left a comment

Choose a reason for hiding this comment

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

Cool, thanks for working on this! Overall this looks good.

Could you add a test for the openat(openat(AT_FDCWD, "/"), "..") behaior?

@@ -13,16 +13,7 @@ pub(in super::super) fn compute_oflags(options: &OpenOptions) -> io::Result<OFla
oflags |= OFlags::SYNC;
}
if options.dsync {
#[cfg(not(target_os = "freebsd"))]
Copy link
Member

Choose a reason for hiding this comment

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

What does FreeBSD 12 do with the DSYNC flag? Would it useful to continue to set SYNC on FreeBSD <= 12?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would ignore it, and… well, useful but probably not worth trying to version-detect or anything for a release branch that should become EoL by the end of this year? (Or, this can wait, really not an important part of the PR for me)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, it's an admittedly minor detail, but would you mind splitting this out into a separate PR?

tests/fs.rs Outdated
let metadata0 = check!(tmpdir.metadata(filename));
assert_eq!(mask & metadata0.permissions().mode(), 0);
Copy link
Member

Choose a reason for hiding this comment

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

This test is ported from a corresponding test in Rust's standard library, so ideally we should leave the test as-is and change cap-std to pass it. Can you say more about what this change is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned in the commit message this was failing with EPERM on FreeBSD… at some point for me, anyway, but it seems to pass for me now. Maybe it was due to me screwing around with my in-development kernel patches, oops

Copy link
Member

Choose a reason for hiding this comment

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

If the change isn't needed, would you mind reverting 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.

Yeah, of course, I only haven't pushed the removal due to waiting on the DSYNC decision :)

@valpackett
Copy link
Contributor Author

Could you add a test for the openat(openat(AT_FDCWD, "/"), "..") behaior?

Well, that would currently fail the CI upon reenabling cirrus, as the changed behavior hasn't made it into a release yet.

@valpackett
Copy link
Contributor Author

cleaned up now!

Copy link
Member

@sunfishcode sunfishcode left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for working on this!

@sunfishcode
Copy link
Member

Sorry for being slow here! I've now rebased this on #338 which fixes the CI failure.

@sunfishcode sunfishcode merged commit 5e32356 into bytecodealliance:main Dec 28, 2023
22 checks passed
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.

2 participants