Alexander Pyhalov
2014-08-08 6185b3543717666197bc743a5dd2558e67eead85
commit | author | age
f811d2 1 #!/usr/bin/python
NJ 2 #
3 # CDDL HEADER START
4 #
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
8 #
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 # or http://www.opensolaris.org/os/licensing.
11 # See the License for the specific language governing permissions
12 # and limitations under the License.
13 #
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 #
20 # CDDL HEADER END
21 #
22
23 #
0f1b63 24 # Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
f811d2 25 #
NJ 26
27 # Some userland consolidation specific lint checks
28
29 import pkg.lint.base as base
5572ad 30 from pkg.lint.engine import lint_fmri_successor
6185b3 31 import pkg.fmri
694ef3 32 import pkg.elf as elf
f811d2 33 import re
NJ 34 import os.path
0f1b63 35 import subprocess
6185b3 36 import pkg.client.api
AP 37 import pkg.client.api_errors
38 import pkg.client.progress
f811d2 39
NJ 40 class UserlandActionChecker(base.ActionChecker):
41         """An opensolaris.org-specific class to check actions."""
42
43         name = "userland.action"
44
45         def __init__(self, config):
46                 self.description = _(
47                     "checks Userland packages for common content errors")
8a6659 48         path = os.getenv('PROTO_PATH')
NJ 49         if path != None:
50             self.proto_path = path.split()
51         else:
52             self.proto_path = None
c26e53 53         #
CM 54         # These lists are used to check if a 32/64-bit binary
55         # is in a proper 32/64-bit directory.
56         #
57         self.pathlist32 = [
58             "i86",
59             "sparcv7",
60             "32",
61             "i86pc-solaris-64int",  # perl path
62             "sun4-solaris-64int"    # perl path
63         ]
64         self.pathlist64 = [
65             "amd64",
66             "sparcv9",
67             "64",
68             "i86pc-solaris-64",     # perl path
69             "sun4-solaris-64"       # perl path
70         ]
f811d2 71         self.runpath_re = [
35a012 72             re.compile('^/lib(/.*)?$'),
f811d2 73             re.compile('^/usr/'),
NJ 74             re.compile('^\$ORIGIN/')
75         ]
048c87 76         self.runpath_64_re = [
MS 77             re.compile('^.*/64(/.*)?$'),
78             re.compile('^.*/amd64(/.*)?$'),
c26e53 79             re.compile('^.*/sparcv9(/.*)?$'),
CM 80             re.compile('^.*/i86pc-solaris-64(/.*)?$'), # perl path
81             re.compile('^.*/sun4-solaris-64(/.*)?$')   # perl path
048c87 82         ]
694ef3 83         self.initscript_re = re.compile("^etc/(rc.|init)\.d")
5572ad 84
DD 85                 self.lint_paths = {}
86                 self.ref_paths = {}
87
f811d2 88                 super(UserlandActionChecker, self).__init__(config)
NJ 89
5572ad 90         def startup(self, engine):
DD 91                 """Initialize the checker with a dictionary of paths, so that we
92                 can do link resolution.
93
94                 This is copied from the core pkglint code, but should eventually
95                 be made common.
96                 """
97
98                 def seed_dict(mf, attr, dic, atype=None, verbose=False):
99                         """Updates a dictionary of { attr: [(fmri, action), ..]}
100                         where attr is the value of that attribute from
101                         actions of a given type atype, in the given
102                         manifest."""
103
104                         pkg_vars = mf.get_all_variants()
105
106                         if atype:
107                                 mfg = (a for a in mf.gen_actions_by_type(atype))
108                         else:
109                                 mfg = (a for a in mf.gen_actions())
110
111                         for action in mfg:
112                                 if atype and action.name != atype:
113                                         continue
114                                 if attr not in action.attrs:
115                                         continue
116
117                                 variants = action.get_variant_template()
118                                 variants.merge_unknown(pkg_vars)
119                                 action.attrs.update(variants)
120
121                                 p = action.attrs[attr]
122                                 dic.setdefault(p, []).append((mf.fmri, action))
123
124                 # construct a set of FMRIs being presented for linting, and
125                 # avoid seeding the reference dictionary with any for which
126                 # we're delivering new packages.
127                 lint_fmris = {}
128                 for m in engine.gen_manifests(engine.lint_api_inst,
129                     release=engine.release, pattern=engine.pattern):
130                         lint_fmris.setdefault(m.fmri.get_name(), []).append(m.fmri)
131                 for m in engine.lint_manifests:
132                         lint_fmris.setdefault(m.fmri.get_name(), []).append(m.fmri)
133
134                 engine.logger.debug(
135                     _("Seeding reference action path dictionaries."))
136
137                 for manifest in engine.gen_manifests(engine.ref_api_inst,
138                     release=engine.release):
139                         # Only put this manifest into the reference dictionary
140                         # if it's not an older version of the same package.
141                         if not any(
142                             lint_fmri_successor(fmri, manifest.fmri)
143                             for fmri
144                             in lint_fmris.get(manifest.fmri.get_name(), [])
145                         ):
146                                 seed_dict(manifest, "path", self.ref_paths)
147
148                 engine.logger.debug(
149                     _("Seeding lint action path dictionaries."))
150
151                 # we provide a search pattern, to allow users to lint a
152                 # subset of the packages in the lint_repository
153                 for manifest in engine.gen_manifests(engine.lint_api_inst,
154                     release=engine.release, pattern=engine.pattern):
155                         seed_dict(manifest, "path", self.lint_paths)
156
157                 engine.logger.debug(
158                     _("Seeding local action path dictionaries."))
159
160                 for manifest in engine.lint_manifests:
161                         seed_dict(manifest, "path", self.lint_paths)
162
163                 self.__merge_dict(self.lint_paths, self.ref_paths,
164                     ignore_pubs=engine.ignore_pubs)
165
166         def __merge_dict(self, src, target, ignore_pubs=True):
167                 """Merges the given src dictionary into the target
168                 dictionary, giving us the target content as it would appear,
169                 were the packages in src to get published to the
170                 repositories that made up target.
171
172                 We need to only merge packages at the same or successive
173                 version from the src dictionary into the target dictionary.
174                 If the src dictionary contains a package with no version
175                 information, it is assumed to be more recent than the same
176                 package with no version in the target."""
177
178                 for p in src:
179                         if p not in target:
180                                 target[p] = src[p]
181                                 continue
182
183                         def build_dic(arr):
184                                 """Builds a dictionary of fmri:action entries"""
185                                 dic = {}
186                                 for (pfmri, action) in arr:
187                                         if pfmri in dic:
188                                                 dic[pfmri].append(action)
189                                         else:
190                                                 dic[pfmri] = [action]
191                                 return dic
192
193                         src_dic = build_dic(src[p])
194                         targ_dic = build_dic(target[p])
195
196                         for src_pfmri in src_dic:
197                                 # we want to remove entries deemed older than
198                                 # src_pfmri from targ_dic.
199                                 for targ_pfmri in targ_dic.copy():
200                                         sname = src_pfmri.get_name()
201                                         tname = targ_pfmri.get_name()
202                                         if lint_fmri_successor(src_pfmri,
203                                             targ_pfmri,
204                                             ignore_pubs=ignore_pubs):
205                                                 targ_dic.pop(targ_pfmri)
206                         targ_dic.update(src_dic)
207                         l = []
208                         for pfmri in targ_dic:
209                                 for action in targ_dic[pfmri]:
210                                         l.append((pfmri, action))
211                         target[p] = l
f811d2 212
694ef3 213         def __realpath(self, path, target):
NJ 214         """Combine path and target to get the real path."""
215
216         result = os.path.dirname(path)
217
218         for frag in target.split(os.sep):
219             if frag == '..':
220                 result = os.path.dirname(result)
221             elif frag == '.':
222                 pass
223             else:
224                 result = os.path.join(result, frag)
225
226         return result
227
0f1b63 228     def __elf_aslr_check(self, path, engine):
AC 229         result = None
230
231         ei = elf.get_info(path)
232         type = ei.get("type");
233         if type != "exe":
234             return result
235
236         # get the ASLR tag string for this binary
237         aslr_tag_process = subprocess.Popen(
238             "/usr/bin/elfedit -r -e 'dyn:sunw_aslr' "
239             + path, shell=True,
240             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
241
242         # aslr_tag_string will get stdout; err will get stderr
243         aslr_tag_string, err = aslr_tag_process.communicate()
244
245         # No ASLR tag was found; everthing must be tagged
246         if aslr_tag_process.returncode != 0:
247             engine.error(
248                 _("'%s' is not tagged for aslr") % (path),
249                 msgid="%s%s.5" % (self.name, "001"))
250             return result
251
252         # look for "ENABLE" anywhere in the string;
253         # warn about binaries which are not ASLR enabled
254         if re.search("ENABLE", aslr_tag_string) is not None:
255             return result
256         engine.warning(
257             _("'%s' does not have aslr enabled") % (path),
258             msgid="%s%s.6" % (self.name, "001"))
259         return result
260
048c87 261     def __elf_runpath_check(self, path, engine):
694ef3 262         result = None
NJ 263         list = []
264
265         ed = elf.get_dynamic(path)
048c87 266         ei = elf.get_info(path)
MS 267         bits = ei.get("bits")
694ef3 268         for dir in ed.get("runpath", "").split(":"):
NJ 269             if dir == None or dir == '':
270                 continue
271
272             match = False
273             for expr in self.runpath_re:
274                 if expr.match(dir):
275                     match = True
276                     break
277
278             if match == False:
279                 list.append(dir)
280
048c87 281             if bits == 32:
MS 282                 for expr in self.runpath_64_re:
283                     if expr.search(dir):
284                         engine.warning(
285                             _("64-bit runpath in 32-bit binary, '%s' includes '%s'") % (path, dir),
286                             msgid="%s%s.3" % (self.name, "001"))
287             else:
288                 match = False
289                 for expr in self.runpath_64_re:
290                     if expr.search(dir):
291                         match = True
292                         break
293                 if match == False:
294                     engine.warning(
295                         _("32-bit runpath in 64-bit binary, '%s' includes '%s'") % (path, dir),
296                         msgid="%s%s.3" % (self.name, "001"))
694ef3 297         if len(list) > 0:
NJ 298             result = _("bad RUNPATH, '%%s' includes '%s'" %
299                    ":".join(list))
300
301         return result
302
303     def __elf_wrong_location_check(self, path):
304         result = None
305
306         ei = elf.get_info(path)
307         bits = ei.get("bits")
cc2735 308         type = ei.get("type");
fe3c1d 309                 elems = os.path.dirname(path).split("/")
694ef3 310
c26e53 311                 path64 = False
CM 312         for p in self.pathlist64:
313             if (p in elems):
314                 path64 = True
fe3c1d 315
c26e53 316         path32 = False
CM 317         for p in self.pathlist32:
318             if (p in elems):
319                 path32 = True
cc2735 320
MS 321         # ignore 64-bit executables in normal (non-32-bit-specific)
322         # locations, that's ok now.
323         if (type == "exe" and bits == 64 and path32 == False and path64 == False):
324             return result
325
fe3c1d 326         if bits == 32 and path64:
694ef3 327             result = _("32-bit object '%s' in 64-bit path")
fe3c1d 328         elif bits == 64 and not path64:
694ef3 329             result = _("64-bit object '%s' in 32-bit path")
NJ 330         return result
331
332     def file_action(self, action, manifest, engine, pkglint_id="001"):
f811d2 333         """Checks for existence in the proto area."""
NJ 334
694ef3 335         if action.name not in ["file"]:
f811d2 336             return
694ef3 337
8a6659 338         path = action.hash
NJ 339         if path == None or path == 'NOHASH':
340             path = action.attrs["path"]
694ef3 341
NJ 342         # check for writable files without a preserve attribute
d05a6f 343         if "mode" in action.attrs:
694ef3 344             mode = action.attrs["mode"]
NJ 345
346             if (int(mode, 8) & 0222) != 0 and "preserve" not in action.attrs:
347                 engine.error(
348                 _("%(path)s is writable (%(mode)s), but missing a preserve"
349                   " attribute") %  {"path": path, "mode": mode},
350                 msgid="%s%s.0" % (self.name, pkglint_id))
d05a6f 351         elif "preserve" in action.attrs:
NJ 352             if "mode" in action.attrs:
353                 mode = action.attrs["mode"]
354                 if (int(mode, 8) & 0222) == 0:
355                     engine.error(
356                     _("%(path)s has a preserve action, but is not writable (%(mode)s)") %  {"path": path, "mode": mode},
357                 msgid="%s%s.4" % (self.name, pkglint_id))
358             else:
359                 engine.error(
360                 _("%(path)s has a preserve action, but no mode") %  {"path": path, "mode": mode},
361                 msgid="%s%s.3" % (self.name, pkglint_id))
694ef3 362
NJ 363         # checks that require a physical file to look at
8a6659 364         if self.proto_path is not None:
NJ 365             for directory in self.proto_path:
366                 fullpath = directory + "/" + path
367
368                 if os.path.exists(fullpath):
369                     break
694ef3 370
NJ 371             if not os.path.exists(fullpath):
372                 engine.info(
373                     _("%s missing from proto area, skipping"
374                       " content checks") % path, 
375                     msgid="%s%s.1" % (self.name, pkglint_id))
376             elif elf.is_elf_object(fullpath):
377                 # 32/64 bit in wrong place
378                 result = self.__elf_wrong_location_check(fullpath)
379                 if result != None:
380                     engine.error(result % path, 
381                         msgid="%s%s.2" % (self.name, pkglint_id))
048c87 382                 result = self.__elf_runpath_check(fullpath, engine)
694ef3 383                 if result != None:
NJ 384                     engine.error(result % path, 
385                         msgid="%s%s.3" % (self.name, pkglint_id))
eb1757 386                 # illumos does not support ASLR
AS 387                 #result = self.__elf_aslr_check(fullpath, engine)
694ef3 388
NJ 389     file_action.pkglint_desc = _("Paths should exist in the proto area.")
390
391     def link_resolves(self, action, manifest, engine, pkglint_id="002"):
392         """Checks for link resolution."""
393
394         if action.name not in ["link", "hardlink"]:
395             return
396
397         path = action.attrs["path"]
398         target = action.attrs["target"]
399         realtarget = self.__realpath(path, target)
400
5572ad 401         # Check against the target image (ref_paths), since links might
DD 402         # resolve outside the packages delivering a particular
403         # component.
4e1ae0 404
NJ 405         # links to files should directly match a patch in the reference
406         # repo.
407         if self.ref_paths.get(realtarget, None):
408             return
409
410         # If it didn't match a path in the reference repo, it may still
411         # be a link to a directory that has no action because it uses
412         # the default attributes.  Look for a path that starts with
413         # this value plus a trailing slash to be sure this it will be
414         # resolvable on a fully installed system.
415         realtarget += '/'
416         for key in self.ref_paths:
417             if key.startswith(realtarget):
418                 return
419
420         engine.error(_("%s %s has unresolvable target '%s'") %
421                 (action.name, path, target),
422             msgid="%s%s.0" % (self.name, pkglint_id))
694ef3 423
NJ 424     link_resolves.pkglint_desc = _("links should resolve.")
425
426     def init_script(self, action, manifest, engine, pkglint_id="003"):
427         """Checks for SVR4 startup scripts."""
f811d2 428
NJ 429         if action.name not in ["file", "dir", "link", "hardlink"]:
430             return
431
432         path = action.attrs["path"]
694ef3 433         if self.initscript_re.match(path):
NJ 434             engine.warning(
435                 _("SVR4 startup '%s', deliver SMF"
436                   " service instead") % path,
f811d2 437                 msgid="%s%s.0" % (self.name, pkglint_id))
NJ 438
694ef3 439     init_script.pkglint_desc = _(
NJ 440         "SVR4 startup scripts should not be delivered.")
f811d2 441
NJ 442 class UserlandManifestChecker(base.ManifestChecker):
443         """An opensolaris.org-specific class to check manifests."""
444
445         name = "userland.manifest"
446
447     def __init__(self, config):
448         super(UserlandManifestChecker, self).__init__(config)
449
6185b3 450     def forbidden_publisher(self, manifest, engine, pkglint_id="1001"):
AP 451         if not os.environ.get("ENCUMBERED"):
452             for action in manifest.gen_actions_by_type("depend"):
453                 for f in action.attrlist("fmri"):
454                     pkg_name=pkg.fmri.PkgFmri(f).pkg_name
455                     info_needed = pkg.client.api.PackageInfo.ALL_OPTIONS - \
456                                 (pkg.client.api.PackageInfo.ACTION_OPTIONS |
457                          frozenset([pkg.client.api.PackageInfo.LICENSES]))
458                     progtracker = pkg.client.progress.NullProgressTracker()
459                     interface=pkg.client.api.ImageInterface("/", pkg.client.api.CURRENT_API_VERSION, progtracker, lambda x: False, None,None)
460                     ret = interface.info([pkg_name],True,info_needed)
461                     if ret[pkg.client.api.ImageInterface.INFO_FOUND]:
462                             for i in ret[pkg.client.api.ImageInterface.INFO_FOUND]:
463                             if i.publisher not in ("openindiana.org","userland","on-nightly"):
464                                 engine.error(_("package %(pkg)s depends on %(name)s, which comes from forbidden publisher %(publisher)s") %
465                                     {"pkg":manifest.fmri,"name":pkg_name,"publisher":i.publisher}, msgid="%s%s.1" % (self.name, pkglint_id))
466
467     forbidden_publisher.pkglint_dest = _(
468         "Dependencies should come from standard publishers" )
469
4158c0 470     def component_check(self, manifest, engine, pkglint_id="001"):
694ef3 471         manifest_paths = []
NJ 472         files = False
473         license = False
474
475         for action in manifest.gen_actions_by_type("file"):
476             files = True
477             break
478
479         if files == False:
f811d2 480             return
NJ 481
694ef3 482         for action in manifest.gen_actions_by_type("license"):
4158c0 483             license = True
NJ 484             break
f811d2 485
4158c0 486         if license == False:
NJ 487             engine.error( _("missing license action"),
488                 msgid="%s%s.0" % (self.name, pkglint_id))
f811d2 489
ecc890 490 #        if 'org.opensolaris.arc-caseid' not in manifest:
AS 491 #            engine.error( _("missing ARC data (org.opensolaris.arc-caseid)"),
492 #                msgid="%s%s.0" % (self.name, pkglint_id))
4158c0 493
NJ 494     component_check.pkglint_dest = _(
1de4f0 495         "license actions and ARC information are required if you deliver files.")
34ca1f 496
RB 497         def publisher_in_fmri(self, manifest, engine, pkglint_id="002"):
498                 lint_id = "%s%s" % (self.name, pkglint_id)
499                 allowed_pubs = engine.get_param(
500                     "%s.allowed_pubs" % lint_id).split(" ") 
501
502                 fmri = manifest.fmri
503                 if fmri.publisher and fmri.publisher not in allowed_pubs:
504                         engine.error(_("package %s has a publisher set!") %
505                             manifest.fmri,
506                             msgid="%s%s.2" % (self.name, pkglint_id))