landlock 0.3 → 0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d4dc223f9ec0de53ff3ac2db1547a2bb21cee7fe670106795384c2d508395fd
4
- data.tar.gz: 81db633dd067c82e68746b5bffb7db1b4a46a85984aededc03607a49f1ec5bd1
3
+ metadata.gz: b7494b5195627610f67bb7a41a5adcb99857f19e1639d88b1107d3d5d43a639c
4
+ data.tar.gz: '065496b9a75a046e91b1d9445dae704bd4531d7de61391d4506c9f480789a32a'
5
5
  SHA512:
6
- metadata.gz: bcf9b3f9a3b369aca990a9ead22bdee7e5414baee0c73762504a10eb15fa87e56d75253f58802885621000b8e84f70d7ec5ee81b835ac1209b41476727079b81
7
- data.tar.gz: 24f8ee88a25a18f5049b957878969cdb890a12f93e900bdda1402cbd1bf0ea33ec8841c96a3f6391bc216cea0730d361d4be24dac7ae19caf572f372c8c818c2
6
+ metadata.gz: 310b8cf56b7906297466b1273d4c5e924dc859148bc0adf143d44825b2c2b68be07d67d7058def44f9551470c48880b37963db2eb48be991b4a07829a03a3e9e
7
+ data.tar.gz: 362a3ea8d6aaf5115d100adf06524a53cfd87d9a46cfc29f1677c351196fadbffee88fa80d348b433d8d518ad3e517ec3893b6b5a827f9e67b799c5954b093b7
data/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.4] - 2026-08-10
8
+
9
+ ### Changed
10
+
11
+ - **Breaking.** `read:`, `write:`, `execute:`, `connect_tcp:` and `bind_tcp:` treat an empty array as "block all" rather than "allow all". These were previously controlled by the length of each allowlist, so `read: []` turned off all read restrictions instead of denying everything. `nil` is now the "restrictions disabled" flag and the new default; `[]` means "restriction on, empty allowlist". Callers relying on `[]` to mean unrestricted must pass `nil`; callers passing populated arrays are unaffected.
12
+ - The native helper marks an enabled-but-empty restriction by passing the existing flag with an empty value, e.g. `--write ""`. An omitted flag leaves the restriction disabled.
13
+
7
14
  ## [0.3] - 2026-06-17
8
15
 
9
16
  ### Added
data/README.md CHANGED
@@ -119,8 +119,10 @@ stdout, stderr, status = Landlock.capture(
119
119
  Capture options:
120
120
 
121
121
  - `read:`, `write:`, `execute:` — filesystem allowlists. Explicit paths must exist; missing paths raise `ArgumentError` instead of being silently ignored.
122
+
123
+ These distinguish `nil` from `[]`. Omitting one (or passing `nil`) leaves that access class outside the ruleset's handled mask, so Landlock does not restrict it **anywhere** — `write: nil` means the child can still create, delete and rename files across the filesystem. Passing `[]` enforces the class while granting no paths, which is what you want for "must not write at all". Prefer `[]` over `nil` unless you deliberately want the class unpoliced.
122
124
  - `paths:` — exact path rules with explicit Landlock rights, e.g. `{ path:, rights: %i[read_file] }`.
123
- - `connect_tcp:` and `bind_tcp:` — allowed TCP ports. TCP access is unrestricted unless a network rule is provided.
125
+ - `connect_tcp:` and `bind_tcp:` — allowed TCP ports, with the same `nil` versus `[]` distinction. `[]` denies all TCP for that operation and requires ABI v4+; `nil` leaves TCP unrestricted.
124
126
  - `scope:` — Landlock ABI v6+ scopes such as `:signal` and `:abstract_unix_socket`.
125
127
  - `seccomp_deny_network:` — additionally deny common Linux network syscalls with seccomp. This is Linux-specific and intended as defense in depth.
126
128
  - `rlimits:` — resource limits. Supported keys are `:cpu_seconds`, `:memory_bytes`, `:file_size_bytes`, `:open_files`, and `:processes`. Values must be non-negative integers.
@@ -97,6 +97,15 @@ static void path_rule_list_push(path_rule_list *list, char *path, uint64_t right
97
97
  list->len++;
98
98
  }
99
99
 
100
+ /* An empty value marks the class handled while granting nothing; a real path
101
+ or port is never the empty string, so the two cannot collide. */
102
+ static void push_unless_empty(string_list *list, char *value, int *handled) {
103
+ *handled = 1;
104
+ if (value[0] != '\0') {
105
+ string_list_push(list, value);
106
+ }
107
+ }
108
+
100
109
  static unsigned long long parse_ull(const char *value, const char *name) {
101
110
  if (!value || value[0] == '\0' || value[0] == '-' || value[0] == '+' ||
102
111
  isspace((unsigned char)value[0])) {
@@ -121,6 +130,13 @@ static unsigned long long parse_port(const char *value) {
121
130
  return port;
122
131
  }
123
132
 
133
+ static void push_port_unless_empty(ull_list *list, const char *value, int *handled) {
134
+ *handled = 1;
135
+ if (value[0] != '\0') {
136
+ ull_list_push(list, parse_port(value));
137
+ }
138
+ }
139
+
124
140
  static int abi_version(void) {
125
141
  long abi = ll_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
126
142
  if (abi < 0 && (errno == ENOSYS || errno == EOPNOTSUPP)) {
@@ -239,10 +255,11 @@ static void add_net_rule(int fd, unsigned long long port, uint64_t rights) {
239
255
  static void apply_landlock(string_list *read_paths, string_list *write_paths,
240
256
  string_list *execute_paths, path_rule_list *path_rules,
241
257
  ull_list *connect_ports, ull_list *bind_ports, uint64_t scoped,
242
- int allow_all_known) {
243
- int need_fs = read_paths->len || write_paths->len || execute_paths->len || path_rules->len ||
258
+ int allow_all_known, int handled_read, int handled_write,
259
+ int handled_execute, int handled_connect, int handled_bind) {
260
+ int need_fs = handled_read || handled_write || handled_execute || path_rules->len ||
244
261
  allow_all_known;
245
- int need_net = connect_ports->len || bind_ports->len;
262
+ int need_net = handled_connect || handled_bind;
246
263
  int need_scope = scoped != 0;
247
264
  if (!need_fs && !need_net && !need_scope) {
248
265
  return;
@@ -262,13 +279,13 @@ static void apply_landlock(string_list *read_paths, string_list *write_paths,
262
279
  uint64_t known_fs_rights = known_fs_rights_for_abi(abi);
263
280
  uint64_t fs_handled = allow_all_known ? known_fs_rights : 0;
264
281
  if (!allow_all_known) {
265
- if (read_paths->len) {
282
+ if (handled_read) {
266
283
  fs_handled |= read_rights();
267
284
  }
268
- if (execute_paths->len) {
285
+ if (handled_execute) {
269
286
  fs_handled |= execute_rights();
270
287
  }
271
- if (write_paths->len) {
288
+ if (handled_write) {
272
289
  fs_handled |= write_rights(abi);
273
290
  }
274
291
  for (size_t i = 0; i < path_rules->len; i++) {
@@ -281,10 +298,10 @@ static void apply_landlock(string_list *read_paths, string_list *write_paths,
281
298
  }
282
299
  }
283
300
  uint64_t net_handled = 0;
284
- if (bind_ports->len) {
301
+ if (handled_bind) {
285
302
  net_handled |= LANDLOCK_ACCESS_NET_BIND_TCP;
286
303
  }
287
- if (connect_ports->len) {
304
+ if (handled_connect) {
288
305
  net_handled |= LANDLOCK_ACCESS_NET_CONNECT_TCP;
289
306
  }
290
307
 
@@ -533,6 +550,8 @@ int main(int argc, char **argv) {
533
550
  path_rule_list path_rules = {0};
534
551
  ull_list connect_ports = {0}, bind_ports = {0};
535
552
  int seccomp_deny_network = 0, allow_all_known = 0, close_others = 1;
553
+ int handled_read = 0, handled_write = 0, handled_execute = 0;
554
+ int handled_connect = 0, handled_bind = 0;
536
555
  uint64_t scoped = 0;
537
556
  char *chdir_path = NULL;
538
557
  char **command_argv = NULL;
@@ -544,19 +563,19 @@ int main(int argc, char **argv) {
544
563
  break;
545
564
  }
546
565
  if (strcmp(argv[i], "--read") == 0) {
547
- string_list_push(&read_paths, require_arg(argc, argv, &i));
566
+ push_unless_empty(&read_paths, require_arg(argc, argv, &i), &handled_read);
548
567
  } else if (strcmp(argv[i], "--write") == 0) {
549
- string_list_push(&write_paths, require_arg(argc, argv, &i));
568
+ push_unless_empty(&write_paths, require_arg(argc, argv, &i), &handled_write);
550
569
  } else if (strcmp(argv[i], "--execute") == 0) {
551
- string_list_push(&execute_paths, require_arg(argc, argv, &i));
570
+ push_unless_empty(&execute_paths, require_arg(argc, argv, &i), &handled_execute);
552
571
  } else if (strcmp(argv[i], "--path") == 0) {
553
572
  char *path = require_arg(argc, argv, &i);
554
573
  char *rights = require_arg(argc, argv, &i);
555
574
  path_rule_list_push(&path_rules, path, parse_fs_rights(rights));
556
575
  } else if (strcmp(argv[i], "--connect-tcp") == 0) {
557
- ull_list_push(&connect_ports, parse_port(require_arg(argc, argv, &i)));
576
+ push_port_unless_empty(&connect_ports, require_arg(argc, argv, &i), &handled_connect);
558
577
  } else if (strcmp(argv[i], "--bind-tcp") == 0) {
559
- ull_list_push(&bind_ports, parse_port(require_arg(argc, argv, &i)));
578
+ push_port_unless_empty(&bind_ports, require_arg(argc, argv, &i), &handled_bind);
560
579
  } else if (strcmp(argv[i], "--scope") == 0) {
561
580
  scoped |= scope_name(require_arg(argc, argv, &i));
562
581
  } else if (strcmp(argv[i], "--chdir") == 0) {
@@ -588,7 +607,8 @@ int main(int argc, char **argv) {
588
607
  }
589
608
 
590
609
  apply_landlock(&read_paths, &write_paths, &execute_paths, &path_rules, &connect_ports,
591
- &bind_ports, scoped, allow_all_known);
610
+ &bind_ports, scoped, allow_all_known, handled_read, handled_write,
611
+ handled_execute, handled_connect, handled_bind);
592
612
  if (seccomp_deny_network) {
593
613
  apply_seccomp_deny_network();
594
614
  }
@@ -14,13 +14,13 @@ module Landlock
14
14
 
15
15
  def exec(
16
16
  argv,
17
- read: [],
18
- write: [],
19
- execute: [],
20
- connect_tcp: [],
21
- bind_tcp: [],
22
- paths: [],
23
- scope: [],
17
+ read: nil,
18
+ write: nil,
19
+ execute: nil,
20
+ connect_tcp: nil,
21
+ bind_tcp: nil,
22
+ paths: nil,
23
+ scope: nil,
24
24
  chdir: nil,
25
25
  env: nil,
26
26
  unsetenv_others: false,
@@ -49,13 +49,13 @@ module Landlock
49
49
 
50
50
  def spawn(
51
51
  argv,
52
- read: [],
53
- write: [],
54
- execute: [],
55
- connect_tcp: [],
56
- bind_tcp: [],
57
- paths: [],
58
- scope: [],
52
+ read: nil,
53
+ write: nil,
54
+ execute: nil,
55
+ connect_tcp: nil,
56
+ bind_tcp: nil,
57
+ paths: nil,
58
+ scope: nil,
59
59
  chdir: nil,
60
60
  env: nil,
61
61
  unsetenv_others: false,
@@ -82,13 +82,13 @@ module Landlock
82
82
 
83
83
  def capture_with(
84
84
  argv,
85
- read: [],
86
- write: [],
87
- execute: [],
88
- connect_tcp: [],
89
- bind_tcp: [],
90
- paths: [],
91
- scope: [],
85
+ read: nil,
86
+ write: nil,
87
+ execute: nil,
88
+ connect_tcp: nil,
89
+ bind_tcp: nil,
90
+ paths: nil,
91
+ scope: nil,
92
92
  chdir: nil,
93
93
  env: nil,
94
94
  unsetenv_others: false,
@@ -178,8 +178,8 @@ module Landlock
178
178
  end
179
179
 
180
180
  def prepare_policy(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, chdir:, allow_all_known:)
181
- connect_tcp = Validation.normalize_ports(connect_tcp, :connect_tcp)
182
- bind_tcp = Validation.normalize_ports(bind_tcp, :bind_tcp)
181
+ connect_tcp = connect_tcp.nil? ? nil : Validation.normalize_ports(connect_tcp, :connect_tcp)
182
+ bind_tcp = bind_tcp.nil? ? nil : Validation.normalize_ports(bind_tcp, :bind_tcp)
183
183
  read, write, execute, paths = validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
184
184
  { read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known: }
185
185
  end
@@ -221,9 +221,9 @@ module Landlock
221
221
  def validate_policy_paths!(read:, write:, execute:, paths:, chdir:)
222
222
  base = chdir ? File.expand_path(chdir) : Dir.pwd
223
223
  abi = Native.abi_version
224
- read = Validation.validate_existing_paths(read, :read, chdir:)
225
- write = Validation.validate_existing_paths(write, :write, chdir:)
226
- execute = Validation.validate_existing_paths(execute, :execute, chdir:)
224
+ read = read.nil? ? nil : Validation.validate_existing_paths(read, :read, chdir:)
225
+ write = write.nil? ? nil : Validation.validate_existing_paths(write, :write, chdir:)
226
+ execute = execute.nil? ? nil : Validation.validate_existing_paths(execute, :execute, chdir:)
227
227
  paths =
228
228
  Array(paths).map do |rule|
229
229
  path, rights = Policy.normalize_path_rule(rule)
@@ -8,13 +8,13 @@ module Landlock
8
8
  module_function
9
9
 
10
10
  def restrict!(
11
- read: [],
12
- write: [],
13
- execute: [],
14
- connect_tcp: [],
15
- bind_tcp: [],
16
- paths: [],
17
- scope: [],
11
+ read: nil,
12
+ write: nil,
13
+ execute: nil,
14
+ connect_tcp: nil,
15
+ bind_tcp: nil,
16
+ paths: nil,
17
+ scope: nil,
18
18
  allow_all_known: false
19
19
  )
20
20
  abi = Native.abi_version
@@ -61,8 +61,8 @@ module Landlock
61
61
  end
62
62
 
63
63
  def requested?(read:, write:, execute:, connect_tcp:, bind_tcp:, paths:, scope:, allow_all_known:)
64
- allow_all_known || Array(read).any? || Array(write).any? || Array(execute).any? || Array(connect_tcp).any? ||
65
- Array(bind_tcp).any? || Array(paths).any? || Array(scope).any?
64
+ allow_all_known || !read.nil? || !write.nil? || !execute.nil? || !connect_tcp.nil? || !bind_tcp.nil? ||
65
+ !Array(paths).empty? || !Array(scope).empty?
66
66
  end
67
67
 
68
68
  def path_rights(path, rights)
@@ -126,11 +126,14 @@ module Landlock
126
126
  rights
127
127
  end
128
128
 
129
+ # Landlock leaves any right absent from handled_access_fs unrestricted
130
+ # kernel-wide, so nil (do not police this class) and [] (police it, grant
131
+ # nothing) must produce different masks.
129
132
  def handled_fs_for(read:, write:, execute:, paths:, abi:)
130
133
  bits = 0
131
- bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless Array(read).empty?
132
- bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless Array(execute).empty?
133
- bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless Array(write).empty?
134
+ bits |= mask(READ_RIGHTS, FS_RIGHTS, abi) unless read.nil?
135
+ bits |= mask(EXEC_RIGHTS, FS_RIGHTS, abi) unless execute.nil?
136
+ bits |= mask(WRITE_RIGHTS, FS_RIGHTS, abi) unless write.nil?
134
137
  Array(paths).each do |rule|
135
138
  path, rights = normalize_path_rule(rule)
136
139
  bits |= path_rule_access_mask(File.expand_path(path), rights, abi)
@@ -140,8 +143,8 @@ module Landlock
140
143
 
141
144
  def handled_net_for(connect_tcp:, bind_tcp:, abi:)
142
145
  bits = 0
143
- bits |= ACCESS_NET_CONNECT_TCP unless Array(connect_tcp).empty?
144
- bits |= ACCESS_NET_BIND_TCP unless Array(bind_tcp).empty?
146
+ bits |= ACCESS_NET_CONNECT_TCP unless connect_tcp.nil?
147
+ bits |= ACCESS_NET_BIND_TCP unless bind_tcp.nil?
145
148
  return 0 if bits.zero?
146
149
 
147
150
  raise UnsupportedError, "Landlock network rules require ABI v4+; running ABI v#{abi}" if abi < 4
@@ -202,15 +202,15 @@ module Landlock
202
202
  close_others:
203
203
  )
204
204
  args = [helper_path]
205
- Array(read).each { |path| args << "--read" << path.to_s }
206
- Array(write).each { |path| args << "--write" << path.to_s }
207
- Array(execute).each { |path| args << "--execute" << path.to_s }
205
+ emit_rules(args, "--read", read)
206
+ emit_rules(args, "--write", write)
207
+ emit_rules(args, "--execute", execute)
208
208
  Array(paths).each do |rule|
209
209
  path, rights = Policy.normalize_path_rule(rule)
210
210
  args << "--path" << path.to_s << Array(rights).map(&:to_s).join(",")
211
211
  end
212
- Array(connect_tcp).each { |port| args << "--connect-tcp" << port.to_s }
213
- Array(bind_tcp).each { |port| args << "--bind-tcp" << port.to_s }
212
+ emit_rules(args, "--connect-tcp", connect_tcp)
213
+ emit_rules(args, "--bind-tcp", bind_tcp)
214
214
  Array(scope).each { |name| args << "--scope" << name.to_s }
215
215
  Array(rlimits).each { |key, value| args << "--rlimit" << "#{key}=#{value}" }
216
216
  args << "--allow-all-known" if allow_all_known
@@ -220,6 +220,16 @@ module Landlock
220
220
  args.concat(argv.map(&:to_s))
221
221
  args
222
222
  end
223
+
224
+ # An empty value keeps the rights in the handled mask while granting
225
+ # nothing; the empty string is never a valid path or port, so it cannot
226
+ # collide with a real entry.
227
+ def emit_rules(args, flag, values)
228
+ return if values.nil?
229
+ return args.push(flag, "") if Array(values).empty?
230
+
231
+ Array(values).each { |value| args.push(flag, value.to_s) }
232
+ end
223
233
  end
224
234
  end
225
235
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Landlock
4
- VERSION = "0.3"
4
+ VERSION = "0.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: landlock
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.3'
4
+ version: '0.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubygems_version: 4.0.6
150
+ rubygems_version: 3.6.9
151
151
  specification_version: 4
152
152
  summary: Ruby bindings for Linux Landlock sandboxing
153
153
  test_files: []