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

First steps towards fixing the symlink CVEs #1919

Merged
merged 32 commits into from
Feb 16, 2022

Conversation

pmatilai
Copy link
Member

Details in commits, but basically fixes CVE-2021-35939 and lays down some necessary infrastructure for next steps in securing down our file operations.

@pmatilai
Copy link
Member Author

pmatilai commented Feb 10, 2022

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.

Copy link
Contributor

@DemiMarie DemiMarie left a 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.

INSTALL Show resolved Hide resolved
lib/fsm.c Show resolved Hide resolved
@pmatilai
Copy link
Member Author

I'm quite aware of Linux having all manner of fancy extensions available.
Rpm is portable software and we need to fix this stuff using what's available in POSIX, utilizing non-portable extensions would only make things far more complicated rather than help. There's enough complexities to deal with as it is, thank you very much.

@pmatilai pmatilai force-pushed the dirsafe-pr branch 2 times, most recently from da3507b to 008cbca Compare February 15, 2022 07:30
@pmatilai
Copy link
Member Author

pmatilai commented Feb 15, 2022

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.
Of course a change this big and drastic will have bugs in it initially, I have no illusions about that.

(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.
@pmatilai
Copy link
Member Author

pmatilai commented Feb 15, 2022

Okay, test-suite + all my local tests (install to empty chroot etc) pass now 🥳

@DemiMarie
Copy link
Contributor

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.

Cute (but non-portable) trick: use paths of the form /dev/fd/$FDNUM/something. Works at least on Linux.

@pmatilai
Copy link
Member Author

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.

@pmatilai
Copy link
Member Author

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.

@pmatilai
Copy link
Member Author

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 pmatilai merged commit 6dd6272 into rpm-software-management:master Feb 16, 2022
@pmatilai pmatilai deleted the dirsafe-pr branch April 7, 2022 11:14
@sandy-lcq
Copy link

@pmatilai Thanks for fixing these CVEs. And I want to double check with you that
does these 32 commits in this pull request fully fix CVE-2021-35937, CVE-2021-35938, CVE-2021-35939?
Any plan to porting it to 4.17.x branch?

@pmatilai
Copy link
Member Author

pmatilai commented Feb 8, 2023

There will be no backports.

dmnks added a commit to dmnks/rpm that referenced this pull request Jul 15, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Jul 16, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Jul 16, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Jul 16, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Jul 16, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Jul 30, 2024
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
pmatilai pushed a commit that referenced this pull request Aug 1, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Aug 29, 2024
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)
dmnks added a commit that referenced this pull request Aug 30, 2024
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)
dmnks added a commit to dmnks/rpm that referenced this pull request Oct 21, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Oct 21, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Oct 21, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Oct 21, 2024
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
dmnks added a commit to dmnks/rpm that referenced this pull request Oct 21, 2024
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
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.

3 participants