<div dir="ltr">Why not just check the permissions on disk, from a rootfs postprocess command? That wouldn’t be specific to tar, but would be just as effective, since those run under pseudo.<div class="gmail_extra"><br><div class="gmail_quote">On Fri, Jul 7, 2017 at 12:36 PM, Haris Okanovic <span dir="ltr"><<a href="mailto:haris.okanovic@ni.com" target="_blank">haris.okanovic@ni.com</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">An IMAGE_POSTPROCESS_COMMAND verifying image tarballs have symbolic and<br>
numeric ownership attributes which match the enclosed shadow database:<br>
* uid and uname of each file is present in /etc/passwd<br>
* gid and gname of each file is present in /etc/group<br>
* ids and names are consistent between tar metadata and shadow database<br>
<br>
In other words, this test ensure there aren't any ownership surprises<br>
when a filesystem created from tarball is booted. It's particularly<br>
useful in cases where artifacts built outside of OE are included in<br>
images -- E.g. packages from an external feed.<br>
<br>
Testing: Verified core-image-base still builds<br>
<br>
Patch: <a href="https://github.com/harisokanovic/openembedded-core/commits/dev/hokanovi/image-ownership-sanity-check" rel="noreferrer" target="_blank">https://github.com/<wbr>harisokanovic/openembedded-<wbr>core/commits/dev/hokanovi/<wbr>image-ownership-sanity-check</a><br>
<br>
Signed-off-by: Haris Okanovic <<a href="mailto:haris.okanovic@ni.com">haris.okanovic@ni.com</a>><br>
---<br>
meta/classes/image.bbclass | 25 +++++++++-<br>
meta/lib/oe/image.py | 114 ++++++++++++++++++++++++++++++<wbr>+++++++++++++++<br>
2 files changed, 138 insertions(+), 1 deletion(-)<br>
create mode 100644 meta/lib/oe/image.py<br>
<br>
diff --git a/meta/classes/image.bbclass b/meta/classes/image.bbclass<br>
index 6e30b96745..7b11291a82 100644<br>
--- a/meta/classes/image.bbclass<br>
+++ b/meta/classes/image.bbclass<br>
@@ -192,7 +192,30 @@ python () {<br>
IMAGE_CLASSES += "image_types"<br>
inherit ${IMAGE_CLASSES}<br>
<br>
-IMAGE_POSTPROCESS_COMMAND ?= ""<br>
+python check_image_file_ownership () {<br>
+ import sys, os, oe.image<br>
+<br>
+ imgTypes = d.getVar("IMAGE_FSTYPES", True).split()<br>
+ tarImgTypes = [ x for x in imgTypes if "tar" in x ]<br>
+<br>
+ # bail if there isn't a tar image in this build<br>
+ # TODO: Maybe add other image types<br>
+ if len(tarImgTypes) == 0:<br>
+ bb.debug(1, "Skipping check, no 'tar' image specified")<br>
+ return<br>
+<br>
+ imgDeployDir = d.getVar("IMGDEPLOYDIR", True)<br>
+ imgName = d.getVar("IMAGE_NAME", True)<br>
+ imgNameSuffix = d.getVar("IMAGE_NAME_SUFFIX", True)<br>
+<br>
+ for tarFileExt in tarImgTypes:<br>
+ archiveFilename = (imgName + imgNameSuffix + "." + tarFileExt)<br>
+ archiveFilePath = os.path.join(imgDeployDir, archiveFilename)<br>
+<br>
+ oe.image.check_file_ownership_<wbr>tar(d, archiveFilePath)<br>
+}<br>
+<br>
+IMAGE_POSTPROCESS_COMMAND ?= " check_image_file_ownership "<br>
<br>
# some default locales<br>
IMAGE_LINGUAS ?= "de-de fr-fr en-gb"<br>
diff --git a/meta/lib/oe/image.py b/meta/lib/oe/image.py<br>
new file mode 100644<br>
index 0000000000..f1ba6f2613<br>
--- /dev/null<br>
+++ b/meta/lib/oe/image.py<br>
@@ -0,0 +1,114 @@<br>
+# Helper function for image building<br>
+<br>
+def check_file_ownership_tar(d, archiveFilePath):<br>
+ """<br>
+ Verifies file ownership meta-data in image tarball matches users<br>
+ and groups in shadow database (/etc/passwd and /etc/group files).<br>
+ """<br>
+ import sys, os, tarfile<br>
+ bb.debug(1, "Running check_file_ownership() on image '{0!s}'".format(<wbr>archiveFilePath))<br>
+<br>
+ try:<br>
+ archiveName = os.path.basename(<wbr>archiveFilePath)<br>
+ if not archiveName:<br>
+ archiveName = archiveFilePath<br>
+<br>
+ # maps<br>
+ unameMap = {} # username -> passwd file entry array<br>
+ gnameMap = {} # groupname -> group file entry array<br>
+ uidMap = {} # uid -> passwd file entry array<br>
+ gidMap = {} # gid -> group file entry array<br>
+ fileMap = {} # filepath -> TarInfo object<br>
+<br>
+ def read_shadow_file(fdName, fd, colCount, mapsList):<br>
+ """ Reads a colon (:) delimited shadow database file into mapsList """<br>
+ for mapObj,_ in mapsList:<br>
+ if len(mapObj) != 0:<br>
+ raise Exception("Already read '{0!s}' file.".format(fdName))<br>
+<br>
+ for line in fd:<br>
+ line = line.decode("utf-8").strip()<br>
+ words = line.split(":")<br>
+<br>
+ if len(words) != colCount:<br>
+ raise Exception("Malformed '{0!s}' file. Expected {1!s} cols.".format(fdName, colCount))<br>
+<br>
+ for mapObj,mapKeyCol in mapsList:<br>
+ mapKey = words[mapKeyCol]<br>
+ if len(mapKey) < 1:<br>
+ raise Exception("Map key in '{0!s}' must be at least one char long.".format(fdName))<br>
+<br>
+ if mapKey in mapObj:<br>
+ raise Exception("Malformed '{0!s}' file. Did not expect to find '{0!s}' in map.".format(fdName, mapKey))<br>
+<br>
+ mapObj[mapKey] = words<br>
+<br>
+ for mapObj,_ in mapsList:<br>
+ if len(mapObj) == 0:<br>
+ raise Exception("'{0!s}' is empty.".format(fdName))<br>
+<br>
+ bb.debug(1, "Read the archive and populate maps")<br>
+ with tarfile.open(name=<wbr>archiveFilePath, mode='r:*') as tar:<br>
+ for info in tar:<br>
+ if <a href="http://info.name" rel="noreferrer" target="_blank">info.name</a> in fileMap:<br>
+ raise Exception("Duplicate entry for '{0!s}' found.".format(<a href="http://info.name" rel="noreferrer" target="_blank">info.name</a>))<br>
+<br>
+ fileMap[<a href="http://info.name" rel="noreferrer" target="_blank">info.name</a>] = info<br>
+<br>
+ # populate shadow db maps<br>
+ if <a href="http://info.name" rel="noreferrer" target="_blank">info.name</a> == './etc/passwd': read_shadow_file(fdName=<a href="http://info.name" rel="noreferrer" target="_blank">info.<wbr>name</a>, fd=tar.extractfile(info), colCount=7, mapsList=[(unameMap, 0), (uidMap, 2), ])<br>
+ elif <a href="http://info.name" rel="noreferrer" target="_blank">info.name</a> == './etc/group': read_shadow_file(fdName=<a href="http://info.name" rel="noreferrer" target="_blank">info.<wbr>name</a>, fd=tar.extractfile(info), colCount=4, mapsList=[(gnameMap, 0), (gidMap, 2), ])<br>
+<br>
+ bb.debug(1, "Check for no shadow db")<br>
+ shadowItemCount = 0<br>
+ for mapObj in [unameMap, gnameMap, uidMap, gidMap]:<br>
+ shadowItemCount = shadowItemCount + len(mapObj)<br>
+ if shadowItemCount == 0:<br>
+ bb.warn(1, "check_file_ownership(): Skip; no shadow database in image '{0!s}'".format(archiveName))<br>
+ return<br>
+<br>
+ bb.debug(1, "Map sanity check")<br>
+ for mapObj in [unameMap, gnameMap, uidMap, gidMap, fileMap]:<br>
+ if len(mapObj) < 1:<br>
+ raise Exception("Uh oh. Empty map found.")<br>
+<br>
+ def badFileError(errorMessage, info, shadowEntry=None):<br>
+ linkname = ""<br>
+ if info.linkname:<br>
+ linkname = " -> {0!s}".format(info.linkname)<br>
+<br>
+ bb.error("BAD FILE '{0!s}': {1!s}".format(<a href="http://info.name" rel="noreferrer" target="_blank">info.name</a>, errorMessage))<br>
+ bb.error("## {info.uid!s}:{info.gid!s} ({info.uname!s}:{info.gname!s}<wbr>) 0{info.mode:o} {<a href="http://info.name" rel="noreferrer" target="_blank">info.name</a>!s}{linkname!s}".<wbr>format(info=info, linkname=linkname))<br>
+ if shadowEntry:<br>
+ bb.error("## {0!s}".format(shadowEntry))<br>
+<br>
+ bb.debug(1, "Check for bad files")<br>
+ for filepath,info in fileMap.items():<br>
+ if not info.uname in unameMap:<br>
+ badFileError("uname not in unameMap", info)<br>
+ continue<br>
+<br>
+ if not info.gname in gnameMap:<br>
+ badFileError("gname not in gnameMap", info)<br>
+ continue<br>
+<br>
+ if not str(info.uid) in uidMap:<br>
+ badFileError("Uid '{0!s}' not found".format(info.uid), info)<br>
+<br>
+ if not str(info.gid) in gidMap:<br>
+ badFileError("Gid '{0!s}' not found".format(info.gid), info)<br>
+<br>
+ unameEntry = unameMap[info.uname]<br>
+ gnameEntry = gnameMap[info.gname]<br>
+<br>
+ if str(info.uid) != unameEntry[2]:<br>
+ badFileError("uid mismatch, expecting '{0!s}' for uname '{1!s}'".format(unameEntry[2], info.uname), info, unameEntry)<br>
+<br>
+ if str(info.gid) != gnameEntry[2]:<br>
+ badFileError("gid mismatch, expecting '{0!s}' for gname '{1!s}'".format(gnameEntry[2], info.gname), info, gnameEntry)<br>
+<br>
+ bb.debug(1, "Successfully verified image '{0!s}'".format(archiveName))<br>
+<br>
+ # Error on any exceptions<br>
+ except Exception as e:<br>
+ bb.error("check_file_<wbr>ownership() exception: {0!s}".format(e))<br>
<span class="HOEnZb"><font color="#888888">--<br>
2.13.2<br>
<br>
--<br>
______________________________<wbr>_________________<br>
Openembedded-core mailing list<br>
<a href="mailto:Openembedded-core@lists.openembedded.org">Openembedded-core@lists.<wbr>openembedded.org</a><br>
<a href="http://lists.openembedded.org/mailman/listinfo/openembedded-core" rel="noreferrer" target="_blank">http://lists.openembedded.org/<wbr>mailman/listinfo/openembedded-<wbr>core</a><br>
</font></span></blockquote></div><br><br clear="all"><div><br></div>-- <br><div class="gmail_signature" data-smartmail="gmail_signature"><div dir="ltr"><div><div dir="ltr">Christopher Larson<br>kergoth at gmail dot com<br>Founder - BitBake, OpenEmbedded, OpenZaurus<br>Senior Software Engineer, Mentor Graphics</div></div></div></div>
</div></div>