filesystem: allow .fscrypt to be a symlink
Support the case where the user has a read-only root filesystem (e.g.
with OSTree) and had previously created a symlink /.fscrypt pointing to
a writable location, so that login protectors can be created there.
Resolves https://github.com/google/fscrypt/issues/131
Eric Biggers
4 years ago
84 | 84 | // used when a Policy on filesystem A is protected with Protector on filesystem |
85 | 85 | // B. In this scenario, we store a "link file" in the protectors directory whose |
86 | 86 | // contents look like "UUID=3a6d9a76-47f0-4f13-81bf-3332fbe984fb". |
87 | // | |
88 | // We also allow ".fscrypt" to be a symlink which was previously created. This | |
89 | // allows login protectors to be created when the root filesystem is read-only, | |
90 | // provided that "/.fscrypt" is a symlink pointing to a writable location. | |
87 | 91 | type Mount struct { |
88 | 92 | Path string |
89 | 93 | Filesystem string |
123 | 127 | Device: %s`, m.Path, m.Filesystem, m.Options, m.Device) |
124 | 128 | } |
125 | 129 | |
126 | // BaseDir returns the path of the base fscrypt directory on this filesystem. | |
130 | // BaseDir returns the path to the base fscrypt directory for this filesystem. | |
127 | 131 | func (m *Mount) BaseDir() string { |
128 | return filepath.Join(m.Path, baseDirName) | |
132 | rawBaseDir := filepath.Join(m.Path, baseDirName) | |
133 | // We allow the base directory to be a symlink, but some callers need | |
134 | // the real path, so dereference the symlink here if needed. Since the | |
135 | // directory the symlink points to may not exist yet, we have to read | |
136 | // the symlink manually rather than use filepath.EvalSymlinks. | |
137 | target, err := os.Readlink(rawBaseDir) | |
138 | if err != nil { | |
139 | return rawBaseDir // not a symlink | |
140 | } | |
141 | if filepath.IsAbs(target) { | |
142 | return target | |
143 | } | |
144 | return filepath.Join(m.Path, target) | |
129 | 145 | } |
130 | 146 | |
131 | 147 | // ProtectorDir returns the directory containing the protector metadata. |
156 | 172 | return filepath.Join(m.PolicyDir(), descriptor) |
157 | 173 | } |
158 | 174 | |
159 | // tempMount creates a temporary Mount under the main directory. The path for | |
160 | // the returned tempMount should be removed by the caller. | |
175 | // tempMount creates a temporary directory alongside this Mount's base fscrypt | |
176 | // directory and returns a temporary Mount which represents this temporary | |
177 | // directory. The caller is responsible for removing this temporary directory. | |
161 | 178 | func (m *Mount) tempMount() (*Mount, error) { |
162 | trashDir, err := ioutil.TempDir(m.Path, tempPrefix) | |
163 | return &Mount{Path: trashDir}, err | |
179 | tempDir, err := ioutil.TempDir(filepath.Dir(m.BaseDir()), tempPrefix) | |
180 | return &Mount{Path: tempDir}, err | |
164 | 181 | } |
165 | 182 | |
166 | 183 | // err modifies an error to contain the path of this filesystem. |
19 | 19 | package filesystem |
20 | 20 | |
21 | 21 | import ( |
22 | "io/ioutil" | |
22 | 23 | "os" |
23 | 24 | "path/filepath" |
24 | 25 | "testing" |
108 | 109 | } |
109 | 110 | } |
110 | 111 | |
112 | // Test that when MOUNTPOINT/.fscrypt is a pre-created symlink, fscrypt will | |
113 | // create/delete the metadata at the location pointed to by the symlink. | |
114 | // | |
115 | // This is a helper function that is called twice: once to test an absolute | |
116 | // symlink and once to test a relative symlink. | |
117 | func testSetupWithSymlink(t *testing.T, mnt *Mount, symlinkTarget string, realDir string) { | |
118 | rawBaseDir := filepath.Join(mnt.Path, baseDirName) | |
119 | if err := os.Symlink(symlinkTarget, rawBaseDir); err != nil { | |
120 | t.Fatal(err) | |
121 | } | |
122 | defer os.Remove(rawBaseDir) | |
123 | ||
124 | if err := mnt.Setup(); err != nil { | |
125 | t.Fatal(err) | |
126 | } | |
127 | defer mnt.RemoveAllMetadata() | |
128 | if err := mnt.CheckSetup(); err != nil { | |
129 | t.Fatal(err) | |
130 | } | |
131 | if !isSymlink(rawBaseDir) { | |
132 | t.Fatal("base dir should still be a symlink") | |
133 | } | |
134 | if !isDir(realDir) { | |
135 | t.Fatal("real base dir should exist") | |
136 | } | |
137 | if err := mnt.RemoveAllMetadata(); err != nil { | |
138 | t.Fatal(err) | |
139 | } | |
140 | if !isSymlink(rawBaseDir) { | |
141 | t.Fatal("base dir should still be a symlink") | |
142 | } | |
143 | if isDir(realDir) { | |
144 | t.Fatal("real base dir should no longer exist") | |
145 | } | |
146 | } | |
147 | ||
148 | func TestSetupWithAbsoluteSymlink(t *testing.T) { | |
149 | mnt, err := getTestMount(t) | |
150 | if err != nil { | |
151 | t.Fatal(err) | |
152 | } | |
153 | tempDir, err := ioutil.TempDir("", "fscrypt") | |
154 | if err != nil { | |
155 | t.Fatal(err) | |
156 | } | |
157 | defer os.RemoveAll(tempDir) | |
158 | realDir := filepath.Join(tempDir, "realDir") | |
159 | if realDir, err = filepath.Abs(realDir); err != nil { | |
160 | t.Fatal(err) | |
161 | } | |
162 | testSetupWithSymlink(t, mnt, realDir, realDir) | |
163 | } | |
164 | ||
165 | func TestSetupWithRelativeSymlink(t *testing.T) { | |
166 | mnt, err := getTestMount(t) | |
167 | if err != nil { | |
168 | t.Fatal(err) | |
169 | } | |
170 | realDir := filepath.Join(mnt.Path, ".fscrypt-real") | |
171 | testSetupWithSymlink(t, mnt, ".fscrypt-real", realDir) | |
172 | } | |
173 | ||
111 | 174 | // Adding a good Protector should succeed, adding a bad one should fail |
112 | 175 | func TestAddProtector(t *testing.T) { |
113 | 176 | mnt, err := getSetupMount(t) |
55 | 55 | return info, err |
56 | 56 | } |
57 | 57 | |
58 | // loggedLstat runs os.Lstat (doesn't dereference trailing symlink), but it logs | |
59 | // the error if lstat returns any error other than nil or IsNotExist. | |
60 | func loggedLstat(name string) (os.FileInfo, error) { | |
61 | info, err := os.Lstat(name) | |
62 | if err != nil && !os.IsNotExist(err) { | |
63 | log.Print(err) | |
64 | } | |
65 | return info, err | |
66 | } | |
67 | ||
58 | 68 | // isDir returns true if the path exists and is that of a directory. |
59 | 69 | func isDir(path string) bool { |
60 | 70 | info, err := loggedStat(path) |
65 | 75 | func isDevice(path string) bool { |
66 | 76 | info, err := loggedStat(path) |
67 | 77 | return err == nil && info.Mode()&os.ModeDevice != 0 |
78 | } | |
79 | ||
80 | // isSymlink returns true if the path exists and is that of a symlink. | |
81 | func isSymlink(path string) bool { | |
82 | info, err := loggedLstat(path) | |
83 | return err == nil && info.Mode()&os.ModeSymlink != 0 | |
68 | 84 | } |
69 | 85 | |
70 | 86 | // isDirCheckPerm returns true if the path exists and is a directory. If the |