<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">&lt;<a href="mailto:haris.okanovic@ni.com" target="_blank">haris.okanovic@ni.com</a>&gt;</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&#39;t any ownership surprises<br>
when a filesystem created from tarball is booted. It&#39;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 &lt;<a href="mailto:haris.okanovic@ni.com">haris.okanovic@ni.com</a>&gt;<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 += &quot;image_types&quot;<br>
 inherit ${IMAGE_CLASSES}<br>
<br>
-IMAGE_POSTPROCESS_COMMAND ?= &quot;&quot;<br>
+python check_image_file_ownership () {<br>
+    import sys, os, oe.image<br>
+<br>
+    imgTypes = d.getVar(&quot;IMAGE_FSTYPES&quot;, True).split()<br>
+    tarImgTypes = [ x for x in imgTypes if &quot;tar&quot; in x ]<br>
+<br>
+    # bail if there isn&#39;t a tar image in this build<br>
+    # TODO: Maybe add other image types<br>
+    if len(tarImgTypes) == 0:<br>
+        bb.debug(1, &quot;Skipping check, no &#39;tar&#39; image specified&quot;)<br>
+        return<br>
+<br>
+    imgDeployDir = d.getVar(&quot;IMGDEPLOYDIR&quot;, True)<br>
+    imgName = d.getVar(&quot;IMAGE_NAME&quot;, True)<br>
+    imgNameSuffix = d.getVar(&quot;IMAGE_NAME_SUFFIX&quot;, True)<br>
+<br>
+    for tarFileExt in tarImgTypes:<br>
+        archiveFilename = (imgName + imgNameSuffix + &quot;.&quot; + 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 ?= &quot; check_image_file_ownership &quot;<br>
<br>
 # some default locales<br>
 IMAGE_LINGUAS ?= &quot;de-de fr-fr en-gb&quot;<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>
+    &quot;&quot;&quot;<br>
+    Verifies file ownership meta-data in image tarball matches users<br>
+    and groups in shadow database (/etc/passwd and /etc/group files).<br>
+    &quot;&quot;&quot;<br>
+    import sys, os, tarfile<br>
+    bb.debug(1, &quot;Running check_file_ownership() on image &#39;{0!s}&#39;&quot;.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 -&gt; passwd file entry array<br>
+        gnameMap = {}  # groupname -&gt; group file entry array<br>
+        uidMap = {}  # uid -&gt; passwd file entry array<br>
+        gidMap   = {}  # gid -&gt; group file entry array<br>
+        fileMap = {}  # filepath -&gt; TarInfo object<br>
+<br>
+        def read_shadow_file(fdName, fd, colCount, mapsList):<br>
+            &quot;&quot;&quot; Reads a colon (:) delimited shadow database file into mapsList &quot;&quot;&quot;<br>
+            for mapObj,_ in mapsList:<br>
+                if len(mapObj) != 0:<br>
+                    raise Exception(&quot;Already read &#39;{0!s}&#39; file.&quot;.format(fdName))<br>
+<br>
+            for line in fd:<br>
+                line = line.decode(&quot;utf-8&quot;).strip()<br>
+                words = line.split(&quot;:&quot;)<br>
+<br>
+                if len(words) != colCount:<br>
+                    raise Exception(&quot;Malformed &#39;{0!s}&#39; file. Expected {1!s} cols.&quot;.format(fdName, colCount))<br>
+<br>
+                for mapObj,mapKeyCol in mapsList:<br>
+                    mapKey = words[mapKeyCol]<br>
+                    if len(mapKey) &lt; 1:<br>
+                        raise Exception(&quot;Map key in &#39;{0!s}&#39; must be at least one char long.&quot;.format(fdName))<br>
+<br>
+                    if mapKey in mapObj:<br>
+                        raise Exception(&quot;Malformed &#39;{0!s}&#39; file. Did not expect to find &#39;{0!s}&#39; in map.&quot;.format(fdName, mapKey))<br>
+<br>
+                    mapObj[mapKey] = words<br>
+<br>
+            for mapObj,_ in mapsList:<br>
+                if len(mapObj) == 0:<br>
+                    raise Exception(&quot;&#39;{0!s}&#39; is empty.&quot;.format(fdName))<br>
+<br>
+        bb.debug(1, &quot;Read the archive and populate maps&quot;)<br>
+        with tarfile.open(name=<wbr>archiveFilePath, mode=&#39;r:*&#39;) 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(&quot;Duplicate entry for &#39;{0!s}&#39; found.&quot;.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> == &#39;./etc/passwd&#39;:  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> == &#39;./etc/group&#39;:   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, &quot;Check for no shadow db&quot;)<br>
+        shadowItemCount = 0<br>
+        for mapObj in [unameMap, gnameMap, uidMap, gidMap]:<br>
+            shadowItemCount = shadowItemCount + len(mapObj)<br>
+        if shadowItemCount == 0:<br>
+            bb.warn(1, &quot;check_file_ownership(): Skip; no shadow database in image &#39;{0!s}&#39;&quot;.format(archiveName))<br>
+            return<br>
+<br>
+        bb.debug(1, &quot;Map sanity check&quot;)<br>
+        for mapObj in [unameMap, gnameMap, uidMap, gidMap, fileMap]:<br>
+            if len(mapObj) &lt; 1:<br>
+                raise Exception(&quot;Uh oh. Empty map found.&quot;)<br>
+<br>
+        def badFileError(errorMessage, info, shadowEntry=None):<br>
+            linkname = &quot;&quot;<br>
+            if info.linkname:<br>
+                linkname = &quot; -&gt; {0!s}&quot;.format(info.linkname)<br>
+<br>
+            bb.error(&quot;BAD FILE &#39;{0!s}&#39;: {1!s}&quot;.format(<a href="http://info.name" rel="noreferrer" target="_blank">info.name</a>, errorMessage))<br>
+            bb.error(&quot;## {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}&quot;.<wbr>format(info=info, linkname=linkname))<br>
+            if shadowEntry:<br>
+                bb.error(&quot;## {0!s}&quot;.format(shadowEntry))<br>
+<br>
+        bb.debug(1, &quot;Check for bad files&quot;)<br>
+        for filepath,info in fileMap.items():<br>
+            if not info.uname in unameMap:<br>
+                badFileError(&quot;uname not in unameMap&quot;, info)<br>
+                continue<br>
+<br>
+            if not info.gname in gnameMap:<br>
+                badFileError(&quot;gname not in gnameMap&quot;, info)<br>
+                continue<br>
+<br>
+            if not str(info.uid) in uidMap:<br>
+                badFileError(&quot;Uid &#39;{0!s}&#39; not found&quot;.format(info.uid), info)<br>
+<br>
+            if not str(info.gid) in gidMap:<br>
+                badFileError(&quot;Gid &#39;{0!s}&#39; not found&quot;.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(&quot;uid mismatch, expecting &#39;{0!s}&#39; for uname &#39;{1!s}&#39;&quot;.format(unameEntry[2], info.uname), info, unameEntry)<br>
+<br>
+            if str(info.gid) != gnameEntry[2]:<br>
+                badFileError(&quot;gid mismatch, expecting &#39;{0!s}&#39; for gname &#39;{1!s}&#39;&quot;.format(gnameEntry[2], info.gname), info, gnameEntry)<br>
+<br>
+        bb.debug(1, &quot;Successfully verified image &#39;{0!s}&#39;&quot;.format(archiveName))<br>
+<br>
+    # Error on any exceptions<br>
+    except Exception as e:<br>
+        bb.error(&quot;check_file_<wbr>ownership() exception: {0!s}&quot;.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>