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

Handle bind mounts correctly, and other mount-related improvements #154

Merged
merged 10 commits into from
Oct 30, 2019
Merged

Handle bind mounts correctly, and other mount-related improvements #154

merged 10 commits into from
Oct 30, 2019

Conversation

ebiggers
Copy link
Collaborator

@ebiggers ebiggers commented Oct 1, 2019

Store fscrypt metadata in only one place per filesystem, so that bind mounts don't get their own metadata directories (which was ambiguous, as the same file may be accessible via multiple mounts).

Also correctly set the source device for root filesystems mounted via the kernel command line, and fix creating linked protectors to such filesystems.

See the commit messages for details.

@ebiggers
Copy link
Collaborator Author

ebiggers commented Oct 1, 2019

@josephlr if/when you merge this and #148, please use the merge option rather than rebase, so the individual commits are preserved. Thanks!

@josephlr
Copy link
Member

josephlr commented Oct 1, 2019

@josephlr if/when you merge this and #148, please use the merge option rather than rebase, so the individual commits are preserved. Thanks!

Will do, thanks for splitting this out into such nice commits! This is a fairly large PR, but it's mostly test code, so it should be easier to review than #148.

Copy link
Member

@josephlr josephlr left a comment

Choose a reason for hiding this comment

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

I'm slowly making my way though this I have a lot more comments though

filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint_test.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
Copy link
Member

@josephlr josephlr left a comment

Choose a reason for hiding this comment

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

This looks mostly good, we mostly just need to figure out what bind mount cases we can safely support.

Also, right now GetMount() is the only function that uses mountsByPath after initialization, and that can be replaced with a wrapper around FindMount(), but I should fix that up in a future PR.

filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/path.go Outdated Show resolved Hide resolved
filesystem/path.go Outdated Show resolved Hide resolved
filesystem/path.go Outdated Show resolved Hide resolved
filesystem/mountpoint_test.go Show resolved Hide resolved
filesystem/mountpoint_test.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
Make it clear that this refers to a type of filesystem such as "ext4",
rather than to a specific filesystem instance.
fscrypt doesn't currently do anything with the mount options, so remove
them from the Mount structure for now.
Make it clearer that this function loads data into global data
structures, and doesn't return anything.
@ebiggers
Copy link
Collaborator Author

Also, right now GetMount() is the only function that uses mountsByPath after initialization, and that can be replaced with a wrapper around FindMount(), but I should fix that up in a future PR.

This is done in the latest version: GetMount() now calls FindMount(), then uses os.SameFile() to check whether the given path names the filesystem's root directory. This actually fixes a bug in my original proposal; the bug was that if a filesystem was fully mounted in multiple places, then GetMount() would only succeed on one of these mountpoints. But it should succeed on any of them, so that e.g. you can use fscrypt status on any full mountpoint rather than have to guess which one is the right one.

Copy link
Member

@josephlr josephlr left a comment

Choose a reason for hiding this comment

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

Thanks again for working on this, looks good to me (modulo nits).

filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Show resolved Hide resolved
filesystem/mountpoint.go Outdated Show resolved Hide resolved
filesystem/mountpoint.go Show resolved Hide resolved
Comment on lines 62 to 67
str[i+1] >= '0' && str[i+1] < '8' &&
str[i+2] >= '0' && str[i+2] < '8' &&
str[i+3] >= '0' && str[i+3] < '8' {
b = ((str[i+1] - '0') << 6) |
((str[i+2] - '0') << 3) |
(str[i+3] - '0')
Copy link
Member

Choose a reason for hiding this comment

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

I agree with not having strconv handle all of the escaping, but it probably should handle the octal encoding. Something like:

func unescapeString(str string) string {
	escapedStr := make([]byte, len(str))
	j := 0
	for i := 0; i < len(str); i++ {
		b := str[i]
		if b == '\\' && i+4 <= len(str) {
			if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
				b = parsed
				i += 3
			}
		}
		escapedStr[j] = b
		j++
	}
	return string(escapedStr[:j])
}

or if we use strings.Builder

func unescapeString(str string) string {
	var sb strings.Builder
	for i := 0; i < len(str); i++ {
		b := str[i]
		if b == '\\' && i+4 <= len(str) {
			if parsed, err := strconv.ParseInt(str[i+1:i+4], 8, 8); err == nil {
				b = parsed
				i += 3
			}
		}
		sb.WriteByte(b)
	}
	return sb.String()
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done with the strings.Builder version, but I kept i + 3 < len(str) rather than i + 4 <= len(str). I find the first version easier to understand since it checks whether 3 more bytes are available beyond the current one.

Also, parsed needed to be cast to uint8.

Change loadMountInfo() to load the mounts directly from
/proc/self/mountinfo, rather than use the mntent.h C library calls.

This is needed for correct handling of bind mounts and of "/dev/root",
since /proc/self/mountinfo has extra fields which show the mounted
subtree and the filesystem's device number.  /proc/mounts lacks these
fields, and the C library calls can't provide them.

To start, this patch just switches to using /proc/self/mountinfo,
without doing anything with the extra fields yet.

As a bonus, this eliminates all C code in mountpoint.go.
The kernel always shows mountpoints as absolute paths without symlinks,
so there's no need to canonicalize them in userspace.
Add a utility type and functions for handling device numbers.
A root filesystem mounted via the kernel command line always has a
source of "/dev/root", which isn't a real device node.  This makes
fscrypt think this filesystem doesn't have a source device, which breaks
creating login passphrase-protected directories on other filesystems:

    fscrypt encrypt: filesystem /: no device for mount "/": system error: cannot create filesystem link

This also makes 'fscrypt status' show a blank source device:

    MOUNTPOINT  DEVICE          FILESYSTEM  ENCRYPTION     FSCRYPT
    /                           ext4        supported      Yes

To fix this case, update loadMountInfo() to map the device number to the
device name via sysfs rather than use the mount source field.
The previous patch fixed making linked protectors to /dev/root, by
setting Mount.Device to the real device node rather than /dev/root.

That's good, but it also hints that the linked protector handling is
unnecessarily fragile, as it relies on the device node name matching
exactly.  The Linux kernel allows the same device to have multiple
device nodes, and path comparisons are slow and error-prone in general.

Change it to compare the device number instead.
Currently, fscrypt treats bind mounts as separate filesystems.  This is
broken because fscrypt will look for a directory's encryption policy in
different places depending on which mount it's accessed through.  This
forces users to create an fscrypt metadata directory at every bind
mount, and to copy fscrypt metadata around between mounts.

Fix this by storing fscrypt metadata only at the root of the filesystem.

To accomplish this:

- Make mountsByDevice store only a single Mount per filesystem, rather
  than multiple.  For this Mount, choose a mount of the full filesystem
  if available, preferably a read-write mount.  If the filesystem has
  only bind mounts, store a nil entry in mountsByDevice so we can show a
  proper error message later.

- Change FindMount() and GetMount() to look up the Mount by device
  number rather than by path, so that they don't return different Mounts
  depending on which path is used.

- Change AllFilesystems() to not return bind mounts.

- Due to the above changes, the mountsByPath map is no longer needed
  outside of loadMountInfo().  So make it a local variable there.

Resolves #59
Add a version of loadMountInfo() that takes an io.Reader parameter to
allow injecting a custom mountinfo file, then add some unit tests.
@josephlr
Copy link
Member

Merging, Travis is passing, but GitHub just isn't updating:
https://travis-ci.org/google/fscrypt/builds/605074267

@josephlr josephlr merged commit 9b2f1c3 into google:master Oct 30, 2019
@ebiggers ebiggers deleted the bind-mounts branch October 30, 2019 21:53
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