[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