seccomp-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seccomp
4
+ # Architecture tokens and name conversion.
5
+ module Arch
6
+ module_function
7
+
8
+ # @return [Hash<Symbol, Integer>] constants available in the linked header
9
+ # @example `Arch.all[:x86_64]`
10
+ def all
11
+ @all ||= constants(false).grep_v(:NAMES).to_h do |name|
12
+ [name.to_s.downcase.to_sym, const_get(name)]
13
+ end.freeze
14
+ end
15
+
16
+ # @return [Integer] native runtime architecture token
17
+ # @example `Arch.native`
18
+ def native
19
+ LowLevel.arch_native
20
+ end
21
+
22
+ # @param value [Symbol, String, Integer] architecture
23
+ # @return [Integer] architecture token
24
+ # @raise [ArgumentError] for an unknown name
25
+ # @example `Arch.resolve(:x86_64)`
26
+ def resolve(value)
27
+ return value if value.is_a?(Integer)
28
+ unless value.respond_to?(:to_sym)
29
+ raise ArgumentError, "unknown architecture: #{value.inspect}"
30
+ end
31
+
32
+ key = value.to_sym
33
+ all.fetch(key) do
34
+ token = LowLevel.arch_resolve_name(value.to_s)
35
+ raise ArgumentError, "unknown architecture: #{value.inspect}" if token.zero?
36
+
37
+ token
38
+ end
39
+ end
40
+
41
+ # @param token [Integer] architecture token
42
+ # @return [Symbol, nil] known name
43
+ # @example `Arch.name(Arch.native)`
44
+ def name(token)
45
+ all.key(token) || (token == native ? all.key(native) : nil)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seccomp
4
+ # Immutable syscall argument comparison.
5
+ class ArgCmp
6
+ # Largest accepted unsigned syscall datum.
7
+ # @api private
8
+ MAX_DATUM = (1 << 64) - 1
9
+ # Symbol-to-C-operator lookup.
10
+ # @api private
11
+ OPERATORS = {
12
+ ne: Compare::NE,
13
+ lt: Compare::LT,
14
+ le: Compare::LE,
15
+ eq: Compare::EQ,
16
+ ge: Compare::GE,
17
+ gt: Compare::GT,
18
+ masked_eq: Compare::MASKED_EQ
19
+ }.freeze
20
+
21
+ attr_reader :arg, :op, :datum_a, :datum_b
22
+
23
+ # @param arg [Integer] argument index from 0 through 5
24
+ # @param op [Symbol] comparison operator
25
+ # @param datum_a [Integer] first comparison datum
26
+ # @param datum_b [Integer] second datum for masked equality
27
+ # @param width [Integer] 32 or 64
28
+ # @return [void]
29
+ # @raise [ArgumentError] for invalid input
30
+ # @example `ArgCmp.new(arg: 0, op: :eq, datum_a: 2)`
31
+ def initialize(arg:, op:, datum_a:, datum_b: 0, width: 64)
32
+ unless arg.is_a?(Integer) && (0..5).cover?(arg)
33
+ raise ArgumentError, "arg must be an Integer between 0 and 5"
34
+ end
35
+ raise ArgumentError, "width must be 32 or 64" unless [32, 64].include?(width)
36
+
37
+ @arg = arg
38
+ operator = op.to_sym if op.respond_to?(:to_sym)
39
+ @op = OPERATORS.fetch(operator) { raise ArgumentError, "unknown comparison: #{op.inspect}" }
40
+ @datum_a = datum(datum_a, width)
41
+ @datum_b = datum(datum_b, width)
42
+ end
43
+
44
+ # @return [Array<Integer>] low-level comparison tuple
45
+ # @example `comparison.to_a`
46
+ def to_a
47
+ [arg, op, datum_a, datum_b]
48
+ end
49
+
50
+ private
51
+
52
+ def datum(value, width)
53
+ raise ArgumentError, "datum must be an Integer" unless value.is_a?(Integer)
54
+ raise ArgumentError, "datum must be between 0 and 2**64-1" unless (0..MAX_DATUM).cover?(value)
55
+
56
+ width == 32 ? value & 0xFFFF_FFFF : value
57
+ end
58
+ end
59
+
60
+ # Argument comparison DSL.
61
+ module Arg
62
+ # Builder bound to one syscall argument index.
63
+ class Reference
64
+ # @param index [Integer] argument index from 0 through 5
65
+ # @return [void]
66
+ # @raise [ArgumentError] outside 0..5
67
+ # @example `Reference.new(0)`
68
+ def initialize(index)
69
+ unless index.is_a?(Integer) && (0..5).cover?(index)
70
+ raise ArgumentError, "arg must be an Integer between 0 and 5"
71
+ end
72
+
73
+ @index = index
74
+ end
75
+
76
+ %i[ne lt le eq ge gt].each do |operator|
77
+ define_method(operator) do |value, width: 64|
78
+ ArgCmp.new(arg: @index, op: operator, datum_a: value, width: width)
79
+ end
80
+ end
81
+
82
+ alias == eq
83
+
84
+ # Build a masked equality comparison.
85
+ # @param mask [Integer] bit mask
86
+ # @param value [Integer] expected masked value
87
+ # @param width [Integer] 32 or 64
88
+ # @return [ArgCmp]
89
+ # @raise [ArgumentError] for invalid data
90
+ # @example `reference.masked_eq(mask: 0xff, value: 1)`
91
+ def masked_eq(mask:, value:, width: 64)
92
+ ArgCmp.new(arg: @index, op: :masked_eq, datum_a: mask, datum_b: value, width: width)
93
+ end
94
+ end
95
+
96
+ # Adds {#arg} to filter and DSL contexts.
97
+ module DSL
98
+ # @param index [Integer] argument index
99
+ # @return [Reference] comparison builder
100
+ # @raise [ArgumentError] outside 0..5
101
+ # @example `arg(0).eq(2)`
102
+ def arg(index)
103
+ Reference.new(index)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seccomp
4
+ # Named accessors for libseccomp filter attributes.
5
+ module Attributes
6
+ # Symbol-to-C-attribute lookup.
7
+ # @api private
8
+ ATTRIBUTE_NAMES = {
9
+ default_action: FilterAttr::ACT_DEFAULT,
10
+ bad_arch_action: FilterAttr::ACT_BADARCH,
11
+ no_new_privs: FilterAttr::CTL_NNP,
12
+ tsync: FilterAttr::CTL_TSYNC,
13
+ tskip: FilterAttr::API_TSKIP,
14
+ log: FilterAttr::CTL_LOG,
15
+ ssb: FilterAttr::CTL_SSB,
16
+ optimize: FilterAttr.const_defined?(:CTL_OPTIMIZE) ? FilterAttr::CTL_OPTIMIZE : nil,
17
+ raw_rc: FilterAttr.const_defined?(:API_SYSRAWRC) ? FilterAttr::API_SYSRAWRC : nil,
18
+ wait_killable: FilterAttr.const_defined?(:CTL_WAITKILL) ? FilterAttr::CTL_WAITKILL : nil
19
+ }.compact.freeze
20
+
21
+ # Attributes exposed as predicate accessors.
22
+ # @api private
23
+ BOOLEAN_ATTRIBUTES = %i[no_new_privs tsync tskip log ssb raw_rc wait_killable].freeze
24
+ # Minimum runtime API level for newer attributes.
25
+ # @api private
26
+ ATTRIBUTE_API_LEVELS = {
27
+ FilterAttr::CTL_TSYNC => 2,
28
+ FilterAttr::CTL_LOG => 3,
29
+ FilterAttr::CTL_SSB => 4,
30
+ (FilterAttr::CTL_WAITKILL if FilterAttr.const_defined?(:CTL_WAITKILL)) => 7
31
+ }.compact.freeze
32
+
33
+ # @param attribute [Symbol, Integer] attribute name or enum value
34
+ # @return [Integer] raw attribute value
35
+ # @raise [Error] if the attribute cannot be read
36
+ # @example `filter[:optimize]`
37
+ def [](attribute)
38
+ synchronize do
39
+ rc, value = LowLevel.attr_get(@context, resolve_attribute(attribute))
40
+ Error.check!(rc, call: "seccomp_attr_get", hint: "attribute #{attribute}")
41
+ value
42
+ end
43
+ end
44
+
45
+ # @param attribute [Symbol, Integer] attribute name or enum value
46
+ # @param value [Integer] raw attribute value
47
+ # @return [Integer] assigned value
48
+ # @raise [Error] if the attribute cannot be set
49
+ # @example `filter[:optimize] = 2`
50
+ def []=(attribute, value)
51
+ synchronize do
52
+ resolved = resolve_attribute(attribute)
53
+ value = Action.resolve(value) if resolved == FilterAttr::ACT_BADARCH
54
+ if resolved == FilterAttr::CTL_TSYNC && value.is_a?(Integer) && !value.zero?
55
+ ensure_notification_tsync_supported!(tsync: true)
56
+ end
57
+
58
+ Error.check!(LowLevel.attr_set(@context, resolved, value),
59
+ call: "seccomp_attr_set", hint: "attribute #{attribute}")
60
+ end
61
+ value
62
+ end
63
+
64
+ # @return [Integer] default action
65
+ # @example `filter.default_action`
66
+ def default_action
67
+ self[:default_action]
68
+ end
69
+
70
+ # @return [Integer] action for unsupported architectures
71
+ # @example `filter.bad_arch_action`
72
+ def bad_arch_action
73
+ self[:bad_arch_action]
74
+ end
75
+
76
+ # @param value [Symbol, Integer] action
77
+ # @return [Integer] assigned action
78
+ # @raise [Error] if rejected
79
+ # @example `filter.bad_arch_action = :kill_process`
80
+ def bad_arch_action=(value)
81
+ self[:bad_arch_action] = value
82
+ end
83
+
84
+ BOOLEAN_ATTRIBUTES.each do |name|
85
+ define_method("#{name}?") { self[name] != 0 }
86
+ define_method("#{name}=") do |value|
87
+ enabled = !!value
88
+ self[name] = enabled ? 1 : 0
89
+ enabled
90
+ end
91
+ end
92
+
93
+ # @return [Integer] optimization level
94
+ # @example `filter.optimize`
95
+ def optimize
96
+ self[:optimize]
97
+ end
98
+
99
+ # @param value [Integer] optimization level
100
+ # @return [Integer] assigned level
101
+ # @raise [Error] if rejected
102
+ # @example `filter.optimize = 2`
103
+ def optimize=(value)
104
+ self[:optimize] = value
105
+ end
106
+
107
+ private
108
+
109
+ def resolve_attribute(attribute)
110
+ resolved = if attribute.is_a?(Integer)
111
+ attribute
112
+ elsif attribute.respond_to?(:to_sym)
113
+ ATTRIBUTE_NAMES.fetch(attribute.to_sym) do
114
+ message = "filter attribute is not available: #{attribute.inspect}"
115
+ raise NotSupportedError, message
116
+ end
117
+ else
118
+ raise ArgumentError, "unknown filter attribute: #{attribute.inspect}"
119
+ end
120
+ required = ATTRIBUTE_API_LEVELS[resolved]
121
+ return resolved unless required
122
+ return resolved if Seccomp.api_level >= required
123
+
124
+ raise NotSupportedError, "filter attribute requires libseccomp API level #{required}"
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seccomp
4
+ # Convenience methods used by {Seccomp.filter} blocks.
5
+ module DSL
6
+ # @param architectures [Array<Symbol, String, Integer>] architectures to add
7
+ # @return [Filter] receiver
8
+ # @raise [Error] if an architecture cannot be added
9
+ # @example `arch :x86_64, :x86`
10
+ def arch(*architectures)
11
+ architectures.each { |architecture| add_arch(architecture) }
12
+ self
13
+ end
14
+
15
+ # @param value [Boolean] no-new-privileges setting
16
+ # @return [Filter] receiver
17
+ # @raise [Error] if rejected
18
+ # @example `no_new_privs true`
19
+ def no_new_privs(value)
20
+ self.no_new_privs = value
21
+ self
22
+ end
23
+
24
+ # @param value [Boolean] thread synchronization setting
25
+ # @return [Filter] receiver
26
+ # @raise [Error] if rejected
27
+ # @example `tsync true`
28
+ def tsync(value)
29
+ self.tsync = value
30
+ self
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seccomp
4
+ # Base error carrying the libseccomp errno and C call name.
5
+ class Error < StandardError
6
+ attr_reader :errno, :call
7
+
8
+ # errno-to-exception lookup.
9
+ # @api private
10
+ ERRORS = {
11
+ Errno::EACCES::Errno => :PermissionError,
12
+ Errno::ECANCELED::Errno => :KernelError,
13
+ Errno::EDOM::Errno => :ArchError,
14
+ Errno::EEXIST::Errno => :ExistsError,
15
+ Errno::EINVAL::Errno => :InvalidArgumentError,
16
+ Errno::ENOENT::Errno => :NotFoundError,
17
+ Errno::ENOMEM::Errno => :OutOfMemoryError,
18
+ Errno::EOPNOTSUPP::Errno => :NotSupportedError,
19
+ Errno::ERANGE::Errno => :ValueRangeError,
20
+ Errno::ESRCH::Errno => :ThreadSyncError
21
+ }.freeze
22
+
23
+ # @param message [String, nil] error message
24
+ # @param errno [Integer, nil] positive errno
25
+ # @param call [String, nil] C function name
26
+ # @return [void]
27
+ # @example `Error.new("failed", errno: 22, call: "seccomp_init")`
28
+ def initialize(message = nil, errno: nil, call: nil)
29
+ @errno = errno
30
+ @call = call
31
+ super(message)
32
+ end
33
+
34
+ # @param result [Integer] libseccomp return code
35
+ # @param call [String] C function name
36
+ # @param hint [String, nil] contextual hint
37
+ # @return [Integer] nonnegative result
38
+ # @raise [Error] for a negative result
39
+ # @example `Error.check!(0, call: "seccomp_load")`
40
+ def self.check!(result, call:, hint: nil)
41
+ return result unless result.negative?
42
+
43
+ errno = -result
44
+ name = Errno.constants.find do |constant|
45
+ error_class = Errno.const_get(constant)
46
+ error_class.is_a?(Class) && error_class < SystemCallError && errno == error_class::Errno
47
+ end
48
+ klass = Seccomp.const_get(ERRORS.fetch(errno, :Error))
49
+ message = "#{call} failed: #{name || "errno #{errno}"}"
50
+ message += " (#{hint})" if hint
51
+ raise klass.new(message, errno: errno, call: call)
52
+ end
53
+ end
54
+
55
+ # Operation was not permitted by libseccomp.
56
+ class PermissionError < Error; end
57
+ # Kernel-side filter generation or loading failed.
58
+ class KernelError < Error; end
59
+ # Architecture or ABI is invalid for the operation.
60
+ class ArchError < Error; end
61
+ # Architecture or rule already exists.
62
+ class ExistsError < Error; end
63
+ # An argument was rejected by libseccomp.
64
+ class InvalidArgumentError < Error; end
65
+ # Requested object was not found.
66
+ class NotFoundError < Error; end
67
+ # Native allocation failed.
68
+ class OutOfMemoryError < Error; end
69
+ # Linked libseccomp or kernel lacks the feature.
70
+ class NotSupportedError < Error; end
71
+ # A value is outside the supported range.
72
+ class ValueRangeError < Error; end
73
+ # Thread synchronization failed.
74
+ class ThreadSyncError < Error; end
75
+ # Syscall name could not be resolved.
76
+ class UnknownSyscallError < Error; end
77
+ # A released filter was accessed.
78
+ class ClosedFilterError < Error; end
79
+ # Base user-notification error.
80
+ class NotificationError < Error; end
81
+ # Notification target exited before a response.
82
+ class NotificationCanceledError < NotificationError; end
83
+ end