userland.py revision 4304
45N/A#!/usr/bin/python
45N/A#
45N/A# CDDL HEADER START
45N/A#
45N/A# The contents of this file are subject to the terms of the
45N/A# Common Development and Distribution License (the "License").
45N/A# You may not use this file except in compliance with the License.
45N/A#
45N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
45N/A# or http://www.opensolaris.org/os/licensing.
45N/A# See the License for the specific language governing permissions
45N/A# and limitations under the License.
45N/A#
45N/A# When distributing Covered Code, include this CDDL HEADER in each
45N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
45N/A# If applicable, add the following below this CDDL HEADER, with the
45N/A# fields enclosed by brackets "[]" replaced with your own identifying
45N/A# information: Portions Copyright [yyyy] [name of copyright owner]
45N/A#
45N/A# CDDL HEADER END
45N/A#
45N/A
45N/A#
4304N/A# Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
45N/A#
45N/A
45N/A# Some userland consolidation specific lint checks
45N/A
45N/Aimport pkg.lint.base as base
186N/Afrom pkg.lint.engine import lint_fmri_successor
84N/Aimport pkg.elf as elf
2756N/Aimport pkg.fmri
2756N/Aimport platform
45N/Aimport re
45N/Aimport os.path
2337N/Aimport subprocess
2756N/Aimport sys
45N/A
45N/Aclass UserlandActionChecker(base.ActionChecker):
45N/A """An opensolaris.org-specific class to check actions."""
45N/A
45N/A name = "userland.action"
45N/A
45N/A def __init__(self, config):
45N/A self.description = _(
45N/A "checks Userland packages for common content errors")
117N/A path = os.getenv('PROTO_PATH')
117N/A if path != None:
117N/A self.proto_path = path.split()
117N/A else:
117N/A self.proto_path = None
2756N/A solaris_ver = os.getenv('SOLARIS_VERSION', '')
1531N/A #
1531N/A # These lists are used to check if a 32/64-bit binary
1531N/A # is in a proper 32/64-bit directory.
1531N/A #
1531N/A self.pathlist32 = [
1531N/A "i86",
1531N/A "sparcv7",
1531N/A "32",
1531N/A "i86pc-solaris-64int", # perl path
2541N/A "sun4-solaris-64int", # perl path
2541N/A "i386-solaris" + solaris_ver, # ruby path
2541N/A "sparc-solaris" + solaris_ver # ruby path
1531N/A ]
1531N/A self.pathlist64 = [
1531N/A "amd64",
1531N/A "sparcv9",
1531N/A "64",
1531N/A "i86pc-solaris-64", # perl path
2541N/A "sun4-solaris-64", # perl path
2541N/A "amd64-solaris" + solaris_ver, # ruby path
2541N/A "sparcv9-solaris" + solaris_ver # ruby path
1531N/A ]
45N/A self.runpath_re = [
99N/A re.compile('^/lib(/.*)?$'),
45N/A re.compile('^/usr/'),
45N/A re.compile('^\$ORIGIN/')
45N/A ]
634N/A self.runpath_64_re = [
634N/A re.compile('^.*/64(/.*)?$'),
634N/A re.compile('^.*/amd64(/.*)?$'),
1531N/A re.compile('^.*/sparcv9(/.*)?$'),
1531N/A re.compile('^.*/i86pc-solaris-64(/.*)?$'), # perl path
2541N/A re.compile('^.*/sun4-solaris-64(/.*)?$'), # perl path
2541N/A re.compile('^.*/amd64-solaris2\.[0-9]+(/.*)?$'),
2541N/A # ruby path
2541N/A re.compile('^.*/sparcv9-solaris2\.[0-9]+(/.*)?$')
2541N/A # ruby path
634N/A ]
84N/A self.initscript_re = re.compile("^etc/(rc.|init)\.d")
186N/A
186N/A self.lint_paths = {}
186N/A self.ref_paths = {}
186N/A
45N/A super(UserlandActionChecker, self).__init__(config)
45N/A
186N/A def startup(self, engine):
186N/A """Initialize the checker with a dictionary of paths, so that we
186N/A can do link resolution.
186N/A
186N/A This is copied from the core pkglint code, but should eventually
186N/A be made common.
186N/A """
186N/A
186N/A def seed_dict(mf, attr, dic, atype=None, verbose=False):
186N/A """Updates a dictionary of { attr: [(fmri, action), ..]}
186N/A where attr is the value of that attribute from
186N/A actions of a given type atype, in the given
186N/A manifest."""
186N/A
186N/A pkg_vars = mf.get_all_variants()
186N/A
186N/A if atype:
186N/A mfg = (a for a in mf.gen_actions_by_type(atype))
186N/A else:
186N/A mfg = (a for a in mf.gen_actions())
186N/A
186N/A for action in mfg:
186N/A if atype and action.name != atype:
186N/A continue
186N/A if attr not in action.attrs:
186N/A continue
186N/A
186N/A variants = action.get_variant_template()
186N/A variants.merge_unknown(pkg_vars)
4304N/A # Action attributes must be lists or strings.
4304N/A for k, v in variants.iteritems():
4304N/A if isinstance(v, set):
4304N/A action.attrs[k] = list(v)
4304N/A else:
4304N/A action.attrs[k] = v
186N/A
186N/A p = action.attrs[attr]
186N/A dic.setdefault(p, []).append((mf.fmri, action))
186N/A
186N/A # construct a set of FMRIs being presented for linting, and
186N/A # avoid seeding the reference dictionary with any for which
186N/A # we're delivering new packages.
186N/A lint_fmris = {}
186N/A for m in engine.gen_manifests(engine.lint_api_inst,
186N/A release=engine.release, pattern=engine.pattern):
186N/A lint_fmris.setdefault(m.fmri.get_name(), []).append(m.fmri)
186N/A for m in engine.lint_manifests:
186N/A lint_fmris.setdefault(m.fmri.get_name(), []).append(m.fmri)
186N/A
186N/A engine.logger.debug(
186N/A _("Seeding reference action path dictionaries."))
186N/A
186N/A for manifest in engine.gen_manifests(engine.ref_api_inst,
186N/A release=engine.release):
186N/A # Only put this manifest into the reference dictionary
186N/A # if it's not an older version of the same package.
186N/A if not any(
186N/A lint_fmri_successor(fmri, manifest.fmri)
186N/A for fmri
186N/A in lint_fmris.get(manifest.fmri.get_name(), [])
186N/A ):
186N/A seed_dict(manifest, "path", self.ref_paths)
186N/A
186N/A engine.logger.debug(
186N/A _("Seeding lint action path dictionaries."))
186N/A
186N/A # we provide a search pattern, to allow users to lint a
186N/A # subset of the packages in the lint_repository
186N/A for manifest in engine.gen_manifests(engine.lint_api_inst,
186N/A release=engine.release, pattern=engine.pattern):
186N/A seed_dict(manifest, "path", self.lint_paths)
186N/A
186N/A engine.logger.debug(
186N/A _("Seeding local action path dictionaries."))
186N/A
186N/A for manifest in engine.lint_manifests:
186N/A seed_dict(manifest, "path", self.lint_paths)
186N/A
186N/A self.__merge_dict(self.lint_paths, self.ref_paths,
186N/A ignore_pubs=engine.ignore_pubs)
186N/A
186N/A def __merge_dict(self, src, target, ignore_pubs=True):
186N/A """Merges the given src dictionary into the target
186N/A dictionary, giving us the target content as it would appear,
186N/A were the packages in src to get published to the
186N/A repositories that made up target.
186N/A
186N/A We need to only merge packages at the same or successive
186N/A version from the src dictionary into the target dictionary.
186N/A If the src dictionary contains a package with no version
186N/A information, it is assumed to be more recent than the same
186N/A package with no version in the target."""
186N/A
186N/A for p in src:
186N/A if p not in target:
186N/A target[p] = src[p]
186N/A continue
186N/A
186N/A def build_dic(arr):
186N/A """Builds a dictionary of fmri:action entries"""
186N/A dic = {}
186N/A for (pfmri, action) in arr:
186N/A if pfmri in dic:
186N/A dic[pfmri].append(action)
186N/A else:
186N/A dic[pfmri] = [action]
186N/A return dic
186N/A
186N/A src_dic = build_dic(src[p])
186N/A targ_dic = build_dic(target[p])
186N/A
186N/A for src_pfmri in src_dic:
186N/A # we want to remove entries deemed older than
186N/A # src_pfmri from targ_dic.
186N/A for targ_pfmri in targ_dic.copy():
186N/A sname = src_pfmri.get_name()
186N/A tname = targ_pfmri.get_name()
186N/A if lint_fmri_successor(src_pfmri,
186N/A targ_pfmri,
186N/A ignore_pubs=ignore_pubs):
186N/A targ_dic.pop(targ_pfmri)
186N/A targ_dic.update(src_dic)
186N/A l = []
186N/A for pfmri in targ_dic:
186N/A for action in targ_dic[pfmri]:
186N/A l.append((pfmri, action))
186N/A target[p] = l
84N/A
84N/A def __realpath(self, path, target):
84N/A """Combine path and target to get the real path."""
84N/A
84N/A result = os.path.dirname(path)
84N/A
84N/A for frag in target.split(os.sep):
84N/A if frag == '..':
84N/A result = os.path.dirname(result)
84N/A elif frag == '.':
84N/A pass
84N/A else:
84N/A result = os.path.join(result, frag)
84N/A
84N/A return result
84N/A
2337N/A def __elf_aslr_check(self, path, engine):
2337N/A result = None
2337N/A
2337N/A ei = elf.get_info(path)
2337N/A type = ei.get("type");
2337N/A if type != "exe":
2337N/A return result
2337N/A
2337N/A # get the ASLR tag string for this binary
2337N/A aslr_tag_process = subprocess.Popen(
2337N/A "/usr/bin/elfedit -r -e 'dyn:sunw_aslr' "
2337N/A + path, shell=True,
2337N/A stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2337N/A
2337N/A # aslr_tag_string will get stdout; err will get stderr
2337N/A aslr_tag_string, err = aslr_tag_process.communicate()
2337N/A
2337N/A # No ASLR tag was found; everything must be tagged
2337N/A if aslr_tag_process.returncode != 0:
2337N/A engine.error(
2337N/A _("'%s' is not tagged for aslr") % (path),
2337N/A msgid="%s%s.5" % (self.name, "001"))
2337N/A return result
2337N/A
2337N/A # look for "ENABLE" anywhere in the string;
2337N/A # warn about binaries which are not ASLR enabled
2337N/A if re.search("ENABLE", aslr_tag_string) is not None:
2337N/A return result
2337N/A engine.warning(
2337N/A _("'%s' does not have aslr enabled") % (path),
2337N/A msgid="%s%s.6" % (self.name, "001"))
2337N/A return result
2337N/A
634N/A def __elf_runpath_check(self, path, engine):
84N/A result = None
84N/A list = []
84N/A
84N/A ed = elf.get_dynamic(path)
634N/A ei = elf.get_info(path)
634N/A bits = ei.get("bits")
84N/A for dir in ed.get("runpath", "").split(":"):
84N/A if dir == None or dir == '':
84N/A continue
45N/A
84N/A match = False
84N/A for expr in self.runpath_re:
84N/A if expr.match(dir):
84N/A match = True
84N/A break
84N/A
84N/A if match == False:
84N/A list.append(dir)
2498N/A # Make sure RUNPATH matches against a packaged path.
2498N/A # Don't check runpaths starting with $ORIGIN, which
2498N/A # is specially handled by the linker.
2498N/A
2498N/A elif not dir.startswith('$ORIGIN/'):
2498N/A
2498N/A # Strip out leading and trailing '/' in the
2498N/A # runpath, since the reference paths don't start
2498N/A # with '/' and trailing '/' could cause mismatches.
2498N/A # Check first if there is an exact match, then check
2498N/A # if any reference path starts with this runpath
2498N/A # plus a trailing slash, since it may still be a link
2498N/A # to a directory that has no action because it uses
2498N/A # the default attributes.
2498N/A
2498N/A relative_dir = dir.strip('/')
2498N/A if not relative_dir in self.ref_paths and \
2498N/A not any(key.startswith(relative_dir + '/')
2498N/A for key in self.ref_paths):
2498N/A
2498N/A # If still no match, if the runpath contains
2498N/A # an embedded symlink, emit a warning; it may or may
2498N/A # not resolve to a legitimate path.
2498N/A # E.g., for usr/openwin/lib, usr/openwin->X11 and
2498N/A # usr/X11/lib are packaged, but usr/openwin/lib is not.
2498N/A # Otherwise, runpath is bad; add it to list.
2498N/A embedded_link = False
2498N/A pdir = os.path.dirname(relative_dir)
2498N/A while pdir != '':
2498N/A if (pdir in self.ref_paths and
2498N/A self.ref_paths[pdir][0][1].name == "link"):
2498N/A embedded_link = True
2498N/A engine.warning(
2498N/A _("runpath '%s' in '%s' not found in reference paths but contains symlink at '%s'") % (dir, path, pdir),
2498N/A msgid="%s%s.3" % (self.name, "001"))
2498N/A break
2498N/A pdir = os.path.dirname(pdir)
2498N/A if not embedded_link:
2498N/A list.append(dir)
84N/A
634N/A if bits == 32:
634N/A for expr in self.runpath_64_re:
634N/A if expr.search(dir):
634N/A engine.warning(
634N/A _("64-bit runpath in 32-bit binary, '%s' includes '%s'") % (path, dir),
634N/A msgid="%s%s.3" % (self.name, "001"))
634N/A else:
634N/A match = False
634N/A for expr in self.runpath_64_re:
634N/A if expr.search(dir):
634N/A match = True
634N/A break
634N/A if match == False:
634N/A engine.warning(
634N/A _("32-bit runpath in 64-bit binary, '%s' includes '%s'") % (path, dir),
634N/A msgid="%s%s.3" % (self.name, "001"))
84N/A if len(list) > 0:
84N/A result = _("bad RUNPATH, '%%s' includes '%s'" %
84N/A ":".join(list))
84N/A
84N/A return result
84N/A
2140N/A def __elf_wrong_location_check(self, path, inspath):
84N/A result = None
84N/A
84N/A ei = elf.get_info(path)
84N/A bits = ei.get("bits")
495N/A type = ei.get("type");
2140N/A elems = os.path.dirname(inspath).split("/")
84N/A
1531N/A path64 = False
1531N/A for p in self.pathlist64:
1531N/A if (p in elems):
1531N/A path64 = True
168N/A
1531N/A path32 = False
1531N/A for p in self.pathlist32:
1531N/A if (p in elems):
1531N/A path32 = True
495N/A
495N/A # ignore 64-bit executables in normal (non-32-bit-specific)
495N/A # locations, that's ok now.
495N/A if (type == "exe" and bits == 64 and path32 == False and path64 == False):
495N/A return result
495N/A
168N/A if bits == 32 and path64:
84N/A result = _("32-bit object '%s' in 64-bit path")
168N/A elif bits == 64 and not path64:
84N/A result = _("64-bit object '%s' in 32-bit path")
84N/A return result
84N/A
84N/A def file_action(self, action, manifest, engine, pkglint_id="001"):
45N/A """Checks for existence in the proto area."""
45N/A
84N/A if action.name not in ["file"]:
45N/A return
45N/A
2140N/A inspath=action.attrs["path"]
2140N/A
117N/A path = action.hash
117N/A if path == None or path == 'NOHASH':
2140N/A path = inspath
84N/A
84N/A # check for writable files without a preserve attribute
145N/A if "mode" in action.attrs:
84N/A mode = action.attrs["mode"]
84N/A
84N/A if (int(mode, 8) & 0222) != 0 and "preserve" not in action.attrs:
84N/A engine.error(
84N/A _("%(path)s is writable (%(mode)s), but missing a preserve"
84N/A " attribute") % {"path": path, "mode": mode},
84N/A msgid="%s%s.0" % (self.name, pkglint_id))
145N/A elif "preserve" in action.attrs:
145N/A if "mode" in action.attrs:
145N/A mode = action.attrs["mode"]
145N/A if (int(mode, 8) & 0222) == 0:
145N/A engine.error(
145N/A _("%(path)s has a preserve action, but is not writable (%(mode)s)") % {"path": path, "mode": mode},
145N/A msgid="%s%s.4" % (self.name, pkglint_id))
145N/A else:
145N/A engine.error(
145N/A _("%(path)s has a preserve action, but no mode") % {"path": path, "mode": mode},
145N/A msgid="%s%s.3" % (self.name, pkglint_id))
84N/A
84N/A # checks that require a physical file to look at
117N/A if self.proto_path is not None:
117N/A for directory in self.proto_path:
117N/A fullpath = directory + "/" + path
117N/A
117N/A if os.path.exists(fullpath):
117N/A break
84N/A
84N/A if not os.path.exists(fullpath):
84N/A engine.info(
84N/A _("%s missing from proto area, skipping"
84N/A " content checks") % path,
84N/A msgid="%s%s.1" % (self.name, pkglint_id))
84N/A elif elf.is_elf_object(fullpath):
84N/A # 32/64 bit in wrong place
2140N/A result = self.__elf_wrong_location_check(fullpath, inspath)
84N/A if result != None:
2140N/A engine.error(result % inspath,
84N/A msgid="%s%s.2" % (self.name, pkglint_id))
634N/A result = self.__elf_runpath_check(fullpath, engine)
84N/A if result != None:
84N/A engine.error(result % path,
84N/A msgid="%s%s.3" % (self.name, pkglint_id))
2337N/A result = self.__elf_aslr_check(fullpath, engine)
84N/A
84N/A file_action.pkglint_desc = _("Paths should exist in the proto area.")
84N/A
84N/A def link_resolves(self, action, manifest, engine, pkglint_id="002"):
84N/A """Checks for link resolution."""
84N/A
84N/A if action.name not in ["link", "hardlink"]:
84N/A return
84N/A
84N/A path = action.attrs["path"]
84N/A target = action.attrs["target"]
84N/A realtarget = self.__realpath(path, target)
84N/A
186N/A # Check against the target image (ref_paths), since links might
186N/A # resolve outside the packages delivering a particular
186N/A # component.
1923N/A
1923N/A # links to files should directly match a patch in the reference
1923N/A # repo.
1923N/A if self.ref_paths.get(realtarget, None):
1923N/A return
1923N/A
1923N/A # If it didn't match a path in the reference repo, it may still
1923N/A # be a link to a directory that has no action because it uses
1923N/A # the default attributes. Look for a path that starts with
1923N/A # this value plus a trailing slash to be sure this it will be
1923N/A # resolvable on a fully installed system.
1923N/A realtarget += '/'
1923N/A for key in self.ref_paths:
1923N/A if key.startswith(realtarget):
1923N/A return
1923N/A
1923N/A engine.error(_("%s %s has unresolvable target '%s'") %
1923N/A (action.name, path, target),
1923N/A msgid="%s%s.0" % (self.name, pkglint_id))
84N/A
84N/A link_resolves.pkglint_desc = _("links should resolve.")
84N/A
84N/A def init_script(self, action, manifest, engine, pkglint_id="003"):
84N/A """Checks for SVR4 startup scripts."""
84N/A
45N/A if action.name not in ["file", "dir", "link", "hardlink"]:
45N/A return
45N/A
45N/A path = action.attrs["path"]
84N/A if self.initscript_re.match(path):
84N/A engine.warning(
84N/A _("SVR4 startup '%s', deliver SMF"
84N/A " service instead") % path,
45N/A msgid="%s%s.0" % (self.name, pkglint_id))
45N/A
84N/A init_script.pkglint_desc = _(
84N/A "SVR4 startup scripts should not be delivered.")
45N/A
45N/Aclass UserlandManifestChecker(base.ManifestChecker):
45N/A """An opensolaris.org-specific class to check manifests."""
45N/A
45N/A name = "userland.manifest"
45N/A
45N/A def __init__(self, config):
45N/A super(UserlandManifestChecker, self).__init__(config)
45N/A
181N/A def component_check(self, manifest, engine, pkglint_id="001"):
84N/A manifest_paths = []
84N/A files = False
84N/A license = False
84N/A
84N/A for action in manifest.gen_actions_by_type("file"):
84N/A files = True
84N/A break
84N/A
84N/A if files == False:
45N/A return
45N/A
84N/A for action in manifest.gen_actions_by_type("license"):
181N/A license = True
181N/A break
181N/A
181N/A if license == False:
181N/A engine.error( _("missing license action"),
181N/A msgid="%s%s.0" % (self.name, pkglint_id))
45N/A
464N/A if 'org.opensolaris.arc-caseid' not in manifest:
464N/A engine.error( _("missing ARC data (org.opensolaris.arc-caseid)"),
181N/A msgid="%s%s.0" % (self.name, pkglint_id))
45N/A
2756N/A component_check.pkglint_desc = _(
464N/A "license actions and ARC information are required if you deliver files.")
2756N/A
2756N/A # CFFI names the modules it creates with a hash that includes the
2756N/A # version of CFFI (since the schema may change from one version to
2756N/A # another). This means that if a package depends on CFFI, then it must
2756N/A # also incorporate the version it builds with, which should be the
2756N/A # version in the gate.
2756N/A def uses_cffi(self, manifest, engine, pkglint_id="003"):
2756N/A cffi_match = {"fmri": "*/cffi*"}
2756N/A cffi_require = None
2756N/A cffi_incorp = None
2756N/A for action in manifest.gen_actions_by_type("depend"):
2756N/A if not any(
2756N/A f
2756N/A for f in action.attrlist("fmri")
2756N/A if "/cffi-" in pkg.fmri.PkgFmri(f, "5.11").pkg_name):
2756N/A continue
2756N/A if action.attrs["type"] in ("require", "require-any"):
2756N/A cffi_require = action
2756N/A elif action.attrs["type"] == "incorporate":
2756N/A cffi_incorp = action
2756N/A
2756N/A try:
2756N/A sys.path[0:0] = [os.path.join(os.getenv("WS_TOP", ""),
2756N/A "components/python/cffi/build/prototype/"
2756N/A "%s/usr/lib/python%d.%d/vendor-packages" %
2756N/A ((platform.processor(),) + sys.version_info[:2]))]
2756N/A import cffi
2756N/A cffi_version = cffi.__version__
2756N/A del sys.path[0]
2756N/A except ImportError:
2756N/A cffi_version = None
2756N/A
2756N/A if not cffi_require:
2756N/A return
2756N/A
2756N/A if not cffi_version:
2756N/A engine.warning(_("package %s depends on CFFI, but we "
2756N/A "cannot determine the version of CFFI needed") %
2756N/A manifest.fmri,
2756N/A msgid="%s%s.1" % (self.name, pkglint_id))
2756N/A
2756N/A if not cffi_incorp:
2756N/A engine.error(_("package %(pkg)s depends on CFFI, but "
2756N/A "does not incorporate it (should be at %(should)s)")
2756N/A % {"pkg": manifest.fmri, "should": cffi_version},
2756N/A msgid="%s%s.2" % (self.name, pkglint_id))
2756N/A
2756N/A # The final check can only be done if neither of the previous
2756N/A # checks have fired.
2756N/A if not cffi_version or not cffi_incorp:
2756N/A return
2756N/A
2756N/A cffi_incorp_ver = str(pkg.fmri.PkgFmri(
2756N/A cffi_incorp.attrs["fmri"], "5.11").version.release)
2756N/A if cffi_incorp_ver != cffi_version:
2756N/A engine.error(_("package %(pkg)s depends on CFFI, but "
2756N/A "incorporates it at the wrong version (%(actual)s "
2756N/A "instead of %(should)s)") % {"pkg": manifest.fmri,
2756N/A "actual": cffi_incorp_ver, "should": cffi_version},
2756N/A msgid="%s%s.3" % (self.name, pkglint_id))
2756N/A
2756N/A uses_cffi.pkglint_desc = _(
2756N/A "Packages using CFFI incorporate CFFI at the correct version.")