Skip to content

Commit e5da54b

Browse files
committed
oci: layer: correctly handle trusted.overlay. xattr escaping
Signed-off-by: Aleksa Sarai <[email protected]>
1 parent a9d0756 commit e5da54b

File tree

6 files changed

+401
-79
lines changed

6 files changed

+401
-79
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
9090
extract plain whiteouts within an opaque whiteout directory in the same
9191
layer. (As per the OCI spec requirements, this is regardless of the order
9292
of the opaque whiteout and the regular whiteout in the layer archive.)
93+
- `UnpackLayer` and `Generate(Insert)Layer` now correctly handle
94+
`trusted.overlay.*` xattr escaping when extracting and generating layers with
95+
the overlayfs on-disk format. This escaping feature [has been supported by
96+
overlayfs since Linux 6.7][linux-overlayfs-escaping-dad02fad84cbc], and
97+
allows for you to created images that contain an overlayfs layout inside the
98+
image (nested to arbitrary levels).
99+
* If an image contains `trusted.overlay.*` xattrs, `UnpackLayer` will
100+
rewrite the xattrs to instead be in the `trusted.overlay.overlay.*`
101+
namespace, so that when merged using overlayfs the user will see the
102+
expected xattrs.
103+
* If an on-disk overlayfs directory used with `Generate(Insert)Layer`
104+
contains escaped `trusted.overlay.overlay.*` xattrs, they will be rewritten
105+
so that the generated layer contains `trusted.overlay.*` xattrs. If we
106+
encounter an unescaped `trusted.overlay.*` xattr they will not be included
107+
in the image (though they may cause the file to be converted to a whiteout
108+
in the image) because they are considered to be an internal aspect of the
109+
host on-disk format (i.e. `trusted.overlay.origin` might be automatically
110+
set by whatever tool is using the overlayfs layers).
111+
Note that in the regular extraction mode, these xattrs will be treated like
112+
any other xattrs (this is in contrast to the previous behaviour where they
113+
would be silently ignored regardless of the on-disk format being used).
114+
115+
[linux-overlayfs-escaping-dad02fad84cbc]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=dad02fad84cbce30f317b69a4f2391f90045f79d
93116

94117
## [0.4.7] - 2021-04-05 ##
95118

oci/layer/tar_extract.go

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,15 @@ func (te *TarExtractor) restoreMetadata(path string, hdr *tar.Header) error {
168168
// want, we first clear the set of xattrs from the file then apply the ones
169169
// set in the tar.Header.
170170
err := te.fsEval.Lclearxattrs(path, func(xattrName string) bool {
171-
_, ok := ignoreXattrs[xattrName]
172-
return ok
171+
filter, isComplex := findComplexXattrFilter(xattrName)
172+
return isComplex && !filter.UnpackShouldClear(te.whiteoutMode, hdr.Name, xattrName)
173173
})
174174
if err != nil {
175175
if !errors.Is(err, unix.ENOTSUP) {
176176
return fmt.Errorf("clear xattr metadata: %s: %w", path, err)
177177
}
178178
if !te.enotsupWarned {
179-
log.Warnf("xattr{%s} ignoring ENOTSUP on clearxattrs", path)
179+
log.Warnf("xattr{%s} ignoring ENOTSUP on clearxattrs", hdr.Name)
180180
log.Warnf("xattr{%s} destination filesystem does not support xattrs, further warnings will be suppressed", path)
181181
te.enotsupWarned = true
182182
} else {
@@ -187,26 +187,37 @@ func (te *TarExtractor) restoreMetadata(path string, hdr *tar.Header) error {
187187
for name, value := range hdr.Xattrs {
188188
value := []byte(value)
189189

190-
// Forbidden xattrs should never be touched.
191-
if _, skip := ignoreXattrs[name]; skip {
192-
// If the xattr is already set to the requested value, don't bail.
193-
// The reason for this logic is kinda convoluted, but effectively
194-
// because restoreMetadata is called with the *on-disk* metadata we
195-
// run the risk of things like "security.selinux" being included in
196-
// that metadata (and thus tripping the forbidden xattr error). By
197-
// only touching xattrs that have a different value we are somewhat
198-
// more efficient and we don't have to special case parent restore.
199-
// Of course this will only ever impact ignoreXattrs.
200-
if oldValue, err := te.fsEval.Lgetxattr(path, name); err == nil {
201-
if bytes.Equal(value, oldValue) {
202-
log.Debugf("restore xattr metadata: skipping already-set xattr %q: %s", name, hdr.Name)
203-
continue
190+
// Some xattrs need to be skipped for sanity reasons, such as
191+
// security.selinux, because they are very much host-specific and
192+
// extracting them from layers would be a really bad idea. Other xattrs
193+
// need to be remapped (such as trusted.overlay.* xattrs when in
194+
// overlayfs mode) to have correct values.
195+
mappedName := name
196+
if filter, isComplex := findComplexXattrFilter(name); isComplex {
197+
if newName := filter.UnpackEntry(te.whiteoutMode, hdr.Name, name); newName == nil {
198+
// If the xattr is already set to the requested value, skip it
199+
// more silently. The reason for this logic is kinda
200+
// convoluted, but effectively because restoreMetadata is
201+
// called with the *on-disk* metadata we run the risk of things
202+
// like "security.selinux" being included in that metadata (and
203+
// thus tripping the forbidden xattr error). By only touching
204+
// xattrs that have a different value we are somewhat more
205+
// efficient and we don't have to special case parent restore.
206+
// Of course this will only ever impact ignoreXattrs.
207+
if oldValue, err := te.fsEval.Lgetxattr(path, name); err == nil {
208+
if bytes.Equal(value, oldValue) {
209+
log.Debugf("xattr{%s} restore xattr metadata: skipping already-set xattr %q", hdr.Name, name)
210+
continue
211+
}
204212
}
213+
log.Warnf("xattr{%s} ignoring forbidden xattr: %q", hdr.Name, name)
214+
continue
215+
} else if *newName != name {
216+
mappedName = *newName
217+
log.Debugf("xattr{%s} remapping xattr %q to %q during extraction", hdr.Name, name, mappedName)
205218
}
206-
log.Warnf("xattr{%s} ignoring forbidden xattr: %q", hdr.Name, name)
207-
continue
208219
}
209-
if err := te.fsEval.Lsetxattr(path, name, value, 0); err != nil {
220+
if err := te.fsEval.Lsetxattr(path, mappedName, value, 0); err != nil {
210221
// In rootless mode, some xattrs will fail (security.capability).
211222
// This is _fine_ as long as we're not running as root (in which
212223
// case we shouldn't be ignoring xattrs that we were told to set).
@@ -216,15 +227,15 @@ func (te *TarExtractor) restoreMetadata(path string, hdr *tar.Header) error {
216227
// unprivileged users (we also would need to translate them
217228
// back when creating archives).
218229
if te.partialRootless && errors.Is(err, os.ErrPermission) {
219-
log.Warnf("rootless{%s} ignoring (usually) harmless EPERM on setxattr %q", hdr.Name, name)
230+
log.Warnf("rootless{%s} ignoring (usually) harmless EPERM on setxattr %q", hdr.Name, mappedName)
220231
continue
221232
}
222233
// We cannot do much if we get an ENOTSUP -- this usually means
223234
// that extended attributes are simply unsupported by the
224235
// underlying filesystem (such as AUFS or NFS).
225236
if errors.Is(err, unix.ENOTSUP) {
226237
if !te.enotsupWarned {
227-
log.Warnf("xattr{%s} ignoring ENOTSUP on setxattr %q", hdr.Name, name)
238+
log.Warnf("xattr{%s} ignoring ENOTSUP on setxattr %q", hdr.Name, mappedName)
228239
log.Warnf("xattr{%s} destination filesystem does not support xattrs, further warnings will be suppressed", path)
229240
te.enotsupWarned = true
230241
} else {
@@ -555,7 +566,23 @@ func (te *TarExtractor) UnpackEntry(root string, hdr *tar.Header, r io.Reader) (
555566
if err != nil {
556567
return fmt.Errorf("get xattr: %w", err)
557568
}
558-
dirHdr.Xattrs[xattr] = string(value)
569+
// Because restoreMetadata will re-apply these xattrs
570+
// (potentially remapping them if we have complexXattrs
571+
// filters) we need to map their names to match what we would
572+
// get from an actual archive. If the xattr should be ignored
573+
// we can safely skip it here because UnpackShouldClear will
574+
// also stop them from being cleared.
575+
mappedName := xattr
576+
if filter, isComplex := findComplexXattrFilter(xattr); isComplex {
577+
if newName := filter.GenerateEntry(te.whiteoutMode, unsafeDir, xattr); newName == nil {
578+
log.Warnf("xattr{%s} ignoring forbidden xattr: %q", unsafeDir, xattr)
579+
continue
580+
} else if *newName != xattr {
581+
mappedName = *newName
582+
log.Debugf("xattr{%s} remapping xattr %q to %q for later restoreMetadata", unsafeDir, xattr, mappedName)
583+
}
584+
}
585+
dirHdr.Xattrs[mappedName] = string(value)
559586
}
560587
}
561588

oci/layer/tar_extract_linux_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,137 @@ func TestUnpackEntry_OverlayFSWhiteout_Nested(t *testing.T) {
223223
assertIsOpaqueWhiteout(t, filepath.Join(dir, "opaque-nested/a/b"))
224224
assertIsOpaqueWhiteout(t, filepath.Join(dir, "opaque-nested/a/b/c/d"))
225225
}
226+
227+
func getAllXattrs(t *testing.T, path string) map[string]string {
228+
names, err := system.Llistxattr(path)
229+
require.NoErrorf(t, err, "fetch all xattrs for %q", path)
230+
xattrs := map[string]string{}
231+
for _, name := range names {
232+
value, err := system.Lgetxattr(path, name)
233+
require.NoErrorf(t, err, "fetch xattr %q for %q", name, path)
234+
xattrs[name] = string(value)
235+
}
236+
return xattrs
237+
}
238+
239+
func TestUnpackEntry_ComplexXattr_OverlayFS(t *testing.T) {
240+
dir := t.TempDir()
241+
242+
testNeedsMknod(t)
243+
testNeedsTrustedOverlayXattrs(t)
244+
245+
headers := []struct {
246+
pseudoHdr
247+
setXattrs, remappedXattrs map[string]string
248+
}{
249+
// Set a fake overlayfs xattr in the trusted.overlay namespace on a
250+
// directory that contains entries. Since restoreMetadata() gets called
251+
// on all parent directories when unpacking, this will cause
252+
// restoreMetadata() to be run on the same directory multiple times.
253+
// This lets us test that restoreMetadata will not re-apply the xattr
254+
// escaping even after being called multiple times.
255+
{
256+
pseudoHdr{"foo", "", tar.TypeDir},
257+
map[string]string{"trusted.overlay.fakexattr": "fakexattr"},
258+
map[string]string{"trusted.overlay.overlay.fakexattr": "fakexattr"},
259+
},
260+
// Some subpaths with dummy overlayfs xattrs.
261+
{
262+
pseudoHdr{"foo/bar", "file", tar.TypeReg},
263+
map[string]string{"trusted.overlay.whiteout": "foo"},
264+
map[string]string{"trusted.overlay.overlay.whiteout": "foo"},
265+
},
266+
{
267+
pseudoHdr{"foo/baz", "", tar.TypeDir},
268+
map[string]string{"trusted.overlay.opaque": "y"},
269+
map[string]string{"trusted.overlay.overlay.opaque": "y"},
270+
},
271+
// Several levels nested overlayfs xattrs.
272+
{
273+
pseudoHdr{"foo/extra-nesting", "", tar.TypeDir},
274+
map[string]string{
275+
"trusted.overlay.overlay.opaque": "x",
276+
"trusted.overlay.overlay.overlay.whiteout": "foobar",
277+
"trusted.overlay.overlay.overlay.overlay.overlay.overlay.dummy": "dummy xattr",
278+
},
279+
map[string]string{
280+
"trusted.overlay.overlay.overlay.opaque": "x",
281+
"trusted.overlay.overlay.overlay.overlay.whiteout": "foobar",
282+
"trusted.overlay.overlay.overlay.overlay.overlay.overlay.overlay.dummy": "dummy xattr",
283+
},
284+
},
285+
{
286+
pseudoHdr{"foo/extra-nesting/reg", "reg", tar.TypeReg},
287+
map[string]string{
288+
"trusted.overlay.overlay.overlay.overlay.overlay.dummy123": "dummy xattr 123",
289+
},
290+
map[string]string{
291+
"trusted.overlay.overlay.overlay.overlay.overlay.overlay.dummy123": "dummy xattr 123",
292+
},
293+
},
294+
}
295+
296+
// With extraction using OverlayFSWhiteout we expect to get the remapped
297+
// xattrs.
298+
t.Run("OverlayFSWhiteout", func(t *testing.T) {
299+
unpackOptions := UnpackOptions{
300+
MapOptions: MapOptions{
301+
Rootless: os.Geteuid() != 0,
302+
},
303+
WhiteoutMode: OverlayFSWhiteout,
304+
}
305+
306+
te := NewTarExtractor(unpackOptions)
307+
308+
for _, ph := range headers {
309+
hdr, rdr := fromPseudoHdr(ph.pseudoHdr)
310+
hdr.Xattrs = ph.setXattrs
311+
312+
err := te.UnpackEntry(dir, hdr, rdr)
313+
assert.NoErrorf(t, err, "UnpackEntry %s", hdr.Name)
314+
}
315+
316+
for _, ph := range headers {
317+
path := ph.pseudoHdr.path
318+
fullPath := filepath.Join(dir, path)
319+
320+
xattrs := getAllXattrs(t, fullPath)
321+
assert.Equalf(t, ph.remappedXattrs, xattrs, "UnpackEntry(%q): expected to see %#v remapped properly", path, ph.setXattrs)
322+
323+
// Make sure none of the files are whiteouts.
324+
_, isWo, err := isOverlayWhiteout(fullPath, fseval.Default)
325+
require.NoErrorf(t, err, "isOverlayWhiteout(%q)", path)
326+
assert.Falsef(t, isWo, "isOverlayWhiteout(%q): regular entries with overlayfs xattrs should not end up being unpacked with overlayfs whiteout xattrs", path)
327+
}
328+
})
329+
330+
// With extraction using OverlayFSWhiteout we expect to get the remapped
331+
// xattrs.
332+
t.Run("OCIStandardWhiteout", func(t *testing.T) {
333+
unpackOptions := UnpackOptions{
334+
MapOptions: MapOptions{
335+
Rootless: os.Geteuid() != 0,
336+
},
337+
WhiteoutMode: OCIStandardWhiteout,
338+
}
339+
340+
te := NewTarExtractor(unpackOptions)
341+
342+
for _, ph := range headers {
343+
hdr, rdr := fromPseudoHdr(ph.pseudoHdr)
344+
hdr.Xattrs = ph.setXattrs
345+
346+
err := te.UnpackEntry(dir, hdr, rdr)
347+
assert.NoErrorf(t, err, "UnpackEntry %s", hdr.Name)
348+
}
349+
350+
for _, ph := range headers {
351+
path := ph.pseudoHdr.path
352+
fullPath := filepath.Join(dir, path)
353+
354+
xattrs := getAllXattrs(t, fullPath)
355+
assert.Equalf(t, ph.setXattrs, xattrs, "UnpackEntry(%q): expected to see %#v not be remapped", path, ph.setXattrs)
356+
assert.NotEqualf(t, ph.remappedXattrs, xattrs, "UnpackEntry(%q): expected to see %#v not be remapped", path, ph.setXattrs)
357+
}
358+
})
359+
}

0 commit comments

Comments
 (0)