mxc 0.2.0-x86_64-linux → 0.3.0-x86_64-linux

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: 86bb54e129b48e32e469fa771293b5d887064fad5a98c1ed7d47e98c823288e2
4
- data.tar.gz: 22054e5c8fe708cf11fcacfba0e1612a296716f7f7f7f8428d2786ba93ca9e18
3
+ metadata.gz: e1fede2f737ced692fc0707a6aa148cb471456c51975a69b01453946be3cf86f
4
+ data.tar.gz: c71d74ea18026d8e8e7b4aa543c6fc54a87838986c244bf23576c4ecbe618140
5
5
  SHA512:
6
- metadata.gz: 4db21a001399af4632811e84238a37b32d9d21b10fead1d87e62599385a5aa2fa661690fd182a35d9a0c6453981e4ad39970ca0262dccb2144a3b435b1f73e34
7
- data.tar.gz: eef8f390ec961e9940660c10f2020820a7e341c8e53e584385b80571f8876d8291f6c94796ccb5da110705d1d33af045ac73f6e70b649a3e2119af48539fa61f
6
+ metadata.gz: c224454b5f04ddd562bf747cba8690b8337ec9a7b38300a52e38948eb087101cae22c5638330f955c2b26e6f06a33549406fe9691c8f47efb6aa148181bd472f
7
+ data.tar.gz: 51a0d65079bd000c4f8bd1eb255b6e98f73ffde581d9913273a6bf34a4ea067ac14d26a6e178933aebe0e5255cf73f720ad47ddde26a2fb0241f37bdafc87ce5
data/NOTICE CHANGED
@@ -6,8 +6,8 @@ endorsed by Microsoft.
6
6
 
7
7
  The native extension compiles and links the mxc-sdk crate
8
8
  (https://github.com/microsoft/mxc, https://crates.io/crates/mxc-sdk), from
9
- a fork at https://github.com/crmne/microsoft-mxc that adds resource limits,
10
- which is distributed under the MIT License:
9
+ a fork at https://github.com/crmne/microsoft-mxc that adds resource limits
10
+ and a Landlock backend, which is distributed under the MIT License:
11
11
 
12
12
  MIT License
13
13
 
data/README.md CHANGED
@@ -22,7 +22,7 @@ result.stdout
22
22
  result.success?
23
23
  ```
24
24
 
25
- It is an unofficial Ruby binding to [Microsoft's MXC](https://github.com/microsoft/mxc), the MIT-licensed sandbox library. Not affiliated with or endorsed by Microsoft. Resource limits come from [a fork of MXC](https://github.com/crmne/microsoft-mxc) until they are [part of MXC itself](https://github.com/microsoft/mxc/issues/1498).
25
+ It is an unofficial Ruby binding to [Microsoft's MXC](https://github.com/microsoft/mxc), the MIT-licensed sandbox library. Not affiliated with or endorsed by Microsoft. Resource limits and the Landlock backend come from [a fork of MXC](https://github.com/crmne/microsoft-mxc) until they are part of MXC itself ([resource limits](https://github.com/microsoft/mxc/issues/1498), [Landlock](https://github.com/microsoft/mxc/issues/1494)).
26
26
 
27
27
  ## Installation
28
28
 
@@ -36,6 +36,8 @@ Precompiled gems cover Linux (x86_64 and ARM64, glibc and musl), macOS (Intel an
36
36
  sudo apt-get install bubblewrap
37
37
  ```
38
38
 
39
+ Where Bubblewrap cannot create its namespaces, as in a default Docker container or on Ubuntu with AppArmor's restriction, sandboxes run on Landlock instead (Linux 6.2 or newer), without network access.
40
+
39
41
  Commands that use the network also need `slirp4netns`, `iptables`, util-linux, and `/dev/net/tun`. macOS and Windows need nothing extra.
40
42
 
41
43
  ## Usage
@@ -117,15 +119,16 @@ MXC.backends # => [#<MXC::Backend name=:bubblewrap ...>]
117
119
 
118
120
  ## Platforms
119
121
 
120
- | Platform | Backend | 0.2.0 |
122
+ | Platform | Backend | 0.3.0 |
121
123
  | --- | --- | --- |
122
124
  | Linux | Bubblewrap | Supported and tested, without process limits |
125
+ | Linux 6.2 and later | Landlock, where Bubblewrap cannot run or when asked for | Supported and tested, without network access or process limits |
123
126
  | macOS | Seatbelt | Supported and tested, without memory or process limits |
124
127
  | Windows 11 24H2 and later | ProcessContainer | Supported and tested, without host allow-lists |
125
128
 
126
129
  On Windows, a single String runs through `cmd.exe` instead of `sh`. See [Running Commands](https://crmne.github.io/mxc/running-commands/#windows) and [Policies](https://crmne.github.io/mxc/policies/#windows) for what else differs.
127
130
 
128
- Docker blocks the namespaces Bubblewrap needs by default. [Running in Docker](https://crmne.github.io/mxc/running-in-docker/) shows the smallest set of changes that makes it work, with a seccomp profile and a Kamal configuration for a job role.
131
+ Docker blocks the namespaces Bubblewrap needs by default, so a default container runs sandboxes on Landlock. [Running in Docker](https://crmne.github.io/mxc/running-in-docker/) shows what that gives up, and the smallest set of changes that makes Bubblewrap work, with a seccomp profile and a Kamal configuration for a job role.
129
132
 
130
133
  ## Documentation
131
134
 
data/lib/mxc/3.2/mxc.so CHANGED
Binary file
data/lib/mxc/3.3/mxc.so CHANGED
Binary file
data/lib/mxc/3.4/mxc.so CHANGED
Binary file
data/lib/mxc/4.0/mxc.so CHANGED
Binary file
@@ -16,9 +16,13 @@ module MXC
16
16
  # Returns a suggestion for making the host supported, or nil.
17
17
  attr_reader :remediation
18
18
 
19
+ # Returns the backend sandboxes run on, such as <tt>:bubblewrap</tt>, or <tt>:landlock</tt>
20
+ # when <tt>:auto</tt> falls back to it. Returns nil when no sandbox can start.
21
+ attr_reader :runs_on
22
+
19
23
  def self.probe(backend) # :nodoc:
20
24
  result = Sandbox.new(backend:, timeout: 10).run('exit 0')
21
- return new(backend:, supported: true) if result.success?
25
+ return new(backend:, supported: true, runs_on: result.backend) if result.success?
22
26
 
23
27
  outcome = result.timed_out? ? 'timed out' : "exited with #{result.exit_code}"
24
28
  new(backend:, supported: false, reason: "A sandboxed `exit 0` #{outcome}: #{result.stderr.strip}")
@@ -26,8 +30,9 @@ module MXC
26
30
  new(backend:, supported: false, reason: e.message, remediation: e.remediation)
27
31
  end
28
32
 
29
- def initialize(backend:, supported:, reason: nil, remediation: nil) # :nodoc:
33
+ def initialize(backend:, supported:, reason: nil, remediation: nil, runs_on: nil) # :nodoc:
30
34
  @backend = backend
35
+ @runs_on = runs_on
31
36
  @supported = supported
32
37
  @reason = reason
33
38
  @remediation = remediation
@@ -39,7 +44,8 @@ module MXC
39
44
  end
40
45
 
41
46
  def inspect # :nodoc:
42
- "#<MXC::Availability backend=#{backend.inspect} supported=#{supported?} reason=#{reason.inspect}>"
47
+ "#<MXC::Availability backend=#{backend.inspect} supported=#{supported?} runs_on=#{runs_on.inspect} " \
48
+ "reason=#{reason.inspect}>"
43
49
  end
44
50
  end
45
51
  end
@@ -15,7 +15,8 @@ module MXC
15
15
  @timeout = nil
16
16
  end
17
17
 
18
- # Sets the default backend: <tt>:auto</tt>, <tt>:bubblewrap</tt>, or <tt>:seatbelt</tt>.
18
+ # Sets the default backend: <tt>:auto</tt>, <tt>:bubblewrap</tt>, <tt>:landlock</tt>,
19
+ # <tt>:seatbelt</tt>, or <tt>:process_container</tt>.
19
20
  def backend=(name)
20
21
  @backend = Sandbox.backend_name(name)
21
22
  end
data/lib/mxc/native.rb CHANGED
@@ -13,6 +13,9 @@ module MXC
13
13
  MISSING_BUBBLEWRAP = /bwrap\) is not installed/
14
14
  INSTALL_BUBBLEWRAP = 'Install Bubblewrap 0.5.0 or newer (Debian/Ubuntu: apt-get install bubblewrap; ' \
15
15
  'Fedora: dnf install bubblewrap; Arch: pacman -S bubblewrap)'
16
+ LANDLOCK_REFUSAL = /\ALandlock: /
17
+ LANDLOCK_POLICY = 'Change the policy as the message says, or run on Bubblewrap: a sandbox with backend :auto ' \
18
+ 'runs on Landlock only where Bubblewrap cannot create its namespaces'
16
19
 
17
20
  def self.backend_attributes
18
21
  backends.map { |attributes| attributes.merge(name: BACKEND_NAMES.fetch(attributes[:name], attributes[:name])) }
@@ -21,8 +24,12 @@ module MXC
21
24
  def self.error(message, code: nil, remediation: nil, **)
22
25
  if message.match?(MISSING_BUBBLEWRAP)
23
26
  UnsupportedError.new(message, code: :backend_unavailable, remediation: remediation || INSTALL_BUBBLEWRAP, **)
27
+ elsif message.match?(LANDLOCK_REFUSAL)
28
+ UnsupportedError.new(message, code:, remediation: remediation || LANDLOCK_POLICY, **)
29
+ elsif UNSUPPORTED_CODES.include?(code)
30
+ UnsupportedError.new(message, code:, remediation: remediation || StartupFailure.hint(message), **)
24
31
  else
25
- (UNSUPPORTED_CODES.include?(code) ? UnsupportedError : Error).new(message, code:, remediation:, **)
32
+ Error.new(message, code:, remediation:, **)
26
33
  end
27
34
  end
28
35
  end
data/lib/mxc/preflight.rb CHANGED
@@ -16,16 +16,27 @@ module MXC
16
16
  'Linux and Windows support it'
17
17
  PROCESS_LIMIT = 'Leave processes out of the limits. Only Windows can count the processes of one sandbox; ' \
18
18
  'in a container, limit them with docker run --pids-limit'
19
+ LANDLOCK_NETWORK = 'Use network: false, or the :bubblewrap backend on a host that allows unprivileged user ' \
20
+ 'namespaces. Landlock cannot tell one address from another, so it could only allow the ' \
21
+ "whole host network, the host's loopback and inbound connections included"
19
22
 
20
23
  module_function
21
24
 
22
- def check!(network:, denied: [], resources: nil)
23
- error = problem(network:, denied:, resources:)
25
+ def check!(network:, denied: [], resources: nil, backend: :auto)
26
+ error = problem(network:, denied:, resources:, backend:)
24
27
  raise error if error
25
28
  end
26
29
 
27
- def problem(network:, denied: [], resources: nil, path: ENV.fetch('PATH', ''), tun: TUN)
28
- resource_problem(resources) || host_problem(network, denied, path, tun)
30
+ def problem(network:, denied: [], resources: nil, backend: :auto, path: ENV.fetch('PATH', ''), tun: TUN)
31
+ resource_problem(resources) || landlock_problem(network, backend) || host_problem(network, denied, path, tun)
32
+ end
33
+
34
+ def landlock_problem(network, backend)
35
+ return unless network && backend == :landlock
36
+
37
+ kind = network.is_a?(Hash) ? 'Host allow-lists are' : 'Network access is'
38
+ UnsupportedError.new("#{kind} not supported on Landlock", code: :backend_unavailable,
39
+ remediation: LANDLOCK_NETWORK)
29
40
  end
30
41
 
31
42
  def resource_problem(resources)
data/lib/mxc/sandbox.rb CHANGED
@@ -28,7 +28,8 @@ module MXC
28
28
  #
29
29
  # SkillSandbox.new.run("python3 app/skills/build.py")
30
30
  class Sandbox
31
- BACKENDS = %i[auto bubblewrap seatbelt process_container].freeze # :nodoc:
31
+ BACKENDS = %i[auto bubblewrap landlock seatbelt process_container].freeze # :nodoc:
32
+ LANDLOCK_FALLBACK = /runs on Landlock instead/ # :nodoc:
32
33
  OPTIONS = %i[readable writable denied network env directory timeout backend limits priority].freeze # :nodoc:
33
34
  HOST_RESOLVER_SOCKETS = %w[/run/systemd/resolve].freeze # :nodoc:
34
35
  WINDOWS_ENV = %w[SYSTEMROOT WINDIR SYSTEMDRIVE COMSPEC PATHEXT PATH LOCALAPPDATA TEMP TMP
@@ -218,8 +219,12 @@ module MXC
218
219
  end
219
220
 
220
221
  # Sets the backend: <tt>:auto</tt> (Bubblewrap on Linux, Seatbelt on macOS,
221
- # ProcessContainer on Windows), <tt>:bubblewrap</tt>, <tt>:seatbelt</tt>, or
222
- # <tt>:process_container</tt>.
222
+ # ProcessContainer on Windows), <tt>:bubblewrap</tt>, <tt>:landlock</tt>, <tt>:seatbelt</tt>,
223
+ # or <tt>:process_container</tt>.
224
+ #
225
+ # On Linux, <tt>:auto</tt> runs on Landlock when Bubblewrap cannot create its namespaces,
226
+ # and says so in Result#warnings. <tt>:bubblewrap</tt> never does. <tt>:landlock</tt>
227
+ # needs Linux 6.2 or newer and runs without network access.
223
228
  def with_backend(name)
224
229
  @settings[:backend] = name && self.class.backend_name(name)
225
230
  self
@@ -279,7 +284,7 @@ module MXC
279
284
  return stream(*command, &) if block_given?
280
285
 
281
286
  outcome = StartupFailure.check!(Native.run(command_line(command), request))
282
- Result.new(**outcome, backend: resolved_backend)
287
+ Result.new(**outcome, backend: resolved_backend(outcome[:warnings]))
283
288
  end
284
289
 
285
290
  # Starts +command+ and returns its Process without waiting for it. Takes the command
@@ -287,7 +292,8 @@ module MXC
287
292
  #
288
293
  # sandbox.spawn("python3", "-i", "-q")
289
294
  def spawn(*command)
290
- Process.new(Native.spawn(command_line(command), request), backend: resolved_backend)
295
+ native = Native.spawn(command_line(command), request)
296
+ Process.new(native, backend: resolved_backend(native.warnings))
291
297
  end
292
298
 
293
299
  # Returns true when this host can run this sandbox, including its network policy.
@@ -298,8 +304,10 @@ module MXC
298
304
  # Returns the Availability of this sandbox on this host, with the reason and
299
305
  # remediation when it cannot run.
300
306
  def availability
301
- problem = Preflight.problem(network:, denied:, resources:)
302
- return MXC.availability(backend:) unless problem
307
+ problem = Preflight.problem(network:, denied:, resources:, backend:)
308
+ host = MXC.availability(backend:) unless problem
309
+ problem ||= Preflight.problem(network:, backend: :landlock) if host&.runs_on == :landlock
310
+ return host unless problem
303
311
 
304
312
  Availability.new(backend:, supported: false, reason: problem.message, remediation: problem.remediation)
305
313
  end
@@ -404,15 +412,16 @@ module MXC
404
412
  arguments.map { |argument| WindowsArguments.quote(argument) }.join(' ')
405
413
  end
406
414
 
407
- def resolved_backend
415
+ def resolved_backend(warnings)
408
416
  return backend unless backend == :auto
409
417
  return :process_container if Preflight.windows?
418
+ return :seatbelt unless Preflight.linux?
410
419
 
411
- Preflight.linux? ? :bubblewrap : :seatbelt
420
+ warnings.any?(LANDLOCK_FALLBACK) ? :landlock : :bubblewrap
412
421
  end
413
422
 
414
423
  def request
415
- Preflight.check!(network:, denied:, resources:)
424
+ Preflight.check!(network:, denied:, resources:, backend:)
416
425
  {
417
426
  readonly: expand(readable),
418
427
  readwrite: expand(writable),
@@ -7,7 +7,8 @@ module MXC
7
7
  /setting up uid map/i, /unshare.*Operation not permitted/i)
8
8
  PROC = /Can't mount proc|mount proc on/i
9
9
  HOST_PROBLEMS = {
10
- NAMESPACES => 'Unprivileged user namespaces are blocked. On a host, enable kernel.unprivileged_userns_clone=1 ' \
10
+ NAMESPACES => 'Unprivileged user namespaces are blocked. Use backend :auto or :landlock to run on Landlock ' \
11
+ 'instead, or allow them for Bubblewrap: on a host, enable kernel.unprivileged_userns_clone=1 ' \
11
12
  'and, on Ubuntu 23.10 and later, allow bwrap in AppArmor ' \
12
13
  '(kernel.apparmor_restrict_unprivileged_userns). ' \
13
14
  "In Docker, run with a seccomp profile that allows namespaces: #{DOCKER_GUIDE}",
@@ -15,23 +16,27 @@ module MXC
15
16
  "In Docker, see #{DOCKER_GUIDE}"
16
17
  }.freeze
17
18
  CHDIR = /Can't chdir/i
19
+ BWRAP_LINE = /^bwrap: .*$/
18
20
 
19
21
  module_function
20
22
 
21
23
  def detect(outcome)
22
- return unless outcome[:exit_code] == 1 && outcome[:stdout].to_s.empty?
24
+ failure = outcome[:failure] or return
25
+ reason = outcome[:stderr].to_s.scan(BWRAP_LINE).last
26
+ return Error.new(failure, code: :backend_error) unless reason
23
27
 
24
- stderr = outcome[:stderr].to_s
25
- return unless stderr.match?(/\Abwrap: [^\n]*\n?\z/)
26
-
27
- message = "Bubblewrap could not start the sandbox: #{stderr.strip}"
28
- _, hint = HOST_PROBLEMS.find { |pattern, _| stderr.match?(pattern) }
28
+ message = "Bubblewrap could not start the sandbox: #{reason}"
29
+ hint = hint(reason)
29
30
  return UnsupportedError.new(message, code: :backend_unavailable, remediation: hint, operation: 'bwrap') if hint
30
31
 
31
- remediation = 'Make the directory readable or writable in the sandbox' if stderr.match?(CHDIR)
32
+ remediation = 'Make the directory readable or writable in the sandbox' if reason.match?(CHDIR)
32
33
  Error.new(message, code: :backend_error, remediation:, operation: 'bwrap')
33
34
  end
34
35
 
36
+ def hint(message)
37
+ HOST_PROBLEMS.find { |pattern, _| message.match?(pattern) }&.last
38
+ end
39
+
35
40
  def check!(outcome)
36
41
  error = detect(outcome)
37
42
  raise error if error
data/lib/mxc/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MXC
4
4
  # The gem version.
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
data/lib/mxc.rb CHANGED
@@ -87,7 +87,8 @@ module MXC
87
87
  Native.backend_attributes.map { |attributes| Backend.new(**attributes) }
88
88
  end
89
89
 
90
- # Returns true when this host supports +capability+ with the configured backend.
90
+ # Returns true when this host supports +capability+ with the configured backend. Where
91
+ # sandboxes run on Landlock, there is no <tt>:network</tt> or <tt>:network_policy</tt>.
91
92
  #
92
93
  # +capability+ is <tt>:network</tt> for unrestricted network access,
93
94
  # <tt>:network_policy</tt> for host allow-lists, <tt>:cpu_time_limit</tt>,
@@ -100,8 +101,8 @@ module MXC
100
101
  return false unless supported?
101
102
 
102
103
  case capability
103
- when :network then Preflight.problem(network: true).nil?
104
- when :network_policy then Preflight.problem(network: { allow: [] }).nil? && proxy_enforcement?
104
+ when :network then Preflight.problem(network: true, backend: runs_on).nil?
105
+ when :network_policy then Preflight.problem(network: { allow: [] }, backend: runs_on).nil? && proxy_enforcement?
105
106
  when *RESOURCE_CAPABILITIES.keys then Preflight.resource_problem(RESOURCE_CAPABILITIES[capability]).nil?
106
107
  else configured_backends.any? { |backend| backend.supports?(capability) }
107
108
  end
@@ -109,6 +110,10 @@ module MXC
109
110
 
110
111
  private
111
112
 
113
+ def runs_on
114
+ availability.runs_on
115
+ end
116
+
112
117
  def proxy_enforcement?
113
118
  !Preflight.linux? || configured_backends.any? { |backend| backend.supports?(:proxy_enforcement) }
114
119
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mxc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: An unofficial Ruby binding to Microsoft's MXC. Run commands in Bubblewrap,
14
14
  Seatbelt or Windows ProcessContainer sandboxes with filesystem, network, environment