1# Copyright 2017 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 glob
6import logging
7import os
8import tempfile
9
10from autotest_lib.client.bin import test
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib import utils
13
14PARTITION_TEST_PATH = 'platform_SecureEraseFile_test_file'
15TEST_PATH = '/mnt/stateful_partition/' + PARTITION_TEST_PATH
16BINARY = '/usr/bin/secure_erase_file'
17DEVNAME_PREFIX = 'DEVNAME='
18
19class platform_SecureEraseFile(test.test):
20    """Validate secure_erase_file tool behavior.
21
22    We can't verify from this test that data has been destroyed from the
23    underlying physical device, but we can confirm that it's not reachable from
24    userspace.
25    """
26    version = 1
27
28    def __write_test_file(self, path, blocksize, count):
29        cmd = '/bin/dd if=/dev/urandom of=%s bs=%s count=%d' % (
30                path, blocksize, count)
31        utils.run(cmd)
32        if not os.path.exists(path):
33            raise error.TestError('Failed to generate test file')
34
35
36    def __get_partition(self, path):
37        info = os.lstat(path)
38        major = os.major(info.st_dev)
39        minor = os.minor(info.st_dev)
40        uevent_path = '/sys/dev/block/%d:%d/uevent' % (major, minor)
41        with open(uevent_path, 'r') as uevent_file:
42            for l in uevent_file.readlines():
43                if l.startswith(DEVNAME_PREFIX):
44                    return '/dev/' + l[len(DEVNAME_PREFIX):].strip()
45        raise error.TestError('Unable to find partition for path: ' + path)
46
47
48    def __get_extents(self, path, partition):
49        extents = []
50        cmd = 'debugfs -R "extents %s" %s' % (path, partition)
51        result = utils.run(cmd)
52        for line in result.stdout.splitlines():
53            # Discard header line.
54            if line.startswith('Level'):
55                continue
56            fields = line.split()
57
58            # Ignore non-leaf extents
59            if fields[0].strip('/') != fields[1]:
60                continue
61            extents.append({'offset': fields[7], 'length': fields[10]})
62
63        return extents
64
65
66    def __verify_cleared(self, partition, extents):
67        out_path = tempfile.mktemp()
68        for e in extents:
69            cmd = 'dd if=%s bs=4K skip=%s count=%s of=%s' % (
70                   partition, e['offset'], e['length'], out_path)
71            utils.run(cmd)
72            with open(out_path, 'r') as out_file:
73                d = out_file.read()
74                for i, byte in enumerate(d):
75                    if ord(byte) != 0x00 and ord(byte) != 0xFF:
76                        logging.info('extent[%d] = %s', i, hex(ord(byte)))
77                        raise error.TestError('Bad byte found')
78
79
80    def __test_and_verify_cleared(self, blocksize, count):
81        self.__write_test_file(TEST_PATH, blocksize, count)
82        utils.run('sync')
83
84        logging.info('original file contents: ')
85        res = utils.run('xxd %s' % TEST_PATH)
86        logging.info(res.stdout)
87
88        partition = self.__get_partition(TEST_PATH)
89        extents = self.__get_extents(PARTITION_TEST_PATH, partition)
90        if len(extents) == 0:
91            raise error.TestError('No extents found for ' + TEST_PATH)
92
93        utils.run('%s %s' % (BINARY, TEST_PATH))
94
95        # secure_erase_file confirms that the file has been erased and that its
96        # contents are not accessible. If that is not the case, it will return
97        # with a non-zero exit code.
98        if os.path.exists(TEST_PATH):
99            raise error.TestError('Secure Erase failed to unlink file.')
100
101        self.__verify_cleared(partition, extents)
102
103
104    def run_once(self):
105        # Secure erase is only supported on eMMC today; pass if
106        # no device is present.
107        if len(glob.glob('/dev/mmcblk*')) == 0:
108            raise error.TestNAError('Skipping test; no eMMC device found.')
109
110        self.__test_and_verify_cleared('64K', 2)
111        self.__test_and_verify_cleared('1M', 16)
112
113    def after_run_once(self):
114        if os.path.exists(TEST_PATH):
115            os.unlink(TEST_PATH)
116
117