1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import grp
6import json
7import logging
8import pwd
9import os
10import stat
11
12from autotest_lib.client.bin import test, utils
13from autotest_lib.client.common_lib import error
14
15
16class security_RootfsStatefulSymlinks(test.test):
17    version = 1
18    _BAD_DESTINATIONS = [
19                         '*/var/*', '*/home/*', '*/stateful_partition/*',
20                         '*/usr/local/*'
21                        ]
22
23    def load_baseline(self):
24        bfile = open(os.path.join(self.bindir, 'baseline'))
25        baseline = json.loads(bfile.read())
26        bfile.close()
27        return baseline
28
29
30    def validate_attributes(self, link, expectations):
31        """
32        Given a symlink, validate that the file it points to
33        matches all of the expected properties (owner, group, mode).
34        Returns True if all expections are met, False otherwise.
35        """
36        destination = os.readlink(link)
37        if destination != expectations['destination']:
38            logging.error(
39                "Expected '%s' to point to '%s', but it points to '%s'",
40                link, expectations['destination'], destination)
41            logging.error(utils.system_output("ls -ald '%s'" % destination))
42            return False
43
44        # By this point, we know it points to the right place, but we
45        # need to determine if the destination exists (and, if not, if
46        # that's permitted by "can_dangle": true in the baseline.
47        if not os.path.exists(destination):
48            logging.warning("'%s' points to '%s', but it's dangling",
49                            link, destination)
50            return expectations['can_dangle']
51
52        # It exists, it's the right path, so check the permissions.
53        s = os.stat(destination)
54        owner = pwd.getpwuid(s.st_uid).pw_name
55        group = grp.getgrgid(s.st_gid).gr_name
56        mode = oct(stat.S_IMODE(s.st_mode))
57        if (owner == expectations['owner'] and
58            group == expectations['group'] and
59            mode == expectations['mode']):
60            return True
61        else:
62            logging.error("'%s': expected %s:%s %s, saw %s:%s %s",
63                          destination, expectations['owner'],
64                          expectations['group'], expectations['mode'],
65                          owner, group, mode)
66            return False
67
68
69    def run_once(self):
70        """
71        Find any symlinks that point from the rootfs into
72        "bad destinations" (e.g., stateful partition). Validate
73        that any approved cases meet with all expectations, and
74        that there are no unexpected additional such links found.
75        """
76        baseline = self.load_baseline()
77        test_pass = True
78
79        clauses = ["-lname '%s'" % i for i in self._BAD_DESTINATIONS]
80        cmd = 'find / -xdev %s' % ' -o '.join(clauses)
81        cmd_output = utils.system_output(cmd, ignore_status=True)
82
83        links_seen = set(cmd_output.splitlines())
84        for link in links_seen:
85            # Check if this link is in the baseline. If not, test fails.
86            if not link in baseline:
87                logging.error("No baseline entry for '%s'", link)
88                logging.error(utils.system_output("ls -ald '%s'" % link))
89                test_pass = False
90                continue
91            # If it is, proceed to validate other attributes (where it points,
92            # permissions of what it points to, etc).
93            file_pass = self.validate_attributes(link, baseline[link])
94            test_pass = test_pass and file_pass
95
96        # The above will have flagged any links for which we had no baseline.
97        # Warn (but do not trigger failure) when we have baseline entries
98        # which we did not find on the system.
99        expected_set = set(baseline.keys())
100        diff = expected_set.difference(links_seen)
101        if diff:
102            logging.warning("Warning, possible stale baseline entries:")
103            for d in diff:
104                logging.warning(d)
105
106        if not test_pass:
107            raise error.TestFail("Baseline mismatch")
108
109