-
Notifications
You must be signed in to change notification settings - Fork 364
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
First steps towards fixing the symlink CVEs #1919
First steps towards fixing the symlink CVEs #1919
Conversation
It should be noted (probably in the commit message too) that as these symlink CVE's overlap and interact in various ways, this does not fully fix CVE-2021-35939 as the directory tracking does not cover all our installation steps yet. Plugging all the holes requires converting all of FSM to the *at() family of calls plus fd-based ops where possible, so this really is just the first step of many to come. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Linux, using openat2()
will be much simpler and more efficient on kernels that support it. RPM is not Linux-specific, but openat2()
might be useful where available.
I'm quite aware of Linux having all manner of fancy extensions available. |
da3507b
to
008cbca
Compare
This is now using fd or dirfd+basename for file ops within the fsm, as much as possible. Plugins pose special problems as external libraries generally dont support dirfd+basename style operation, but may still need to operate on symlinks so we're stuck with "insecure" absolute paths there, for now at least. I'm seeing a couple of install glitches on fresh chroot install still, but it's getting close now. (edit: hmph, the test-suite was passing just a minute ago...) |
Internal only for now in case we need to fiddle with the API some more, but no reason this couldn't be made public later.
Whenever directory changes during unpacking, walk the entire tree from starting from / and validate any symlinks crossed, fail the install on invalid links. This is the first of step of many towards securing our file operations against local tamperers and besides plugging that one CVE, paves the way for the next step by adding the necessary directory fd tracking. This also bumps the rpm OS requirements to a whole new level by requiring the *at() family of calls from POSIX-1.2008. This necessarily does a whole lot of huffing and puffing we previously did not do. It should be possible to cache secure (ie root-owned) directory structures to avoid validating everything a million times but for now, just keeping things simple.
Any unowned directories will be created inline during processing now so we can just flush this big pile of code that was insecure anyhow. As an additional bonus creating the directories inline gives us an opportunity to track the creation so we can undo too, but that is not done here.
Handling this in a separate clause makes the logic much clearer and (in theory at least) lets us handle hardlinks to any content, not just regular files.
Fix the initial setmeta value to something meaningful: we will never set metadata on skipped files, and hardlinks are handled with a special logic during install. They'd need different kind of special logic on FA_TOUCH so just play it safe and always apply metadata on those. Harlink metadata setting on install should happen on the *last* entry of hardlinked set that gets installed (wrt various skip scenarios) as otherwise creating those additional links affects the timestamp. Note in particular the "last file of..." case in fsmMkfile() where we the comment said just that, but set the metadata on the *first* file which would then be NULL'ed away. This all gets current masked by the fact that we do the metadata setting on a separate round, but that is about to change plus this makes the overall logic clearer anyhow.
Commit a82251b moved metadata setting to a separate step because there are potential benefits to doing so, but the current downsides are worse: as long as we operate in potentially untrusted directories, we'd need to somehow verify the content is what we initially laid down to avoid possible privilege escalation from non-root owned directories. This commit does not fix that vulnerability, only makes the window much smaller and paves the way for the real fix(es) without introducing a second round of directory tree validation chase to the picture.
Supposedly no functional changes here, we just need all these things converted before we can swap over to relative paths.
This isn't ideal from the sense that some files may get a success post call while something later can still fail, but things get even weirder with doing it in a separate round where things could fail because of a vanished directory and then we'd still need to call the plugin hook with some result. Also, this lets us skip the backwards walk on the normal case of success, which is nice.
It doesn't make much sense to call plugins for files that wont be unpacked at all, and in particular it wont make much sense to do the entire directory dance just to be able to pass meaningful path values to plugins. So from now we'll only be calling file-pre for things that we're about to lay down, which it how it used to be before splitting the stages anyhow.
All our renames are (for now at least) within a single directory so the second dirfd is kinda redundant, but shrug...
fsmUnpack() is the only place in FSM that needs to deal with rpmio FD types, everywhere else they are nothing but a hindrance that need to be converted to OS level descriptors for use. Better deal with OS level descriptors to begin with.
This will be needed for using fd-based metadata operations.
Notably cap_set_file() doesn't have a dirfd-based mode, to handle that safely we'll need to use fd-based operation. Which would be nicer anyhow but symlinks can't be opened so we'll have to carry the dirfd/path based mode forever more anyhow (yes Linux has extensions but that's another story).
We need to support both fd-based and (dirfd+) path based operations due to all the lovely mismatches in POSIX, so lotsa half-duplicated tedious stuff here. As of this commit, we only use fd based ops for regular files.
Regular file ops are fd-based already, for the rest we need to open them manually. Files with temporary suffix must never be followed, for directories (and pre-existing FA_TOUCHed files) use the rpm symlink "root or target owner allowed" rule wrt following. This mostly fixes CVE-2021-35938, but as we're not yet using dirfd-based operatiosn for everything there are corner cases left undone. And then there's the plugin API which needs updating for all this.
This is a special case in various places around rpm, worth having a test for.
Within fsm this is just a matter of adjusting error messages to include the directory... if it only wasn't for the plugins requiring absolute paths for outside users. For the plugins, we need to assemble absolute paths as needed, both in ensureDir() and plugin file slots.
Okay, test-suite + all my local tests (install to empty chroot etc) pass now 🥳 |
Cute (but non-portable) trick: use paths of the form |
Yeah once we have the basics working and optimized to a reasonable degree we can start looking at utilizing various OS-specific extensions. The gotcha with those is to find ways to provide extra functionality in the specific OS'es without introducing multiple codepaths (which will inevitably bitrot) to accomplish the same thing. |
Anyway... There will inevitably be bugs in this all, and since the test-suite covers only so much the best way to find the rest is real-world testing. And sitting in a branch does little to achieve that, so I'm merging this as is now. Danger Will Robinson, if you're in the habbit of running rpm daily snapshots then you'll want to stay alert for a while. |
Oh, and to make it absolutely clear: we're nowhere near done with this, I just want to get this bulk of change over with so we can concentrate with the finer nuances. |
@pmatilai Thanks for fixing these CVEs. And I want to double check with you that |
There will be no backports. |
Make sure fsmFsPath() always returns a relative path so that when we pass it to an *at() call, the dirfd argument is always respected (see e.g. fchownat(2) for details). Previously, we returned "/" for file info that CONTINUE HERE which eventually caused an installation to fail in fsmChown() Always pass a relative path to the *at() calls, even if we're processing the root directory itself, to prevent those calls from operating on the real "/" if the root At the same time, make sure to unlink the root directory if it's not the real root but a relocated prefix. Otherwise, if a package owns the "/" path, we would end up calling fchownat() on it and only accidentally succeed if we have write permissions for it (which regular users typically don't). This fixes the installation and removal of such packages for regular users (only applies to relocatable packages This makes relocatable packages installable by regular users again (this regressed in the symlink fixes in rpm-software-management#1919). Additionally, this also fixes the removal of
During payload processing, if we encounter a file entry corresponding to the prefix directory itself, instead of referring to it relative to its parent directory's file descriptor, we pass the absolute / path to the *at() calls which then makes them fall back to path-based operation. This isn't a huge problem as long as we're installing into the real root prefix, however if the package is built as relocatable and the prefix is overridden, we still operate on the real "/" instead of the new prefix. This is wrong and even causes an installation failure if the relocatable package is installed as a regular user who doesn't have write access for the real root. Fix this by simply passing "." instead of "/" to these *at() calls, to always ensure fd-based operation. However, this alone isn't sufficient since we can't delete the relocated prefix directory itself this way when removing the package. This is due to fact that unlinkat(2) with AT_REMOVEDIR actually performs a rmdir(2), and that fails with EINVAL if called with the "." path. Therefore, when removing a package and encountering the prefix directory itself, stop the traversal in ensureDir() one level higher than usual so that we refer to the prefix directory relative to its parent directory via normal fd-based operation. Lastly, don't even attempt to remove the prefix if it's *not* relocated. This is safer and also gets rid of a spurious warning that's printed when removing a non-relocatable package owning / as the root user. Such packages (that include the sole / in their %files section) perhaps aren't exactly common and those that exist aren't normally installed or uninstalled on a production system (such as the "filesystem" package on Fedora), however it's apparently used as a shorthand for including all files in a relocatable package, as evidenced by TBD. Either way, this is a regression introduced by the fsm rework addressing the symlink CVEs (see rpm-software-management#1919 for details). Fixes: TBD
During payload processing, if we encounter a file entry corresponding to the prefix directory itself, instead of referring to it relative to its parent directory's file descriptor, we pass the absolute / path to the *at() calls which then makes them fall back to path-based operation. This isn't a huge problem as long as we're installing into the real root prefix, however if the package is built as relocatable and the prefix is overridden, we still operate on the real "/" instead of the new prefix. This is wrong and even causes an installation failure if the relocatable package is installed as a regular user who doesn't have write access for the real root. Fix this by simply passing "." instead of "/" to these *at() calls, to always ensure fd-based operation. However, this alone isn't sufficient since we can't delete the relocated prefix directory itself this way when removing the package. This is due to the fact that unlinkat(2) with AT_REMOVEDIR actually does a rmdir(2), and that fails with EINVAL if called with the "." path. Therefore, when removing a package and encountering the prefix directory itself, stop the traversal in ensureDir() one level higher than usual so that we refer to the prefix directory relative to its parent directory via normal fd-based operation. Lastly, don't even attempt to remove the prefix if it's *not* relocated. This is safer and also gets rid of a spurious warning that's printed when removing a non-relocatable package owning / as the root user. Such packages (that include the sole / in their %files section) perhaps aren't exactly common and those that exist aren't normally installed or uninstalled on a production system (such as the "filesystem" package on Fedora), however it's apparently used as a shorthand for including all files in a relocatable package, as evidenced by TBD. Either way, this is a regression introduced by the fsm rework addressing the symlink CVEs (see rpm-software-management#1919 for details). Fixes: TBD
During payload processing, if we encounter a file entry corresponding to the prefix directory itself, instead of referring to it relative to its parent directory's file descriptor, we pass the absolute / path to the *at() calls which then makes them fall back to path-based operation. This isn't a huge problem as long as we're installing into the real root prefix, however if the package is built as relocatable and the prefix is overridden, we still operate on the real / instead of the new prefix. This is wrong and even causes an installation failure if the relocatable package is installed by a regular user who doesn't have write access for the real root. Fix by simply passing "." instead of "/" to these *at() calls, to always ensure fd-based operation. However, this alone isn't sufficient since we can't delete the relocated prefix directory itself along with the package this way. This is due to the fact that unlinkat(2) with AT_REMOVEDIR actually does a rmdir(2), and that fails with EINVAL if called with the "." path. Therefore, when removing a package and encountering the prefix directory itself, stop the traversal in ensureDir() one level higher than usual so that we refer to the prefix directory relative to its parent directory via normal fd-based operation. Lastly, don't even attempt to remove the prefix if it's *not* relocated. This is safer and also gets rid of the spurious warning that's printed when removing a non-relocatable package owning / as the root user. Such packages (that include the sole / in their %files section) perhaps aren't exactly common and those that exist aren't normally installed or uninstalled on a production system (such as the "filesystem" package on Fedora), however it's apparently used as a shorthand for including all files in a relocatable package, as evidenced by TBD. That, and the fact that this all used to work fine before the fsm rework (rpm-software-management#1919) addressing the symlink CVEs means a fix (one that also doesn't reintroduce the TOCTOU issue) is in order, whether pretty or not. Fixes: TBD
During payload processing, if we encounter a file entry corresponding to the prefix directory itself, instead of referring to it relative to its parent directory's file descriptor, we pass the absolute / path to the *at() calls which then makes them fall back to path-based operation. This isn't a huge problem as long as we're installing into the real root prefix, however if the package is built as relocatable and the prefix is overridden, we still operate on the real / instead of the new prefix. This is wrong and even causes an installation failure if the relocatable package is installed by a regular user who doesn't have write access for the real root. Fix by simply passing "." instead of "/" to these *at() calls, to always ensure fd-based operation. However, this alone isn't sufficient since we can't delete the relocated prefix directory itself along with the package this way. This is due to the fact that unlinkat(2) with AT_REMOVEDIR actually does a rmdir(2), and that fails with EINVAL if called with the "." path. Therefore, when removing a package and encountering the prefix directory itself, stop the traversal in ensureDir() one level higher than usual so that we refer to the prefix directory relative to its parent directory via normal fd-based operation. Lastly, don't even attempt to remove the prefix if it's *not* relocated. This is safer and also gets rid of the spurious warning that's printed when removing a non-relocatable package owning / as the root user. Such packages (that include the sole / in their %files section) perhaps aren't exactly common and those that exist aren't normally installed or uninstalled on a production system (such as the "filesystem" package on Fedora), however it's apparently used as a shorthand for including all files in a relocatable package, as evidenced by TBD. That, and the fact that this all used to work fine before the fsm rework (rpm-software-management#1919) addressing the symlink CVEs means a fix (one that also doesn't reintroduce the TOCTOU issue) is in order, whether pretty or not. Fixes: TBD
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Fixes: rpm-software-management#3173
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Fixes: #3173
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Fixes: rpm-software-management#3173 (cherry picked from commit 308ac60)
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Fixes: #3173 (cherry picked from commit 308ac60)
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Backported from commits: 31c14ba 308ac60 Tests are excluded from this backport since they would need significant rework, the use case will be covered by Beaker instead. Fixes: RHEL-49494
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Backported from commits: 31c14ba 308ac60 Tests are excluded from this backport since they would need significant rework, the use case will be covered by Beaker anyway. Fixes: RHEL-49494
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Backported from commits: 31c14ba 308ac60 Tests are excluded from this backport since they would need significant rework, the use case will be covered by Beaker. Fixes: RHEL-49494
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Backported from commits: 31c14ba 308ac60 Tests are excluded from this backport since they would need significant rework, the use case will be covered by Beaker. Fixes: RHEL-49494
When relocating the root directory, make sure we insert the new path's dirname to dirNames[] even if the root itself is owned by the package. This appears to have been the intention from the first version (largely untouched since) of this code as we allow the root to pass through the first checks (by setting len to 0 in that case) as well as the second for loop where we do the relocations. This allows fsm to properly create and remove the relocated directory since we're now using fd-based calls (rpm-software-management#1919) and the parent directory needs to be opened first. No need to do string comparison here, the empty basename signals that we're processing the root directory, so just use that. Building a relocatable package that owns the root directory seems to be a handy way to create user-installable packages (see RHEL-28967) and it happened to work before with the path-based calls so this technically was a regression. Add a test that emulates this use case. Backported from commits: 31c14ba 308ac60 Tests are excluded from this backport since they would need significant rework, the use case will be covered by Beaker. Fixes: RHEL-49494
Details in commits, but basically fixes CVE-2021-35939 and lays down some necessary infrastructure for next steps in securing down our file operations.