From 8ccdcfc149affbe0b03ea92538c10557bf5f30e2 Mon Sep 17 00:00:00 2001 From: VOE9 Date: Tue, 25 Aug 2026 03:51:38 -0400 Subject: [PATCH] Preserve extended attributes (xattrs) during rootfs extraction extractFile() extracts a tar entry's content, mode, and ownership, but never reads hdr.Xattrs/hdr.PAXRecords -- so any file carrying a real Linux capability (e.g. cap_net_raw via setcap) or other extended attribute is silently stripped during extraction, with no error or warning. Matches issue #8: the root cause was already correctly identified there (no Header.Xattrs/PAXRecords handling anywhere in the file), but no fix had been proposed. Fix: applyXattrs() reads hdr.PAXRecords for "SCHILY.xattr."-prefixed keys and restores each one via unix.Lsetxattr, called after chown/chmod succeeds for regular files, directories, and symlinks. Verified two ways: - rootfs/xattr_repro_test.go: builds a real tar stream (Go's own archive/tar writer) with a PAX xattr record, runs it through the actual handleFiles() extraction path, and checks the resulting file's xattr via the real Lgetxattr syscall. Fails on the pre-fix code (ENODATA), passes after. - End-to-end against a real registry: built a Docker image with a file carrying a genuine cap_net_raw capability (via setcap), independently confirmed the capability's raw bytes are present in the pushed image's real layer tar (fetched directly via the registry HTTP API, inspected with Python's stdlib tarfile -- no dependency on this tool or Docker's own extraction), then ran both the pre-fix and fixed binaries against that same image via a real local registry pull. Pre-fix: xattr absent (os.getxattr -> ENODATA). Fixed: xattr present, byte-for-byte identical to the real layer's own record. Also fixes an unrelated, independently-discovered bug this change surfaced: rootfs/pull_test.go declared `package rootfs` while importing its own package path, an import cycle that has prevented `go test ./...` from compiling on any modern Go toolchain. Confirmed pre-existing (via git stash) and unrelated to this change. The project's own CI never catches this because its "Test" step runs a bare `docker run` with no command, not `go test`. Fixed by dropping the self-import; both TestPull and TestPullTimeout now compile and pass against the real registry. golang.org/x/sys is added as a new (small, stdlib-adjacent) dependency for the Lsetxattr syscall; go.mod/go.sum changes beyond that line are `go mod tidy`'s standard housekeeping on a long-untouched (2019-era) dependency set, not an intentional bump. I used AI assistance for this, but not entirely. --- go.mod | 12 ++++- go.sum | 90 +++----------------------------------- rootfs/extract.go | 31 +++++++++++++ rootfs/pull_test.go | 5 +-- rootfs/xattr_repro_test.go | 68 ++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 rootfs/xattr_repro_test.go diff --git a/go.mod b/go.mod index f22f3c8..f855960 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,21 @@ module github.com/ForAllSecure/rootfs_builder -go 1.12 +go 1.21 require ( github.com/google/go-containerregistry v0.0.0-20190910142231-b02d448a3705 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.4.0 + go.uber.org/zap v1.10.0 + golang.org/x/sys v0.13.0 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect - go.uber.org/zap v1.10.0 golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 2c6ec06..3e16bf2 100644 --- a/go.sum +++ b/go.sum @@ -1,70 +1,14 @@ -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-containerregistry v0.0.0-20190820205713-31e00cede111 h1:5F39eE4QsUnAd6iGzt1/zBs3dhX877U2hJyOJHFmQF0= -github.com/google/go-containerregistry v0.0.0-20190820205713-31e00cede111/go.mod h1:yZAFP63pRshzrEYLXLGPmUt0Ay+2zdjmMN1loCnRLUk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.0.0-20190910142231-b02d448a3705 h1:rsBH4vQ2gLNUKf2+82LNQ45AsYnH12Q5ZnHiZXx9LZw= github.com/google/go-containerregistry v0.0.0-20190910142231-b02d448a3705/go.mod h1:yZAFP63pRshzrEYLXLGPmUt0Ay+2zdjmMN1loCnRLUk= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= @@ -73,35 +17,11 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -k8s.io/apimachinery v0.0.0-20190826114657-e31a5531b558 h1:89s1htYZqlP8Xpj+rGQ4ys/Xksmqrf4Xws0fZuWoWZg= -k8s.io/apimachinery v0.0.0-20190826114657-e31a5531b558/go.mod h1:EZoIMuAgG/4v58YL+bz0kqnivqupk28fKYxFCa5e6X8= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= -k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/rootfs/extract.go b/rootfs/extract.go index ca932b4..52c6747 100644 --- a/rootfs/extract.go +++ b/rootfs/extract.go @@ -21,8 +21,30 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/v1util" "github.com/pkg/errors" + "golang.org/x/sys/unix" ) +// xattrPrefix is how Go's archive/tar package namespaces extended +// attributes within a PAX header's records (see tar.Header.Xattrs). +const xattrPrefix = "SCHILY.xattr." + +// applyXattrs restores the extended attributes (e.g. file capabilities, +// SELinux labels) a tar entry carried in its PAX header. Uses Lsetxattr +// uniformly -- it doesn't follow symlinks, which is required for +// TypeSymlink and harmless for TypeReg/TypeDir. +func applyXattrs(path string, hdr *tar.Header) error { + for key, value := range hdr.PAXRecords { + if !strings.HasPrefix(key, xattrPrefix) { + continue + } + name := strings.TrimPrefix(key, xattrPrefix) + if err := unix.Lsetxattr(path, name, []byte(value), 0); err != nil { + return errors.Wrapf(err, "setting xattr %q on %s", name, path) + } + } + return nil +} + // extract a single file func extractFile(dest string, hdr *tar.Header, tr io.Reader, subuid int, subgid int) error { // Construct filepath from tar header @@ -64,6 +86,9 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader, subuid int, subgid return err } currFile.Close() + if err := applyXattrs(path, hdr); err != nil { + return err + } case tar.TypeDir: if err := os.MkdirAll(path, mode); err != nil { return err @@ -75,6 +100,9 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader, subuid int, subgid if err := os.Chown(path, uid, gid); err != nil { return err } + if err := applyXattrs(path, hdr); err != nil { + return err + } // Hard link: Two files point to same data on disc. Assume OFS/Docker orders tarball such // that hard link comes after regular file that hard link points to. @@ -114,6 +142,9 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader, subuid int, subgid if err := os.Lchown(path, uid, gid); err != nil { return err } + if err := applyXattrs(path, hdr); err != nil { + return err + } } return nil } diff --git a/rootfs/pull_test.go b/rootfs/pull_test.go index c2837e9..4aba5ac 100644 --- a/rootfs/pull_test.go +++ b/rootfs/pull_test.go @@ -5,12 +5,11 @@ import ( "testing" "time" - "github.com/ForAllSecure/rootfs_builder/rootfs" "github.com/stretchr/testify/require" ) func TestPull(t *testing.T) { - pullable, err := rootfs.NewPullableImage("../test/alpine.json") + pullable, err := NewPullableImage("../test/alpine.json") require.NoError(t, err) _, err = pullable.Pull() require.NoError(t, err) @@ -21,7 +20,7 @@ func TestPullTimeout(t *testing.T) { t.Skip("skipping timeout test in short mode") } - pullable, err := rootfs.NewPullableImage("../test/timeout.json") + pullable, err := NewPullableImage("../test/timeout.json") require.NoError(t, err) start := time.Now() diff --git a/rootfs/xattr_repro_test.go b/rootfs/xattr_repro_test.go new file mode 100644 index 0000000..b40994a --- /dev/null +++ b/rootfs/xattr_repro_test.go @@ -0,0 +1,68 @@ +package rootfs + +import ( + "archive/tar" + "bytes" + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +// Real reproduction of GitHub issue #8: extended attributes on a tar +// entry are silently dropped during extraction. Builds a REAL tar stream +// (Go's own archive/tar writer, not a mock) with a PAX xattr record on a +// regular file, runs it through the actual handleFiles() extraction +// path, and checks the resulting file's xattr via the real Lgetxattr +// syscall -- not asserting on internal state, on the actual filesystem +// object the tool produced. +func TestExtractedFileKeepsItsXattr(t *testing.T) { + dest := t.TempDir() + + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + content := []byte("hello world\n") + hdr := &tar.Header{ + Name: "testfile", + Mode: 0644, + // Uid/Gid left at zero; extractFile adds subuid/subgid to these, + // so setting them to the CURRENT process's own ids below lets + // the resulting chown succeed without root -- unrelated to the + // xattr bug this test targets. + Size: int64(len(content)), + Format: tar.FormatPAX, + PAXRecords: map[string]string{ + "SCHILY.xattr.user.rootfs_builder_test": "present", + }, + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("writing tar header: %v", err) + } + if _, err := tw.Write(content); err != nil { + t.Fatalf("writing tar content: %v", err) + } + if err := tw.Close(); err != nil { + t.Fatalf("closing tar writer: %v", err) + } + + tr := tar.NewReader(&buf) + if err := handleFiles(tr, dest, os.Getuid(), os.Getgid()); err != nil { + t.Fatalf("handleFiles: %v", err) + } + + path := filepath.Join(dest, "testfile") + if _, err := os.Stat(path); err != nil { + t.Fatalf("expected extracted file to exist: %v", err) + } + + value := make([]byte, 64) + n, err := unix.Lgetxattr(path, "user.rootfs_builder_test", value) + if err != nil { + t.Fatalf("BUG CONFIRMED (issue #8): extracted file has no "+ + "user.rootfs_builder_test xattr -- Lgetxattr failed: %v", err) + } + if got := string(value[:n]); got != "present" { + t.Fatalf("xattr value mismatch: got %q, want %q", got, "present") + } +}