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.
- checksums.yaml +7 -0
- data/.rubocop.yml +43 -0
- data/.yardopts +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/Rakefile +34 -0
- data/examples/arg_filter.rb +18 -0
- data/examples/basic_allowlist.rb +20 -0
- data/examples/errno_filter.rb +18 -0
- data/examples/notify_supervisor.rb +26 -0
- data/ext/seccomp_ext/const.c +133 -0
- data/ext/seccomp_ext/extconf.rb +45 -0
- data/ext/seccomp_ext/filter.c +685 -0
- data/ext/seccomp_ext/notify.c +184 -0
- data/ext/seccomp_ext/seccomp_ext.c +20 -0
- data/ext/seccomp_ext/seccomp_ext.h +35 -0
- data/ext/seccomp_ext/syscall.c +86 -0
- data/ext/seccomp_ext/util.c +110 -0
- data/lib/libseccomp.rb +77 -0
- data/lib/seccomp/action.rb +154 -0
- data/lib/seccomp/arch.rb +48 -0
- data/lib/seccomp/arg.rb +107 -0
- data/lib/seccomp/attributes.rb +127 -0
- data/lib/seccomp/dsl.rb +33 -0
- data/lib/seccomp/errors.rb +83 -0
- data/lib/seccomp/filter.rb +425 -0
- data/lib/seccomp/low_level.rb +210 -0
- data/lib/seccomp/notification.rb +95 -0
- data/lib/seccomp/notifier.rb +187 -0
- data/lib/seccomp/syscall.rb +44 -0
- data/lib/seccomp/version.rb +6 -0
- data/lib/seccomp.rb +3 -0
- data/sig/seccomp.rbs +300 -0
- metadata +75 -0
data/lib/seccomp/arch.rb
ADDED
|
@@ -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
|
data/lib/seccomp/arg.rb
ADDED
|
@@ -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
|
data/lib/seccomp/dsl.rb
ADDED
|
@@ -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
|