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

cmd/snap-discard-ns: add support for per-user mount namespaces #6010

Merged
merged 12 commits into from Oct 25, 2018

Conversation

zyga
Copy link
Collaborator

@zyga zyga commented Oct 17, 2018

This patch rewrites snap-discard-ns to support user mounts. This
involves scanning for the new per-user mount namespaces and mount
profiles.

The program was simplified to not use the "abstraction" of provided by
"ns-support.h". The abstraction was poor and with the upcoming changes
to snap-confine to accommodate per-user mount namespaces it is easier to
audit and understand the code directly. This change makes "ns-support.h"
private to snap-confine so it can evolve separately.

The manual page was updated to reflect the new functionality and refresh
some cruft like project bug report URL.

Signed-off-by: Zygmunt Krynicki zygmunt.krynicki@canonical.com

@zyga zyga force-pushed the feature/user-mount-ns-2 branch 2 times, most recently from 302bed0 to 9d4c97d Compare October 17, 2018 14:18
This patch rewrites snap-discard-ns to support user mounts. This
involves scanning for the new per-user mount namespaces and mount
profiles.

The program was simplified to not use the "abstraction" of provided by
"ns-support.h". The abstraction was poor and with the upcoming changes
to snap-confine to accommodate per-user mount namespaces it is easier to
audit and understand the code directly. This change makes "ns-support.h"
private to snap-confine so it can evolve separately.

The manual page was updated to reflect the new functionality and refresh
some cruft like project bug report URL.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This patch fixes an issue that only manifests itself on Ubuntu 14.04
where /etc/mtab is a real file and not a symbolic link to /proc/mtab.
On such system mount(1) which uses libmount internally, keeps track
of all the operations by editing the mtab file. Sadly, our hackery inside
snap-confine and snap-discard-ns is totally not editing mtab. This causes
confusion when umount(1) is asked to detach a mount namespace file.

The problem is that at that time the file is already unmounted, thanks
to updated snap-discard-ns. Because the test had atificially created the
.mnt file with touch and mount(1) there is now a stale mount entry in mtab.
In the cleanup section of each test (actually the prepare section but
conceptually it is the cleanup after previous test) we, via snapd.postrm
or snap-mgmt.sh unmount and remove all the preserved mount namespaces.
When umount(1) is given a file that is not a mount point it goes to
search mtab, treating it as a mount _source_ (not target) and proceeds
to umount the target of whatever is found.

To avoid this issue, since we are not using libmount in snap-discard-ns
or snap-confine, simply avoid using libmount driven mount(1) in the test
as well.

This patch implements a thin python wrapper around mount and uses it
for the construction of the fake per-user mount namespace.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
On Amazon Linux 2 we don't have Python 3 but we do have Python 2.
Implement a Python 2.7 compatible version of mount.py and use it
automatically if Python 3 is not available.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
@zyga zyga requested a review from bboozzoo October 18, 2018 20:15
cmd/snap-discard-ns/snap-discard-ns.c Outdated Show resolved Hide resolved
# type: (unicode, unicode, unicode, int, unicode) -> None
"""mount is a thin wrapper around the mount library function."""
libc_name = find_library(b"c")
if libc_name is None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

if not libc_name:?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

PEP8 suggests testing None the way I did to avoid masking the distinction between None and other things that evaluate to false in boolean context.

@@ -0,0 +1,6 @@
#!/bin/sh
if [ "$(command -v python3)" != "" ]; then
exec ./mount-py3.py "$@";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have just one version of the script? The way you've written py2 version, it should work with py3 just fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried to initially but in the end I decided no to. Using one version of the script puts most pressure on the unicode vs string vs bytes across the two versions. Right now both versions are correct but this could be subtly broken with a single script.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think one script is enough. With the exception of b"", I don't see anything wrong with py2 version that would blow up under py3.

try:
main()
except Exception as err:
print(err, file=stderr)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you forget to raise? This will mask any errors, even programming ones.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I didn't intend to show the traceback but I think I did forget to return the exit status. I will fix this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can then drop the whole try/except block and just call main() or catch say OSError and let the others fall through. Right now you'll catch stuff like NameError which is probably not very useful without the actual backtrace.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then you could

try (OSError, RuntimeError) as err:
   print(err, file=stderr)
   raise SystemExit(1)

Thanks to Maciej for spotting this.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Thanks to Maciej for spotting this.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
try:
main()
except Exception as err:
print(err, file=stderr)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can then drop the whole try/except block and just call main() or catch say OSError and let the others fall through. Right now you'll catch stuff like NameError which is probably not very useful without the actual backtrace.

"""mount is a thin wrapper around the mount library function."""
libc_name = find_library(b"c")
if libc_name is None:
raise Exception("cannot find the C library")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe make that more specific, say RuntimeError

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I switched error handling, it should work fine as-is now.

try:
main()
except Exception as err:
print(err, file=stderr)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Then you could

try (OSError, RuntimeError) as err:
   print(err, file=stderr)
   raise SystemExit(1)

group.add_argument("-t", "--type", help="filesystem type to mount")
opts = parser.parse_args()
if opts.bind is not None:
mount(opts.source, opts.target, b"", MountOpts.Bind)
Copy link
Collaborator

Choose a reason for hiding this comment

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

mount(opts.source, opts.target, "", MountOpts.Bind)? If you run this under Py3, b"".encode('utf-8') when calling mount will fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What is the problem with b"".encode('utf-8')?

Copy link
Collaborator

Choose a reason for hiding this comment

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

bytes has no encode(), only decode()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In python 2 bytes is just a string. It does have the encode method.
The b prefix makes it a plain string, not an unicode string. Note that this is the py2 version of the code.

Having said that I will look at unifying this even though I would much rather not have to use python 2 and all of its quirks at all.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What I meant is that if you use the Py2 version of the script in Py3, b"" is bytes which does not have encode(). I think it's the only line here that can cause compatibility problems.

IIRC u"" is the unicode string in Py2, b"" and "" are the same (str). On the other hand, in Py3 u"" is the same as "" (str), but not b"" (which is bytes).

Copy link
Collaborator Author

@zyga zyga Oct 24, 2018

Choose a reason for hiding this comment

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

Yeah but this was for Python 2 specifically. The Python 3 version did not need that.

EDIT, removed part about u"" on Python3.

Please look at what I did now, it works and is Mypy-clean on both versions.

@@ -0,0 +1,6 @@
#!/bin/sh
if [ "$(command -v python3)" != "" ]; then
exec ./mount-py3.py "$@";
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think one script is enough. With the exception of b"", I don't see anything wrong with py2 version that would blow up under py3.

raise Exception("cannot find the C library")
libc = CDLL(libc_name, use_errno=True)
retval = libc.mount(
c_char_p(source.encode("UTF-8")),
Copy link
Collaborator

Choose a reason for hiding this comment

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

utf-8 is the default, so you may drop that.

Copy link
Collaborator Author

@zyga zyga Oct 24, 2018

Choose a reason for hiding this comment

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

UTF-8 is not the default, defaults depend on locale.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is in Py3. In Py2 it was derived from your locale or sth. OTOH, I'm not sure we should be forcing utf-8 here which may not be the same as input encoding so the input path provided to the script will be a different byte sequence from one encoded to utf-8.

#!/bin/sh
if [ "$(command -v python3)" != "" ]; then
exec python3 ./mount.py "$@";
elif [ "$(command -v python3)" != "" ]; then
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/3/2/ ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

if [ "$(command -v python3)" != "" ]; then
exec python3 ./mount.py "$@";
elif [ "$(command -v python3)" != "" ]; then
exec python2 ./mount-py2.py "$@";
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/mount-py2/mount/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Copy link
Contributor

@stolowski stolowski 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, but a couple of questions.

cmd/snap-discard-ns/snap-discard-ns.c Outdated Show resolved Hide resolved
cmd/snap-discard-ns/snap-discard-ns.c Outdated Show resolved Hide resolved
die("cannot remove current mount profile: %s",
profile_path);

const char *snap_instance_name = argv[1];
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make sure snap instance name is sane?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, we probably should. I will make that adjustment.

cmd/snap-discard-ns/snap-discard-ns.c Outdated Show resolved Hide resolved
/* We are only interested in regular files. The .mnt files, even if
* bind-mounted, appear as regular files and not as symbolic links due
* to the peculiarities of the Linux kernel. */
if (!S_ISREG(file_info.st_mode)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to move this up & check early before trying to match?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wrote this watching strace logs and realised we are stat-ing things that we don't care about. The pattern match is much faster than a system call so I chose to order it this way around.

|| fs_info.f_type == PROC_SUPER_MAGIC) {
debug("unmounting %s", dname);
if (umount2(dname, MNT_DETACH | UMOUNT_NOFOLLOW)
< 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This would read better if < 0) { was on the same line where the if starts, but I presume it was formatted automatically like this? Slightly odd given that the comment block above is much wider.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is automatically formatted, the mystery of indent. Perhaps given that this a near-full rewrite I could use clang-format for this file and actually switch to a sane format profile?

Copy link
Contributor

Choose a reason for hiding this comment

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

That might be a good idea... Feel free to give it a try before landing and apply if it produces sensible output.

if [ "$(command -v python3)" != "" ]; then
exec python3 ./mount.py "$@";
elif [ "$(command -v python2)" != "" ]; then
exec python2 ./mount.py "$@";
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this logic if it works with both versions of python, why not simply check for /usr/bin/python (and have #!/usr/bin/env python shebang?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

python is usually python2 but I wanted to be explicit. In practice we will only pick up the other case (python2) on the Amazon Linux 2 systems.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough, not a problem at all, just looks a little elaborate considering the fact that we don't actually care which python version it is.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Copy link
Contributor

@stolowski stolowski left a comment

Choose a reason for hiding this comment

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

Thanks for the changes, looks good to me! +1

const char *snap_instance_name = argv[1];
struct sc_error *err = NULL;
sc_instance_name_validate(snap_instance_name, &err);
sc_die_on_error(err);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, thank you.

|| fs_info.f_type == PROC_SUPER_MAGIC) {
debug("unmounting %s", dname);
if (umount2(dname, MNT_DETACH | UMOUNT_NOFOLLOW)
< 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

That might be a good idea... Feel free to give it a try before landing and apply if it produces sensible output.

if [ "$(command -v python3)" != "" ]; then
exec python3 ./mount.py "$@";
elif [ "$(command -v python2)" != "" ]; then
exec python2 ./mount.py "$@";
Copy link
Contributor

Choose a reason for hiding this comment

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

Fair enough, not a problem at all, just looks a little elaborate considering the fact that we don't actually care which python version it is.

Since this is a "clean-slate" due to the rewrite we can introduce
alternate formatting mode. After some experimentation I picked
clang-format and used the following style:

    {BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}

This makes the code read better than with defaults, especially due to
longer column limit.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
@zyga zyga merged commit 5decc83 into snapcore:master Oct 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants