summaryrefslogtreecommitdiff
path: root/src/python/seccomp.pyx
blob: ed7c1bb5551d42c26b99c0d09e936ca416b80692 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
#
# Seccomp Library Python Bindings
#
# Copyright (c) 2012,2013 Red Hat <pmoore@redhat.com>
# Author: Paul Moore <pmoore@redhat.com>
#

#
# This library is free software; you can redistribute it and/or modify it
# under the terms of version 2.1 of the GNU Lesser General Public License as
# published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, see <http://www.gnu.org/licenses>.
#

""" Python bindings for the libseccomp library

The libseccomp library provides and easy to use, platform independent,
interface to the Linux Kernel's syscall filtering mechanism: seccomp.  The
libseccomp API is designed to abstract away the underlying BPF based
syscall filter language and present a more conventional function-call
based filtering interface that should be familiar to, and easily adopted
by application developers.

Filter action values:
    KILL - kill the process
    ALLOW - allow the syscall to execute
    TRAP - a SIGSYS signal will be thrown
    ERRNO(x) - syscall will return (x)
    TRACE(x) - if the process is being traced, (x) will be returned to the
               tracing process via PTRACE_EVENT_SECCOMP and the
               PTRACE_GETEVENTMSG option

Argument comparison values (see the Arg class):

    NE - arg != datum_a
    LT - arg < datum_a
    LE - arg <= datum_a
    EQ - arg == datum_a
    GT - arg > datum_a
    GE - arg >= datum_a
    MASKED_EQ - (arg & datum_b) == datum_a


Example:

    import sys
    from seccomp import *

    # create a filter object with a default KILL action
    f = SyscallFilter(defaction=KILL)

    # add syscall filter rules to allow certain syscalls
    f.add_rule(ALLOW, "open")
    f.add_rule(ALLOW, "close")
    f.add_rule(ALLOW, "read", Arg(0, EQ, sys.stdin))
    f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stdout))
    f.add_rule(ALLOW, "write", Arg(0, EQ, sys.stderr))
    f.add_rule(ALLOW, "rt_sigreturn")

    # load the filter into the kernel
    f.load()
"""
__author__ =  'Paul Moore <paul@paul-moore.com>'
__date__ = "7 January 2013"

from libc.stdint cimport uint32_t
import errno

cimport libseccomp

KILL = libseccomp.SCMP_ACT_KILL
TRAP = libseccomp.SCMP_ACT_TRAP
ALLOW = libseccomp.SCMP_ACT_ALLOW
def ERRNO(int errno):
    return libseccomp.SCMP_ACT_ERRNO(errno)
def TRACE(int value):
    return libseccomp.SCMP_ACT_TRACE(value)

NE = libseccomp.SCMP_CMP_NE
LT = libseccomp.SCMP_CMP_LT
LE = libseccomp.SCMP_CMP_LE
EQ = libseccomp.SCMP_CMP_EQ
GE = libseccomp.SCMP_CMP_GE
GT = libseccomp.SCMP_CMP_GT
MASKED_EQ = libseccomp.SCMP_CMP_MASKED_EQ

def system_arch():
    """ Return the system architecture value.

    Description:
    Returns the native system architecture value.
    """
    return libseccomp.seccomp_arch_native()

def resolve_syscall(arch, syscall):
    """ Resolve the syscall.

    Arguments:
    arch - the architecture value, e.g. Arch.*
    syscall - the syscall name or number

    Description:
    Resolve an architecture's syscall name to the correct number or the
    syscall number to the correct name.
    """
    if (isinstance(syscall, basestring)):
        return libseccomp.seccomp_syscall_resolve_name_arch(arch, syscall)
    elif (isinstance(syscall, int)):
        return libseccomp.seccomp_syscall_resolve_num_arch(arch, syscall)
    else:
        raise TypeError("Syscall must either be an int or str type")

cdef class Arch:
    """ Python object representing the SyscallFilter architecture values.

    Data values:
    NATIVE - the native architecture
    X86 - 32-bit x86
    X86_64 - 64-bit x86
    X32 - 64-bit x86 using the x32 ABI
    ARM - ARM
    """

    NATIVE = libseccomp.SCMP_ARCH_NATIVE
    X86 = libseccomp.SCMP_ARCH_X86
    X86_64 = libseccomp.SCMP_ARCH_X86_64
    X32 = libseccomp.SCMP_ARCH_X32
    ARM = libseccomp.SCMP_ARCH_ARM

cdef class Attr:
    """ Python object representing the SyscallFilter attributes.

    Data values:
    ACT_DEFAULT - the filter's default action
    ACT_BADARCH - the filter's bad architecture action
    CTL_NNP - the filter's "no new privileges" flag
    """
    ACT_DEFAULT = libseccomp.SCMP_FLTATR_ACT_DEFAULT
    ACT_BADARCH = libseccomp.SCMP_FLTATR_ACT_BADARCH
    CTL_NNP = libseccomp.SCMP_FLTATR_CTL_NNP

cdef class Arg:
    """ Python object representing a SyscallFilter syscall argument.
    """
    cdef libseccomp.scmp_arg_cmp _arg

    def __cinit__(self, arg, op, datum_a, datum_b = 0):
        """ Initialize the argument comparison.

        Arguments:
        arg - the arguement number, starting at 0
        op - the argument comparison operator, e.g. {NE,LT,LE,...}
        datum_a - argument value
        datum_b - argument value, only valid when op == MASKED_EQ

        Description:
        Create an argument comparison object for use with SyscallFilter.
        """
        self._arg.arg = arg
        self._arg.op = op
        if isinstance(datum_a, file):
            self._arg.datum_a = datum_a.fileno()
        else:
            self._arg.datum_a = datum_a
        if isinstance(datum_b, file):
            self._arg.datum_b = datum_b.fileno()
        else:
            self._arg.datum_b = datum_b

    def to_c(self):
        """ Convert the object into a C structure.

        Description:
        Helper function which should only be used internally by
        SyscallFilter objects and exists for the sole purpose of making it
        easier to deal with the varadic functions of the libseccomp API,
        e.g. seccomp_rule_add().
        """
        return self._arg

cdef class SyscallFilter:
    """ Python object representing a seccomp syscall filter. """
    cdef int _defaction
    cdef libseccomp.scmp_filter_ctx _ctx

    def __cinit__(self, int defaction):
        self._ctx = libseccomp.seccomp_init(defaction)
        if self._ctx == NULL:
            raise RuntimeError("Library error")
        _defaction = defaction

    def __init__(self, defaction):
        """ Initialize the filter state

        Arguments:
        defaction - the default filter action

        Description:
        Initializes the seccomp filter state to the defaults.
        """

    def __dealloc__(self):
        """ Destroys the filter state and releases any resources.

        Description:
        Destroys the seccomp filter state and releases any resources
        associated with the filter state.  This function does not affect
        any seccomp filters already loaded into the kernel.
        """
        if self._ctx != NULL:
            libseccomp.seccomp_release(self._ctx)

    def reset(self, int defaction = -1):
        """ Reset the filter state.

        Arguments:
        defaction - the default filter action

        Description:
        Resets the seccomp filter state to an initial default state, if a
        default filter action is not specified in the reset call the
        original action will be reused.  This function does not affect any
        seccomp filters alread loaded into the kernel.
        """
        if defaction == -1:
            defaction = self._defaction
        rc = libseccomp.seccomp_reset(self._ctx, defaction)
        if rc == -errno.EINVAL:
            raise ValueError("Invalid action")
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))
        _defaction = defaction

    def merge(self, SyscallFilter filter):
        """ Merge two existing SyscallFilter objects.

        Arguments:
        filter - a valid SyscallFilter object

        Description:
        Merges a valid SyscallFilter object with the current SyscallFilter
        object; the passed filter object will be reset on success.  In
        order to successfully merge two seccomp filters they must have the
        same attribute values and not share any of the same architectures.
        """
        rc = libseccomp.seccomp_merge(self._ctx, filter._ctx)
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))
        filter._ctx = NULL
        filter = SyscallFilter(filter._defaction)

    def exist_arch(self, arch):
        """ Check if the seccomp filter contains a given architecture.

        Arguments:
        arch - the architecture value, e.g. Arch.*

        Description:
        Test to see if a given architecture is included in the filter.
        Return True is the architecture exists, False if it does not
        exist.
        """
        rc = libseccomp.seccomp_arch_exist(self._ctx, arch)
        if rc == 0:
            return True
        elif rc == -errno.EEXIST:
            return False
        elif rc == -errno.EINVAL:
            raise ValueError("Invalid architecture")
        else:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def add_arch(self, arch):
        """ Add an architecture to the filter.

        Arguments:
        arch - the architecture value, e.g. Arch.*

        Description:
        Add the given architecture to the filter.  Any new rules added
        after this method returns successfully will be added to this new
        architecture, but any existing rules will not be added to the new
        architecture.
        """
        rc = libseccomp.seccomp_arch_add(self._ctx, arch)
        if rc == -errno.EINVAL:
            raise ValueError("Invalid architecture")
        elif rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def remove_arch(self, arch):
        """ Remove an architecture from the filter.

        Arguments:
        arch - the architecture value, e.g. Arch.*

        Description:
        Remove the given architecture from the filter.  The filter must
        always contain at least one architecture, so if only one
        architecture exists in the filter this method will fail.
        """
        rc = libseccomp.seccomp_arch_remove(self._ctx, arch)
        if rc == -errno.EINVAL:
            raise ValueError("Invalid architecture")
        elif rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def load(self):
        """ Load the filter into the Linux Kernel.

        Description:
        Load the current filter into the Linux Kernel.  As soon as the
        method returns the filter will be active and enforcing.
        """
        rc = libseccomp.seccomp_load(self._ctx)
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def get_attr(self, attr):
        """ Get an attribute value from the filter.

        Arguments:
        attr - the attribute, e.g. Attr.*

        Description:
        Lookup the given attribute in the filter and return the
        attribute's value to the caller.
        """
        value = 0
        rc = libseccomp.seccomp_attr_get(self._ctx,
                                         attr, <uint32_t *>&value)
        if rc == -errno.EINVAL:
            raise ValueError("Invalid attribute")
        elif rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))
        return value

    def set_attr(self, attr, int value):
        """ Set a filter attribute.

        Arguments:
        attr - the attribute, e.g. Attr.*
        value - the attribute value

        Description:
        Lookup the given attribute in the filter and assign it the given
        value.
        """
        rc = libseccomp.seccomp_attr_set(self._ctx, attr, value)
        if rc == -errno.EINVAL:
            raise ValueError("Invalid attribute")
        elif rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def syscall_priority(self, syscall, int priority):
        """ Set the filter priority of a syscall.

        Arguments:
        syscall - the syscall name or number
        priority - the priority of the syscall

        Description:
        Set the filter priority of the given syscall.  A syscall with a
        higher priority will have less overhead in the generated filter
        code which is loaded into the system.  Priority values can range
        from 0 to 255 inclusive.
        """
        if priority < 0 or priority > 255:
            raise ValueError("Syscall priority must be between 0 and 255")
        if isinstance(syscall, str):
            syscall_str = syscall.encode()
            syscall_num = libseccomp.seccomp_syscall_resolve_name(syscall_str)
        elif isinstance(syscall, int):
            syscall_num = syscall
        else:
            raise TypeError("Syscall must either be an int or str type")
        rc = libseccomp.seccomp_syscall_priority(self._ctx,
                                                 syscall_num, priority)
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def add_rule(self, int action, syscall, *args):
        """ Add a new rule to filter.

        Arguments:
        action - the rule action: KILL, TRAP, ERRNO(), TRACE(), or ALLOW
        syscall - the syscall name or number
        args - variable number of Arg objects

        Description:
        Add a new rule to the filter, matching on the given syscall and an
        optional list of argument comparisons.  If the rule is triggered
        the given action will be taken by the kernel.  In order for the
        rule to trigger, the syscall as well as each argument comparison
        must be true.

        In the case where the specific rule is not valid on a specific
        architecture, e.g. socket() on 32-bit x86, this method rewrites
        the rule to the best possible match.  If you don't want this fule
        rewriting to take place use add_rule_exactly().
        """
        cdef libseccomp.scmp_arg_cmp c_arg[6]
        if isinstance(syscall, str):
            syscall_str = syscall.encode()
            syscall_num = libseccomp.seccomp_syscall_resolve_name(syscall_str)
        elif isinstance(syscall, int):
            syscall_num = syscall
        else:
            raise TypeError("Syscall must either be an int or str type")
        """ NOTE: the code below exists solely to deal with the varadic
        nature of seccomp_rule_add() function and the inability of Cython
        to handle this automatically """
        for i, arg in enumerate(args):
            c_arg[i] = arg.to_c()
        if len(args) == 0:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num, 0)
        elif len(args) == 1:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0])
        elif len(args) == 2:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0],
                                             c_arg[1])
        elif len(args) == 3:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0],
                                             c_arg[1],
                                             c_arg[2])
        elif len(args) == 4:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0],
                                             c_arg[1],
                                             c_arg[2],
                                             c_arg[3])
        elif len(args) == 5:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0],
                                             c_arg[1],
                                             c_arg[2],
                                             c_arg[3],
                                             c_arg[4])
        elif len(args) == 6:
            rc = libseccomp.seccomp_rule_add(self._ctx, action, syscall_num,
                                             len(args),
                                             c_arg[0],
                                             c_arg[1],
                                             c_arg[2],
                                             c_arg[3],
                                             c_arg[4],
                                             c_arg[5])
        else:
            raise RuntimeError("Maximum number of arguments exceeded")
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def add_rule_exactly(self, int action, syscall, *args):
        """ Add a new rule to filter.

        Arguments:
        action - the rule action: KILL, TRAP, ERRNO(), TRACE(), or ALLOW
        syscall - the syscall name or number
        args - variable number of Arg objects

        Description:
        Add a new rule to the filter, matching on the given syscall and an
        optional list of argument comparisons.  If the rule is triggered
        the given action will be taken by the kernel.  In order for the
        rule to trigger, the syscall as well as each argument comparison
        must be true.

        This method attempts to add the filter rule exactly as specified
        which can cause problems on certain architectures, e.g. socket()
        on 32-bit x86.  For a architecture independent version of this
        method use add_rule().
        """
        cdef libseccomp.scmp_arg_cmp c_arg[6]
        if isinstance(syscall, str):
            syscall_str = syscall.encode()
            syscall_num = libseccomp.seccomp_syscall_resolve_name(syscall_str)
        elif isinstance(syscall, int):
            syscall_num = syscall
        else:
            raise TypeError("Syscall must either be an int or str type")
        """ NOTE: the code below exists solely to deal with the varadic
        nature of seccomp_rule_add_exact() function and the inability of
        Cython to handle this automatically """
        for i, arg in enumerate(args):
            c_arg[i] = arg.to_c()
        if len(args) == 0:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, 0)
        elif len(args) == 1:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0])
        elif len(args) == 2:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0],
                                                   c_arg[1])
        elif len(args) == 3:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0],
                                                   c_arg[1],
                                                   c_arg[2])
        elif len(args) == 4:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0],
                                                   c_arg[1],
                                                   c_arg[2],
                                                   c_arg[3])
        elif len(args) == 5:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0],
                                                   c_arg[1],
                                                   c_arg[2],
                                                   c_arg[3],
                                                   c_arg[4])
        elif len(args) == 6:
            rc = libseccomp.seccomp_rule_add_exact(self._ctx, action,
                                                   syscall_num, len(args),
                                                   c_arg[0],
                                                   c_arg[1],
                                                   c_arg[2],
                                                   c_arg[3],
                                                   c_arg[4],
                                                   c_arg[5])
        else:
            raise RuntimeError("Maximum number of arguments exceeded")
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def export_pfc(self, file):
        """ Export the filter in PFC format.

        Arguments:
        file - the output file

        Description:
        Output the filter in Pseudo Filter Code (PFC) to the given file.
        The output is functionally equivalent to the BPF based filter
        which is loaded into the Linux Kernel.
        """
        rc = libseccomp.seccomp_export_pfc(self._ctx, file.fileno())
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

    def export_bpf(self, file):
        """ Export the filter in BPF format.

        Arguments:
        file - the output file

        Output the filter in Berkley Packet Filter (BPF) to the given
        file.  The output is identical to what is loaded into the
        Linux Kernel.
        """
        rc = libseccomp.seccomp_export_bpf(self._ctx, file.fileno())
        if rc != 0:
            raise RuntimeError(str.format("Library error (errno = {0})", rc))

# kate: syntax python;
# kate: indent-mode python; space-indent on; indent-width 4; mixedindent off;