Module: Landlock::Policy

Defined in:
lib/landlock/policy.rb

Class Method Summary collapse

Class Method Details

.add_net_rules(fd, ports, rights, abi) ⇒ Object

Raises:



88
89
90
91
92
93
94
95
96
97
# File 'lib/landlock/policy.rb', line 88

def add_net_rules(fd, ports, rights, abi)
  ports = Array(ports)
  return if ports.empty?
  raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4

  access_mask = mask(rights, NET_RIGHTS, abi)
  return if access_mask.zero?

  ports.each { |port| Native.add_net_rule(fd, Integer(port), access_mask) }
end

.add_path_rules(fd, paths, rights, abi) ⇒ Object



78
79
80
81
82
83
84
85
86
# File 'lib/landlock/policy.rb', line 78

def add_path_rules(fd, paths, rights, abi)
  Array(paths).each do |path|
    expanded_path = File.expand_path(path)
    access_mask = mask(path_rights(expanded_path, rights), FS_RIGHTS, abi)
    next if access_mask.zero?

    Native.add_path_rule(fd, expanded_path, access_mask)
  end
end

.fs_rights_for_abi(abi) ⇒ Object



121
122
123
124
125
126
127
# File 'lib/landlock/policy.rb', line 121

def fs_rights_for_abi(abi)
  rights = FS_RIGHTS.values.reduce(0, :|)
  rights &= ~ACCESS_FS_REFER if abi < 2
  rights &= ~ACCESS_FS_TRUNCATE if abi < 3
  rights &= ~ACCESS_FS_IOCTL_DEV if abi < 5
  rights
end

.handled_fs_for(read:, write:, execute:, paths:, abi:) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/landlock/policy.rb', line 129

def handled_fs_for(read:, write:, execute:, paths:, abi:)
  bits = 0
  bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless Array(read).empty?
  bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless Array(execute).empty?
  bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless Array(write).empty?
  Array(paths).each do |rule|
    path, rights = normalize_path_rule(rule)
    bits |= path_rule_access_mask(File.expand_path(path), rights, abi)
  end
  bits
end

.handled_net_for(connect_tcp:, bind_tcp:, abi:) ⇒ Object

Raises:



141
142
143
144
145
146
147
148
149
150
# File 'lib/landlock/policy.rb', line 141

def handled_net_for(connect_tcp:, bind_tcp:, abi:)
  bits = 0
  bits |= ACCESS_NET_CONNECT_TCP unless Array(connect_tcp).empty?
  bits |= ACCESS_NET_BIND_TCP unless Array(bind_tcp).empty?
  return 0 if bits.zero?

  raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4

  bits
end

.mask(names, table, abi) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/landlock/policy.rb', line 110

def mask(names, table, abi)
  Array(names).reduce(0) do |bits, name|
    bit = table.fetch(name.to_sym) { raise ArgumentError, "unknown Landlock right: #{name.inspect}" }
    next bits if bit == ACCESS_FS_REFER && abi < 2
    next bits if bit == ACCESS_FS_TRUNCATE && abi < 3
    next bits if bit == ACCESS_FS_IOCTL_DEV && abi < 5

    bits | bit
  end
end

.normalize_path_rule(rule) ⇒ Object



99
100
101
102
103
104
105
106
107
108
# File 'lib/landlock/policy.rb', line 99

def normalize_path_rule(rule)
  case rule
  when Hash
    [rule.fetch(:path), Array(rule.fetch(:rights))]
  when Array
    [rule.fetch(0), Array(rule.fetch(1))]
  else
    raise ArgumentError, "path rule must be {path:, rights:} or [path, rights]"
  end
end

.path_rights(path, rights) ⇒ Object



68
69
70
# File 'lib/landlock/policy.rb', line 68

def path_rights(path, rights)
  File.directory?(path) ? rights : Array(rights) & FILE_PATH_RIGHTS
end

.path_rule_access_mask(path, rights, abi) ⇒ Object



72
73
74
75
76
# File 'lib/landlock/policy.rb', line 72

def path_rule_access_mask(path, rights, abi)
  mask(path_rights(path, rights), FS_RIGHTS, abi).tap do |access_mask|
    raise ArgumentError, "path rule has no effective rights: #{path}" if access_mask.zero?
  end
end

.requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:) ⇒ Boolean

Returns:

  • (Boolean)


63
64
65
66
# File 'lib/landlock/policy.rb', line 63

def requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
  allow_all_known || Array(read).any? || Array(write).any? || Array(execute).any? || Array(connect_tcp).any? ||
    Array(bind_tcp).any? || Array(paths).any? || Array(scope).any?
end

.restrict!(read: [], write: [], execute: [], connect_tcp: [], bind_tcp: [], paths: [], scope: [], allow_all_known: false) ⇒ Object

Raises:



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
# File 'lib/landlock/policy.rb', line 10

def restrict!(
  read: [],
  write: [],
  execute: [],
  connect_tcp: [],
  bind_tcp: [],
  paths: [],
  scope: [],
  allow_all_known: false
)
  abi = Native.abi_version
  raise UnsupportedError, "Linux Landlock is unavailable" unless abi.positive?

  fs_handled =
    (
      if allow_all_known
        fs_rights_for_abi(abi)
      else
        handled_fs_for(read:, write:, execute:, paths:, abi:)
      end
    )
  net_handled = handled_net_for(connect_tcp:, bind_tcp:, abi:)
  scoped = scope_for(scope:, abi:)

  if fs_handled.zero? && net_handled.zero? && scoped.zero?
    raise ArgumentError, "empty Landlock policy: provide filesystem paths, TCP ports, or scopes"
  end

  fd = Native.create_ruleset(fs_handled, net_handled, scoped)
  begin
    add_path_rules(fd, read, READ_RIGHTS, abi)
    add_path_rules(fd, execute, EXEC_RIGHTS, abi)
    add_path_rules(fd, write, WRITE_RIGHTS, abi)

    Array(paths).each do |rule|
      path, rights = normalize_path_rule(rule)
      expanded_path = File.expand_path(path)
      access_mask = path_rule_access_mask(expanded_path, rights, abi)

      Native.add_path_rule(fd, expanded_path, access_mask)
    end

    add_net_rules(fd, connect_tcp, [:connect_tcp], abi)
    add_net_rules(fd, bind_tcp, [:bind_tcp], abi)

    Native.restrict_self(fd)
  ensure
    Native.close_fd(fd) if fd && fd >= 0
  end

  true
end

.scope_for(scope:, abi:) ⇒ Object

Raises:



152
153
154
155
156
157
158
159
# File 'lib/landlock/policy.rb', line 152

def scope_for(scope:, abi:)
  bits = mask(scope, SCOPE_FLAGS, abi)
  return 0 if bits.zero?

  raise UnsupportedError, "Landlock scopes require ABI v6+; running ABI v#{abi}" if abi < 6

  bits
end