[OE-core] [PATCH] [RFC] image.bbclass & image.py: Add check_image_file_ownership()

Haris Okanovic haris.okanovic at ni.com
Fri Jul 7 19:36:23 UTC 2017


An IMAGE_POSTPROCESS_COMMAND verifying image tarballs have symbolic and
numeric ownership attributes which match the enclosed shadow database:
 * uid and uname of each file is present in /etc/passwd
 * gid and gname of each file is present in /etc/group
 * ids and names are consistent between tar metadata and shadow database

In other words, this test ensure there aren't any ownership surprises
when a filesystem created from tarball is booted. It's particularly
useful in cases where artifacts built outside of OE are included in
images -- E.g. packages from an external feed.

Testing: Verified core-image-base still builds

Patch: https://github.com/harisokanovic/openembedded-core/commits/dev/hokanovi/image-ownership-sanity-check

Signed-off-by: Haris Okanovic <haris.okanovic at ni.com>
---
 meta/classes/image.bbclass |  25 +++++++++-
 meta/lib/oe/image.py       | 114 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 138 insertions(+), 1 deletion(-)
 create mode 100644 meta/lib/oe/image.py

diff --git a/meta/classes/image.bbclass b/meta/classes/image.bbclass
index 6e30b96745..7b11291a82 100644
--- a/meta/classes/image.bbclass
+++ b/meta/classes/image.bbclass
@@ -192,7 +192,30 @@ python () {
 IMAGE_CLASSES += "image_types"
 inherit ${IMAGE_CLASSES}
 
-IMAGE_POSTPROCESS_COMMAND ?= ""
+python check_image_file_ownership () {
+    import sys, os, oe.image
+
+    imgTypes = d.getVar("IMAGE_FSTYPES", True).split()
+    tarImgTypes = [ x for x in imgTypes if "tar" in x ]
+
+    # bail if there isn't a tar image in this build
+    # TODO: Maybe add other image types
+    if len(tarImgTypes) == 0:
+        bb.debug(1, "Skipping check, no 'tar' image specified")
+        return
+
+    imgDeployDir = d.getVar("IMGDEPLOYDIR", True)
+    imgName = d.getVar("IMAGE_NAME", True)
+    imgNameSuffix = d.getVar("IMAGE_NAME_SUFFIX", True)
+
+    for tarFileExt in tarImgTypes:
+        archiveFilename = (imgName + imgNameSuffix + "." + tarFileExt)
+        archiveFilePath = os.path.join(imgDeployDir, archiveFilename)
+
+        oe.image.check_file_ownership_tar(d, archiveFilePath)
+}
+
+IMAGE_POSTPROCESS_COMMAND ?= " check_image_file_ownership "
 
 # some default locales
 IMAGE_LINGUAS ?= "de-de fr-fr en-gb"
diff --git a/meta/lib/oe/image.py b/meta/lib/oe/image.py
new file mode 100644
index 0000000000..f1ba6f2613
--- /dev/null
+++ b/meta/lib/oe/image.py
@@ -0,0 +1,114 @@
+# Helper function for image building
+
+def check_file_ownership_tar(d, archiveFilePath):
+    """
+    Verifies file ownership meta-data in image tarball matches users
+    and groups in shadow database (/etc/passwd and /etc/group files).
+    """
+    import sys, os, tarfile
+    bb.debug(1, "Running check_file_ownership() on image '{0!s}'".format(archiveFilePath))
+
+    try:
+        archiveName = os.path.basename(archiveFilePath)
+        if not archiveName:
+            archiveName = archiveFilePath
+
+        # maps
+        unameMap = {}  # username -> passwd file entry array
+        gnameMap = {}  # groupname -> group file entry array
+        uidMap = {}  # uid -> passwd file entry array
+        gidMap   = {}  # gid -> group file entry array
+        fileMap = {}  # filepath -> TarInfo object
+
+        def read_shadow_file(fdName, fd, colCount, mapsList):
+            """ Reads a colon (:) delimited shadow database file into mapsList """
+            for mapObj,_ in mapsList:
+                if len(mapObj) != 0:
+                    raise Exception("Already read '{0!s}' file.".format(fdName))
+
+            for line in fd:
+                line = line.decode("utf-8").strip()
+                words = line.split(":")
+
+                if len(words) != colCount:
+                    raise Exception("Malformed '{0!s}' file. Expected {1!s} cols.".format(fdName, colCount))
+
+                for mapObj,mapKeyCol in mapsList:
+                    mapKey = words[mapKeyCol]
+                    if len(mapKey) < 1:
+                        raise Exception("Map key in '{0!s}' must be at least one char long.".format(fdName))
+
+                    if mapKey in mapObj:
+                        raise Exception("Malformed '{0!s}' file. Did not expect to find '{0!s}' in map.".format(fdName, mapKey))
+
+                    mapObj[mapKey] = words
+
+            for mapObj,_ in mapsList:
+                if len(mapObj) == 0:
+                    raise Exception("'{0!s}' is empty.".format(fdName))
+
+        bb.debug(1, "Read the archive and populate maps")
+        with tarfile.open(name=archiveFilePath, mode='r:*') as tar:
+            for info in tar:
+                if info.name in fileMap:
+                    raise Exception("Duplicate entry for '{0!s}' found.".format(info.name))
+
+                fileMap[info.name] = info
+
+                # populate shadow db maps
+                if    info.name == './etc/passwd':  read_shadow_file(fdName=info.name, fd=tar.extractfile(info), colCount=7, mapsList=[(unameMap, 0), (uidMap, 2), ])
+                elif  info.name == './etc/group':   read_shadow_file(fdName=info.name, fd=tar.extractfile(info), colCount=4, mapsList=[(gnameMap, 0), (gidMap, 2), ])
+
+        bb.debug(1, "Check for no shadow db")
+        shadowItemCount = 0
+        for mapObj in [unameMap, gnameMap, uidMap, gidMap]:
+            shadowItemCount = shadowItemCount + len(mapObj)
+        if shadowItemCount == 0:
+            bb.warn(1, "check_file_ownership(): Skip; no shadow database in image '{0!s}'".format(archiveName))
+            return
+
+        bb.debug(1, "Map sanity check")
+        for mapObj in [unameMap, gnameMap, uidMap, gidMap, fileMap]:
+            if len(mapObj) < 1:
+                raise Exception("Uh oh. Empty map found.")
+
+        def badFileError(errorMessage, info, shadowEntry=None):
+            linkname = ""
+            if info.linkname:
+                linkname = " -> {0!s}".format(info.linkname)
+
+            bb.error("BAD FILE '{0!s}': {1!s}".format(info.name, errorMessage))
+            bb.error("## {info.uid!s}:{info.gid!s} ({info.uname!s}:{info.gname!s}) 0{info.mode:o} {info.name!s}{linkname!s}".format(info=info, linkname=linkname))
+            if shadowEntry:
+                bb.error("## {0!s}".format(shadowEntry))
+
+        bb.debug(1, "Check for bad files")
+        for filepath,info in fileMap.items():
+            if not info.uname in unameMap:
+                badFileError("uname not in unameMap", info)
+                continue
+
+            if not info.gname in gnameMap:
+                badFileError("gname not in gnameMap", info)
+                continue
+
+            if not str(info.uid) in uidMap:
+                badFileError("Uid '{0!s}' not found".format(info.uid), info)
+
+            if not str(info.gid) in gidMap:
+                badFileError("Gid '{0!s}' not found".format(info.gid), info)
+
+            unameEntry = unameMap[info.uname]
+            gnameEntry = gnameMap[info.gname]
+
+            if str(info.uid) != unameEntry[2]:
+                badFileError("uid mismatch, expecting '{0!s}' for uname '{1!s}'".format(unameEntry[2], info.uname), info, unameEntry)
+
+            if str(info.gid) != gnameEntry[2]:
+                badFileError("gid mismatch, expecting '{0!s}' for gname '{1!s}'".format(gnameEntry[2], info.gname), info, gnameEntry)
+
+        bb.debug(1, "Successfully verified image '{0!s}'".format(archiveName))
+
+    # Error on any exceptions
+    except Exception as e:
+        bb.error("check_file_ownership() exception: {0!s}".format(e))
-- 
2.13.2




More information about the Openembedded-core mailing list