cmd/snap-update-ns: add planWritableMimic #4170

Merged
merged 12 commits into from Nov 23, 2017

Conversation

Projects
None yet
6 participants
Contributor

zyga commented Nov 7, 2017

This patch adds a helper function that inspects a directory and designs
changes that will create a "writable mimic" for that directory.

The writable mimic is done largely the same way we already do this in
the snap-confine source code (quirks.c) where we use a tmpfs that is
bind mounted over the original read-only place and a series of bind
mounts (a bind-mount farm essentially).

The solution presented here is more generic. The original directory is
"stashed" (bind mounted to a temporary directory), a tmpfs is mounted
and a series of bind mounts are created. In addition to supporting just
directories the code also supports creating files and even symlinks.

Files are bind-mounted just as directories but symlinks are created
directly, since the tmpfs is writable and symlinks cannot participate in
mounting.

The code only designs the necessary changes, the rest of the features
will come once prerequisite branches land.

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

cmd/snap-update-ns: add designWritableMimic
This patch adds a helper function that inspects a directory and designs
changes that will create a "writable mimic" for that directory.

The writable mimic is done largely the same way we already do this in
the snap-confine source code (quirks.c) where we use a tmpfs that is
bind mounted over the original read-only place and a series of bind
mounts (a bind-mount farm essentially).

The solution presented here is more generic. The original directory is
"stashed" (bind mounted to a temporary directory), a tmpfs is mounted
and a series of bind mounts are created. In addition to supporting just
directories the code also supports creating files and even symlinks.

Files are bind-mounted just as directories but symlinks are created
directly, since the tmpfs is writable and symlinks cannot participate in
mounting.

The code only designs the necessary changes, the rest of the features
will come once prerequisite branches land.

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

@zyga zyga requested a review from jdstrand Nov 7, 2017

codecov-io commented Nov 7, 2017

Codecov Report

Merging #4170 into master will increase coverage by 0.03%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #4170      +/-   ##
=========================================
+ Coverage   75.96%     76%   +0.03%     
=========================================
  Files         442     442              
  Lines       38490   38545      +55     
=========================================
+ Hits        29240   29295      +55     
  Misses       7234    7234              
  Partials     2016    2016
Impacted Files Coverage Δ
cmd/snap-update-ns/utils.go 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 53626eb...ca5c115. Read the comment docs.

+ Action: Mount, Entry: mount.Entry{Name: "none", Dir: dir, Type: "tmpfs"},
+ })
+ // Iterate over the items in the original directory (nothing is mounted _yet_).
+ entries, err := ioutilReadDir(dir)
@bboozzoo

bboozzoo Nov 8, 2017

Contributor

I'm not sure about using ioutil.ReadDir here. I think a better approach would be to f, err := os.Open(dir) and then range over f.Readdir(<some-sane-limit)). IIRC iotuil.ReadDir() does the same, but it will go and load FileInfo for all entries in directory. This may (but does not need to) be a problem if there are lots of files or dirs inside.

@zyga

zyga Nov 8, 2017

Contributor

I'm happy to tweak this though I don't think it will be a real problem. This is also nicer for mocking (one vs two + stateful helper).

cmd/snap-update-ns/utils.go
@@ -108,6 +115,64 @@ func secureMkdirAll(name string, perm os.FileMode, uid, gid int) error {
return nil
}
+func designWritableMimic(dir string) ([]*Change, error) {
@pedronis

pedronis Nov 10, 2017

Contributor

this function name sounds a bit strange to me... alternatives could be: planWritableMimic, writableMimicPlan, writableMimicChanges

@zyga

zyga Nov 10, 2017

Contributor

+1, renamed to plan...

zyga added some commits Nov 10, 2017

cmd/snap-update-ns: rename design to plan
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga changed the title from cmd/snap-update-ns: add designWritableMimic to cmd/snap-update-ns: add planWritableMimic Nov 10, 2017

cmd/snap-update-ns/utils.go
+ return nil, err
+ }
+ for _, fi := range entries {
+ // Skip non-directory elements as we cannot handle those yet.
@mvo5

mvo5 Nov 13, 2017

Collaborator

This comment says non-dir elements are skipped but some lines below the code handles m.IsRegular. What does that mean?

@zyga

zyga Nov 13, 2017

Contributor

That I didn't remove the comment :-) I'll fix this shortly.

EDIT: Done now

+ // Mount tmpfs over the original directory, hiding its contents.
+ changes = append(changes, &Change{
+ Action: Mount, Entry: mount.Entry{Name: "none", Dir: dir, Type: "tmpfs"},
+ })
@jdstrand

jdstrand Nov 20, 2017

Contributor

This isn't the extent of my review, but thought I'd mention that it doesn't seem like we'll ever need the contents of the mounted over directory ever again, so I think it would be good to try to not be wasteful with these mounts and cleanup. Right now you are doing:

  1. bind current on safe
  2. mount tmpfs on current
  3. for each in safe, mount in tmpfs

However, it seems we could do this instead:

  1. create a safe directory
  2. for each in current, mount in safe
  3. for each in current, unmount in current
  4. mount tmpfs on current
  5. for each in safe, mount in tmpfs
  6. for each in safe, unmount in safe

While this a few more steps, it means that when down we don't have any hidden mount under the tmpfs, taking up kernel resources, space in the mount table, etc

@zyga

zyga Nov 20, 2017

Contributor
22:55 < zyga-ubuntu> jdstrand: so I maybe agree if your proposal is undo-safe
22:56 < zyga-ubuntu> jdstrand: note that we may be asked to completely undo any of this
22:56 < zyga-ubuntu> jdstrand: and that the typical situation, nothing is hidden under the tmpfs mount
22:56 < zyga-ubuntu> jdstrand: (apart from a fragment of a squashfs mount)
22:56 < zyga-ubuntu> jdstrand: the simple approach I took guarantees (I hope, prove me wrong please) that I can always undo
22:56 < zyga-ubuntu> jdstrand: and create another layout
22:58 < jdstrand> zyga-ubuntu: that might be a case for needing the contents of the mounted over directory
23:00 < zyga-ubuntu> jdstrand: I also toyed with one more idea
23:00 < zyga-ubuntu> jdstrand: iff it's a good assumption that we can look at hostfs, we can fish the snap mounts there
23:00 < zyga-ubuntu> jdstrand: but it's not generic as the same code should be useful for poking holes in various places that are not just /snap 
23:01 < zyga-ubuntu> jdstrand: and while it's possible to find the same place in hostfs it may have been changed by the hard-coded mount commands in snap-confine 

I'm going to approve as is, but please consider the open questions inline and add the requested comments.

cmd/snap-update-ns/utils.go
@@ -286,6 +291,63 @@ func secureMkfileAll(name string, perm os.FileMode, uid, gid int) error {
return err
}
+func planWritableMimic(dir string) ([]*Change, error) {
@jdstrand

jdstrand Nov 20, 2017

Contributor

I think a comment above here describing what the function does and why would be handy (eg, describing that the algorithm is intentionally simple so that the changes can be undone be simple going backwards, justifying mounting over 'dir' if you decide to keep the current implementation (eg, so the undo works as expected, there isn't much in there, etc), ....

@zyga

zyga Nov 23, 2017

Contributor

Done

cmd/snap-update-ns/utils.go
+ // Bind mount the original directory elsewhere for safe-keeping.
+ changes = append(changes, &Change{
+ Action: Mount, Entry: mount.Entry{
+ // XXX: should we rbind here?
@jdstrand

jdstrand Nov 20, 2017

Contributor

I think this question needs to be answered before committing.

+ Action: Mount, Entry: mount.Entry{Name: "none", Dir: dir, Type: "tmpfs"},
+ })
+ // Iterate over the items in the original directory (nothing is mounted _yet_).
+ entries, err := ioutilReadDir(dir)
@jdstrand

jdstrand Nov 20, 2017

Contributor

Not a big deal (or a blocker), cause, like you said, nothing is mounted yet and this is just recording changes, but I think it might be easier to read if you did:

entries, err := ioutilReadDir(safeKeepingDir)
@zyga

zyga Nov 23, 2017

Contributor

Well, we cannot because that's just us planning and not really doing stuff. The safe keeping directory doesn't even exist yet, let alone is bound mounted.

cmd/snap-update-ns/utils.go
+ ch := &Change{Action: Mount, Entry: mount.Entry{
+ Name: filepath.Join(safeKeepingDir, fi.Name()),
+ Dir: filepath.Join(dir, fi.Name()),
+ // XXX: should we rbind here?
@jdstrand

jdstrand Nov 20, 2017

Contributor

This too.

@zyga

zyga Nov 23, 2017

Contributor

Done

cmd/snap-update-ns/utils.go
+ Name: filepath.Join(safeKeepingDir, fi.Name()),
+ Dir: filepath.Join(dir, fi.Name()),
+ // XXX: should we rbind here?
+ Options: []string{"bind", "ro"},
@jdstrand

jdstrand Nov 20, 2017

Contributor

Soo... the function is called planWritableMimic, but we are only mounting read-only here. AIUI, the read-only is still hugely useful for making files accessible to where the snap expects them. That said, maybe just call this planMimic()?

@zyga

zyga Nov 23, 2017

Contributor

I was thinking about it and I think the extra "ro" is not needed. Changed.

+ case m.IsRegular():
+ ch.Entry.Options = append(ch.Entry.Options, "x-snapd.kind=file")
+ changes = append(changes, ch)
+ case m&os.ModeSymlink != 0:
@jdstrand

jdstrand Nov 20, 2017

Contributor

/me wonders why golang didn't implement m.IsSymlink()...

@zyga

zyga Nov 23, 2017

Contributor

Probably because of Windows but I don't really know.

cmd/snap-update-ns/utils.go
+ changes = append(changes, ch)
+ }
+ default:
+ logger.Noticef("skipping unsupported thing %s", fi)
@jdstrand

jdstrand Nov 20, 2017

Contributor

Maybe "skipping unsupported file %s"? Bonus points for showing the mode would be good (pipe, etc).

@zyga

zyga Nov 23, 2017

Contributor

Done

cmd/snap-update-ns/utils_test.go
+ changes, err := update.PlanWritableMimic("/foo")
+ c.Assert(err, IsNil)
+ c.Assert(changes, DeepEquals, []*update.Change{
+ // Store /foo in /tmp.snap/foo while we set things up
@jdstrand

jdstrand Nov 20, 2017

Contributor

Comment has a typo. Should be '/tmp/.snap/foo'

@zyga

zyga Nov 23, 2017

Contributor

Fixed.

cmd/snap-update-ns/utils_test.go
+ // Bind mount files and directories over. Note that files are identified by x-snapd.kind=file option.
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "ro", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind", "ro"}}, Action: update.Mount},
+ // Create symlinks, if we cannot readlink just skip that entry.
@jdstrand

jdstrand Nov 20, 2017

Contributor

The comment is unclear. I suggest:

// Create symlink
{Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "ro", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount},
// Bad symlinks and all other file types are skipped and not recoarded in mount changes
// Unmount the safe-keeping directory
...

@pedronis pedronis added the Blocked label Nov 23, 2017

Contributor

pedronis commented Nov 23, 2017

marking blocked because it seems it has +2 but a last batch of comments from jdstrand for consideration

Contributor

zyga commented Nov 23, 2017

@pedronis yes, I'll update everything that Jamie requested soon.

@zyga zyga removed the Blocked label Nov 23, 2017

Contributor

zyga commented Nov 23, 2017

All the points should be addressed now, I removed the blocking label and will merge when green.

zyga added some commits Nov 23, 2017

cmd/snap-update-ns: document planWritableMimic
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-update-ns: document why we're not doing rbind
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-update-ns: create writable bind mounts
This is a writable mimic after all. The original directories are read
only but we don't want to introduce additional walls.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-update-ns: tweak diagnostic message
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-update-ns: fix typo
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-update-ns: adjust comment
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

@zyga zyga merged commit b625c8e into snapcore:master Nov 23, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details

@zyga zyga deleted the zyga:feature/design-writable-mimic branch Nov 23, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment