1import unittest
2import re
3import os
4import string
5from oeqa.runtime.case import OERuntimeTestCase
6from oeqa.core.decorator.depends import OETestDepends
7from oeqa.runtime.decorator.package import OEHasPackage
8from oeqa.core.decorator.data import skipIfNotFeature
9
10MAX_LABEL_LEN = 255
11LABEL = "a" * MAX_LABEL_LEN
12
13class SmackBasicTest(OERuntimeTestCase):
14    ''' base smack test '''
15
16    @classmethod
17    def setUpClass(cls):
18        cls.smack_path = ""
19        cls.current_label  = ""
20        cls.uid = 1000
21
22    @skipIfNotFeature('smack',
23        'Test requires smack to be in DISTRO_FEATURES')
24    @OEHasPackage(['smack-test'])
25    @OETestDepends(['ssh.SSHTest.test_ssh'])
26    def test_smack_basic(self):
27        status, output = self.target.run("grep smack /proc/mounts | awk '{print $2}'")
28        self.smack_path = output
29        status,output = self.target.run("cat /proc/self/attr/current")
30        self.current_label = output.strip()
31
32    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
33    def test_add_access_label(self):
34        ''' Test if chsmack can correctly set a SMACK label '''
35        filename = "/tmp/test_access_label"
36        self.target.run("touch %s" %filename)
37        status, output = self.target.run("chsmack -a %s %s" %(LABEL, filename))
38        self.assertEqual(
39            status, 0,
40            "Cannot set smack access label. "
41            "Status and output: %d %s" %(status, output))
42        status, output = self.target.run("chsmack %s" %filename)
43        self.target.run("rm %s" %filename)
44        m = re.search('(?<=access=")\S+(?=")', output)
45        if m is None:
46            self.fail("Did not find access attribute")
47        else:
48            label_retrieved = m .group(0)
49            self.assertEqual(
50                LABEL, label_retrieved,
51                "label not set correctly. expected and gotten: "
52                "%s %s" %(LABEL,label_retrieved))
53
54
55    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
56    def test_add_exec_label(self):
57        '''Test if chsmack can correctly set a SMACK Exec label'''
58        filename = "/tmp/test_exec_label"
59        self.target.run("touch %s" %filename)
60        status, output = self.target.run("chsmack -e %s %s" %(LABEL, filename))
61        self.assertEqual(
62            status, 0,
63            "Cannot set smack exec label. "
64            "Status and output: %d %s" %(status, output))
65        status, output = self.target.run("chsmack %s" %filename)
66        self.target.run("rm %s" %filename)
67        m= re.search('(?<=execute=")\S+(?=")', output)
68        if m is None:
69            self.fail("Did not find execute attribute")
70        else:
71            label_retrieved = m.group(0)
72            self.assertEqual(
73                LABEL, label_retrieved,
74                "label not set correctly. expected and gotten: " +
75                "%s %s" %(LABEL,label_retrieved))
76
77
78    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
79    def test_add_mmap_label(self):
80        '''Test if chsmack can correctly set a SMACK mmap label'''
81        filename = "/tmp/test_exec_label"
82        self.target.run("touch %s" %filename)
83        status, output = self.target.run("chsmack -m %s %s" %(LABEL, filename))
84        self.assertEqual(
85            status, 0,
86            "Cannot set smack mmap label. "
87            "Status and output: %d %s" %(status, output))
88        status, output = self.target.run("chsmack %s" %filename)
89        self.target.run("rm %s" %filename)
90        m = re.search('(?<=mmap=")\S+(?=")', output)
91        if m is None:
92            self.fail("Did not find mmap attribute")
93        else:
94            label_retrieved = m.group(0)
95            self.assertEqual(
96                LABEL, label_retrieved,
97                "label not set correctly. expected and gotten: " +
98                "%s %s" %(LABEL,label_retrieved))
99
100
101    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
102    def test_add_transmutable(self):
103        '''Test if chsmack can correctly set a SMACK transmutable mode'''
104
105        directory = "~/test_transmutable"
106        self.target.run("mkdir -p %s" %directory)
107        status, output = self.target.run("chsmack -t %s" %directory)
108        self.assertEqual(status, 0, "Cannot set smack transmutable. "
109                        "Status and output: %d %s" %(status, output))
110        status, output = self.target.run("chsmack %s" %directory)
111        self.target.run("rmdir %s" %directory)
112        m = re.search('(?<=transmute=")\S+(?=")', output)
113        if m is None:
114            self.fail("Did not find transmute attribute")
115        else:
116            label_retrieved = m.group(0)
117            self.assertEqual(
118                "TRUE", label_retrieved,
119                "label not set correctly. expected and gotten: " +
120                "%s %s" %(LABEL,label_retrieved))
121
122
123    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
124    def test_privileged_change_self_label(self):
125        '''Test if privileged process (with CAP_MAC_ADMIN privilege)
126        can change its label.
127        '''
128
129        labelf = "/proc/self/attr/current"
130        command = "/bin/sh -c 'echo PRIVILEGED >%s; cat %s'" %(labelf, labelf)
131
132        status, output = self.target.run(
133            "notroot.py 0 %s %s" %(self.current_label, command))
134
135        self.assertIn("PRIVILEGED", output,
136                    "Privilege process did not change label.Output: %s" %output)
137
138    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
139    def test_unprivileged_change_self_label(self):
140        '''Test if unprivileged process (without CAP_MAC_ADMIN privilege)
141        cannot change its label'''
142
143        command = "/bin/sh -c 'echo %s >/proc/self/attr/current'" %LABEL
144        status, output = self.target.run(
145            "notroot.py %d %s %s"
146            %(self.uid, self.current_label, command) +
147            " 2>&1 | grep 'Operation not permitted'" )
148
149        self.assertEqual(
150            status, 0,
151            "Unprivileged process should not be able to change its label")
152
153
154    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
155    def test_unprivileged_change_file_label(self):
156        '''Test if unprivileged process cannot change file labels'''
157
158        status, chsmack = self.target.run("which chsmack")
159        status, touch = self.target.run("which touch")
160        filename = "/tmp/test_unprivileged_change_file_label"
161
162        self.target.run("touch %s" % filename)
163        self.target.run("notroot.py %d %s" %(self.uid, self.current_label))
164        status, output = self.target.run(
165            "notroot.py " +
166            "%d unprivileged %s -a %s %s 2>&1 " %(self.uid, chsmack, LABEL, filename) +
167            "| grep 'Operation not permitted'"  )
168
169        self.target.run("rm %s" % filename)
170        self.assertEqual( status, 0, "Unprivileged process changed label for %s" %filename)
171
172    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
173    def test_load_smack_rule(self):
174        '''Test if new smack access rules can be loaded'''
175
176        # old 23 character format requires special spaces formatting
177        #      12345678901234567890123456789012345678901234567890123
178        ruleA="TheOne                  TheOther                rwxat"
179        ruleB="TheOne                  TheOther                r----"
180        clean="TheOne                  TheOther                -----"
181        modeA = "rwxat"
182        modeB = "r"
183
184        status, output = self.target.run('echo -n "%s" > %s/load' %(ruleA, self.smack_path))
185        status, output = self.target.run( 'cat %s/load | grep "^TheOne" | grep " TheOther "' %self.smack_path)
186        self.assertEqual(status, 0, "Rule A was not added")
187        mode = list(filter(bool, output.split(" ")))[2].strip()
188        self.assertEqual( mode, modeA, "Mode A was not set correctly; mode: %s" %mode)
189
190        status, output = self.target.run( 'echo -n "%s" > %s/load' %(ruleB, self.smack_path))
191        status, output = self.target.run( 'cat %s/load | grep "^TheOne" | grep " TheOther "' %self.smack_path)
192        mode = list(filter(bool, output.split(" ")))[2].strip()
193        self.assertEqual( mode, modeB, "Mode B was not set correctly; mode: %s" %mode)
194
195        self.target.run('echo -n "%s" > %s/load' %(clean, self.smack_path))
196
197
198    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
199    def test_smack_onlycap(self):
200        '''Test if smack onlycap label can be set
201
202        test needs to change the running label of the current process,
203        so whole test takes places on image
204        '''
205        status, output = self.target.run("sh /usr/sbin/test_smack_onlycap.sh")
206        self.assertEqual(status, 0, output)
207
208
209    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
210    def test_smack_netlabel(self):
211
212        test_label="191.191.191.191 TheOne"
213        expected_label="191.191.191.191/32 TheOne"
214
215        status, output = self.target.run( "echo -n '%s' > %s/netlabel" %(test_label, self.smack_path))
216        self.assertEqual( status, 0, "Netlabel /32 could not be set. Output: %s" %output)
217
218        status, output = self.target.run("cat %s/netlabel" %self.smack_path)
219        self.assertIn( expected_label, output, "Did not find expected label in output: %s" %output)
220
221        test_label="253.253.253.0/24 TheOther"
222        status, output = self.target.run( "echo -n '%s' > %s/netlabel" %(test_label, self.smack_path))
223        self.assertEqual( status, 0, "Netlabel /24 could not be set. Output: %s" %output)
224
225        status, output = self.target.run("cat %s/netlabel" %self.smack_path)
226        self.assertIn(
227            test_label, output,
228            "Did not find expected label in output: %s" %output)
229
230
231    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
232    def test_smack_cipso(self):
233        '''Test if smack cipso rules can be set'''
234        #      12345678901234567890123456789012345678901234567890123456
235        ruleA="TheOneA                 2   0   "
236        ruleB="TheOneB                 3   1   55  "
237        ruleC="TheOneC                 4   2   17  33  "
238
239        status, output = self.target.run(
240            "echo -n '%s' > %s/cipso" %(ruleA, self.smack_path))
241        self.assertEqual(status, 0,
242            "Could not set cipso label A. Ouput: %s" %output)
243
244        status, output = self.target.run(
245            "cat %s/cipso | grep '^TheOneA'" %self.smack_path)
246        self.assertEqual(status, 0, "Cipso rule A was not set")
247        self.assertIn(" 2", output, "Rule A was not set correctly")
248
249        status, output = self.target.run(
250            "echo -n '%s' > %s/cipso" %(ruleB, self.smack_path))
251        self.assertEqual(status, 0,
252            "Could not set cipso label B. Ouput: %s" %output)
253
254        status, output = self.target.run(
255            "cat %s/cipso | grep '^TheOneB'" %self.smack_path)
256        self.assertEqual(status, 0, "Cipso rule B was not set")
257        self.assertIn("/55", output, "Rule B was not set correctly")
258
259        status, output = self.target.run(
260            "echo -n '%s' > %s/cipso" %(ruleC, self.smack_path))
261        self.assertEqual(
262            status, 0,
263            "Could not set cipso label C. Ouput: %s" %output)
264
265        status, output = self.target.run(
266            "cat %s/cipso | grep '^TheOneC'" %self.smack_path)
267        self.assertEqual(status, 0, "Cipso rule C was not set")
268        self.assertIn("/17,33", output, "Rule C was not set correctly")
269
270
271    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
272    def test_smack_direct(self):
273        status, initial_direct = self.target.run(
274            "cat %s/direct" %self.smack_path)
275
276        test_direct="17"
277        status, output = self.target.run(
278            "echo '%s' > %s/direct" %(test_direct, self.smack_path))
279        self.assertEqual(status, 0 ,
280            "Could not set smack direct. Output: %s" %output)
281        status, new_direct = self.target.run("cat %s/direct" %self.smack_path)
282        # initial label before checking
283        status, output = self.target.run(
284            "echo '%s' > %s/direct" %(initial_direct, self.smack_path))
285        self.assertEqual(
286            test_direct, new_direct.strip(),
287            "Smack direct label does not match.")
288
289
290    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
291    def test_smack_ambient(self):
292        test_ambient = "test_ambient"
293        status, initial_ambient = self.target.run("cat %s/ambient" %self.smack_path)
294        status, output = self.target.run(
295            "echo '%s' > %s/ambient" %(test_ambient, self.smack_path))
296        self.assertEqual(status, 0,
297            "Could not set smack ambient. Output: %s" %output)
298
299        status, output = self.target.run("cat %s/ambient" %self.smack_path)
300        # Filter '\x00', which is sometimes added to the ambient label
301        new_ambient = ''.join(filter(lambda x: x in string.printable, output))
302        initial_ambient = ''.join(filter(lambda x: x in string.printable, initial_ambient))
303        status, output = self.target.run(
304            "echo '%s' > %s/ambient" %(initial_ambient, self.smack_path))
305        self.assertEqual(
306            test_ambient, new_ambient.strip(),
307            "Ambient label does not match")
308
309
310    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
311    def test_smackload(self):
312        '''Test if smackload command works'''
313        rule="testobject testsubject rwx"
314
315        status, output = self.target.run("echo -n '%s' > /tmp/rules" %rule)
316        status, output = self.target.run("smackload /tmp/rules")
317        self.assertEqual( status, 0, "Smackload failed to load rule. Output: %s" %output)
318
319        status, output = self.target.run( "cat %s/load | grep '%s'" %(self.smack_path, rule))
320        self.assertEqual(status, 0, "Smackload rule was loaded correctly")
321
322
323    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
324    def test_smackcipso(self):
325        '''Test if smackcipso command works'''
326        #     12345678901234567890123456789012345678901234567890123456
327        rule="cipsolabel                  2   2   "
328
329        status, output = self.target.run("echo '%s' | smackcipso" %rule)
330        self.assertEqual( status, 0, "Smackcipso failed to load rule. Output: %s" %output)
331
332        status, output = self.target.run(
333            "cat %s/cipso | grep 'cipsolabel'" %self.smack_path)
334        self.assertEqual(status, 0, "smackcipso rule was loaded correctly")
335        self.assertIn( "2/2", output, "Rule was not set correctly. Got: %s" %output)
336
337
338    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
339    def test_smack_enforce_file_access(self):
340        '''Test if smack file access is enforced (rwx)
341
342        test needs to change the running label of the current process,
343        so whole test takes places on image
344        '''
345        status, output = self.target.run("sh /usr/sbin/smack_test_file_access.sh")
346        self.assertEqual(status, 0, output)
347
348
349    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
350    def test_smack_mmap_enforced(self):
351        '''Test if smack mmap access is enforced'''
352        raise unittest.SkipTest("Depends on mmap_test, which was removed from the layer while investigating its license.")
353
354        #      12345678901234567890123456789012345678901234567890123456
355        delr1="mmap_label              mmap_test_label1        -----"
356        delr2="mmap_label              mmap_test_label2        -----"
357        delr3="mmap_file_label         mmap_test_label1        -----"
358        delr4="mmap_file_label         mmap_test_label2        -----"
359
360        RuleA="mmap_label              mmap_test_label1        rw---"
361        RuleB="mmap_label              mmap_test_label2        r--at"
362        RuleC="mmap_file_label         mmap_test_label1        rw---"
363        RuleD="mmap_file_label         mmap_test_label2        rwxat"
364
365        mmap_label="mmap_label"
366        file_label="mmap_file_label"
367        test_file = "/usr/sbin/smack_test_mmap"
368        mmap_exe = "/tmp/mmap_test"
369        status, echo = self.target.run("which echo")
370        status, output = self.target.run(
371            "notroot.py %d %s %s 'test' > %s" \
372            %(self.uid, self.current_label, echo, test_file))
373        status, output = self.target.run("ls %s" %test_file)
374        self.assertEqual(status, 0, "Could not create mmap test file")
375        self.target.run("chsmack -m %s %s" %(file_label, test_file))
376        self.target.run("chsmack -e %s %s" %(mmap_label, mmap_exe))
377
378        # test with no rules with mmap label or exec label as subject
379        # access should be granted
380        self.target.run('echo -n "%s" > %s/load' %(delr1, self.smack_path))
381        self.target.run('echo -n "%s" > %s/load' %(delr2, self.smack_path))
382        self.target.run('echo -n "%s" > %s/load' %(delr3, self.smack_path))
383        self.target.run('echo -n "%s" > %s/load' %(delr4, self.smack_path))
384        status, output = self.target.run("%s %s 0 2" % (mmap_exe, test_file))
385        self.assertEqual(
386            status, 0,
387            "Should have mmap access without rules. Output: %s" %output)
388
389        # add rules that do not match access required
390        self.target.run('echo -n "%s" > %s/load' %(RuleA, self.smack_path))
391        self.target.run('echo -n "%s" > %s/load' %(RuleB, self.smack_path))
392        status, output = self.target.run("%s %s 0 2" % (mmap_exe, test_file))
393        self.assertNotEqual(
394            status, 0,
395            "Should not have mmap access with unmatching rules. " +
396            "Output: %s" %output)
397        self.assertIn(
398            "Permission denied", output,
399            "Mmap access should be denied with unmatching rules")
400
401        # add rule to match only partially (one way)
402        self.target.run('echo -n "%s" > %s/load' %(RuleC, self.smack_path))
403        status, output = self.target.run("%s %s 0 2" %(mmap_exe, test_file))
404        self.assertNotEqual(
405            status, 0,
406            "Should not have mmap access with partial matching rules. " +
407            "Output: %s" %output)
408        self.assertIn(
409            "Permission denied", output,
410            "Mmap access should be denied with partial matching rules")
411
412        # add rule to match fully
413        self.target.run('echo -n "%s" > %s/load' %(RuleD, self.smack_path))
414        status, output = self.target.run("%s %s 0 2" %(mmap_exe, test_file))
415        self.assertEqual(
416            status, 0,
417            "Should have mmap access with full matching rules." +
418            "Output: %s" %output)
419
420
421    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
422    def test_smack_transmute_dir(self):
423        '''Test if smack transmute attribute works
424
425        test needs to change the running label of the current process,
426        so whole test takes places on image
427        '''
428        test_dir = "/tmp/smack_transmute_dir"
429        label="transmute_label"
430        status, initial_label = self.target.run("cat /proc/self/attr/current")
431
432        self.target.run("mkdir -p %s" % test_dir)
433        self.target.run("chsmack -a %s %s" % (label, test_dir))
434        self.target.run("chsmack -t %s" % test_dir)
435        self.target.run("echo -n '%s %s rwxat' | smackload" %(initial_label, label) )
436
437        self.target.run("touch %s/test" % test_dir)
438        status, output = self.target.run("chsmack %s/test" % test_dir)
439        self.assertIn( 'access="%s"' %label, output,
440            "Did not get expected label. Output: %s" % output)
441
442
443    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
444    def test_smack_tcp_sockets(self):
445        '''Test if smack is enforced on tcp sockets
446
447        whole test takes places on image, depends on tcp_server/tcp_client'''
448
449        status, output = self.target.run("sh /usr/sbin/test_smack_tcp_sockets.sh")
450        self.assertEqual(status, 0, output)
451
452
453    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
454    def test_smack_udp_sockets(self):
455        '''Test if smack is enforced on udp sockets
456
457        whole test takes places on image, depends on udp_server/udp_client'''
458
459        status, output = self.target.run("sh /usr/sbin/test_smack_udp_sockets.sh")
460        self.assertEqual(status, 0, output)
461
462
463    @OETestDepends(['smack.SmackBasicTest.test_smack_basic'])
464    def test_smack_labels(self):
465        '''Check for correct Smack labels.'''
466        expected = '''
467/tmp/ access="*"
468/etc/ access="System::Shared" transmute="TRUE"
469/etc/passwd access="System::Shared"
470/etc/terminfo access="System::Shared" transmute="TRUE"
471/etc/skel/ access="System::Shared" transmute="TRUE"
472/etc/skel/.profile access="System::Shared"
473/var/log/ access="System::Log" transmute="TRUE"
474/var/tmp/ access="*"
475'''
476        files = ' '.join([x.split()[0] for x in expected.split('\n') if x])
477        files_wildcard = ' '.join([x + '/*' for x in files.split()])
478        # Auxiliary information.
479        status, output = self.target.run(
480            'set -x; mount; ls -l -d %s; find %s | xargs ls -d -l; find %s | xargs chsmack' % (
481                ' '.join([x.rstrip('/') for x in files.split()]), files, files
482            )
483        )
484        msg = "File status:\n" + output
485        status, output = self.target.run('chsmack %s' % files)
486        self.assertEqual(
487            status, 0, msg="status and output: %s and %s\n%s" % (status,output, msg))
488        self.longMessage = True
489        self.maxDiff = None
490        self.assertEqual(output.strip().split('\n'), expected.strip().split('\n'), msg=msg)
491