mountfd 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/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +69 -0
- data/benchmark/mounts.rb +34 -0
- data/docs/kernel-notes.md +48 -0
- data/examples/atomic_swap.rb +12 -0
- data/examples/idmapped_volume.rb +17 -0
- data/examples/mini_container.rb +40 -0
- data/examples/readonly_sandbox.rb +39 -0
- data/ext/mountfd/compat.h +197 -0
- data/ext/mountfd/constants.c +53 -0
- data/ext/mountfd/extconf.rb +12 -0
- data/ext/mountfd/mount_info.c +166 -0
- data/ext/mountfd/mountfd.c +393 -0
- data/ext/mountfd/mountfd.h +17 -0
- data/ext/mountfd/namespace.c +74 -0
- data/ext/mountfd/user_namespace.c +220 -0
- data/lib/mountfd/attributes.rb +65 -0
- data/lib/mountfd/core.rb +346 -0
- data/lib/mountfd/mount_info.rb +78 -0
- data/lib/mountfd/namespace.rb +39 -0
- data/lib/mountfd/user_namespace.rb +84 -0
- data/lib/mountfd/version.rb +5 -0
- data/lib/mountfd.rb +32 -0
- data/sig/mountfd.rbs +119 -0
- data/tools/dump_constants.c +54 -0
- data/tools/generate_constants.rb +32 -0
- data/tools/research_idmap.rb +37 -0
- data/tools/vm/Makefile +8 -0
- data/tools/vm/config-fragment +7 -0
- data/tools/vm/run.sh +18 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 75db1001630a8a9cc58592f3daa3c263fe50f3c356de527f4f693d1c34ae7e1f
|
|
4
|
+
data.tar.gz: be9d690de9dae6234faeb83b7f5c712b73690fb703769ad12adba3298dfccedd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a305df091fee137702cc27adc98e1ab4537ce3d1b9dd7580a3a98a81e2943fd65753a79a2713009f50d8271f0c99cfb52d333b6d502a59f4efccf691445ea5cd
|
|
7
|
+
data.tar.gz: e60033d763131eed72f6bf3dd5c2d323dbe2c450614865e53933384ee7d9a155ecbc19391fb222ad3eac3c61ed40dee8727272b9f29907143c4bc698ecb53f19
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Mountfd
|
|
2
|
+
|
|
3
|
+
Mountfd exposes Linux's file-descriptor-based mount API to Ruby. It builds a
|
|
4
|
+
mount while detached, applies its final attributes, and only then attaches it
|
|
5
|
+
to the mount namespace. This avoids the visibility and option-string problems
|
|
6
|
+
of the legacy `mount(2)` API.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
FsContext (fsopen) -> superblock (fsconfig) -> DetachedMount (fsmount)
|
|
10
|
+
|
|
|
11
|
+
existing mount ---------------- open_tree(CLONE) ---+
|
|
12
|
+
|
|
|
13
|
+
move_mount
|
|
14
|
+
v
|
|
15
|
+
mounted path
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The native extension calls `fsopen`, `fsconfig`, `fsmount`, `fspick`,
|
|
19
|
+
`open_tree`, `move_mount`, `mount_setattr`, `statmount`, `listmount`, and
|
|
20
|
+
`umount2` directly. It does not use Fiddle or FFI.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Ruby 3.2 or newer
|
|
25
|
+
- Linux 5.2 or newer for the new mount API
|
|
26
|
+
- Linux 5.12 or newer for `mount_setattr` and idmapped mounts
|
|
27
|
+
- Linux 6.8 or newer for `statmount`/`listmount`; older kernels use
|
|
28
|
+
`/proc/self/mountinfo`
|
|
29
|
+
- `CAP_SYS_ADMIN` in the owning user namespace, normally obtained through an
|
|
30
|
+
unprivileged user namespace or a privileged container
|
|
31
|
+
|
|
32
|
+
`require "mountfd"` succeeds on unsupported platforms so applications can use
|
|
33
|
+
feature detection:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Mountfd.supported? # false on macOS or a kernel older than 5.2
|
|
37
|
+
Mountfd.features # [:new_mount_api, :mount_setattr, :idmap, ...]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Known unsupported or restricted environments:
|
|
41
|
+
|
|
42
|
+
| Environment | Result |
|
|
43
|
+
|---|---|
|
|
44
|
+
| macOS and Windows | The gem builds and loads, but mount operations raise `UnsupportedError`. |
|
|
45
|
+
| WSL2 with a kernel older than 5.2 | The new mount syscalls are unavailable. |
|
|
46
|
+
| Docker Desktop | Operations affect the Linux VM/container namespace, never the host filesystem. |
|
|
47
|
+
| Unprivileged Docker | The default seccomp/capability policy commonly rejects mount and user-namespace operations. |
|
|
48
|
+
| Ubuntu 24.04+ | AppArmor may block unprivileged user namespaces via `kernel.apparmor_restrict_unprivileged_userns=1`. |
|
|
49
|
+
| GitHub-hosted runners | Unit tests work; system tests depend on the runner's user-namespace policy. |
|
|
50
|
+
|
|
51
|
+
Filesystem support for idmapped mounts is kernel-dependent. The safe,
|
|
52
|
+
source-free probe produced:
|
|
53
|
+
|
|
54
|
+
| kernel | tmpfs | ramfs | hugetlbfs |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| 5.10 | no/unavailable | no/unavailable | no/unavailable |
|
|
57
|
+
| 5.15 | no/unavailable | no/unavailable | no/unavailable |
|
|
58
|
+
| 6.1 | no/unavailable | no/unavailable | no/unavailable |
|
|
59
|
+
| 6.6 | yes | no/unavailable | no/unavailable |
|
|
60
|
+
| 6.8 | yes | no/unavailable | no/unavailable |
|
|
61
|
+
| 6.12 | yes | no/unavailable | no/unavailable |
|
|
62
|
+
|
|
63
|
+
Run `rake research:idmap_support` in the target environment. Filesystems that
|
|
64
|
+
need a block device or mount options are deliberately excluded from this safe
|
|
65
|
+
probe; the system suite separately verifies ext4 on a loop device on Linux
|
|
66
|
+
5.15 and 6.8.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bundle add mountfd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or install it directly with `gem install mountfd`.
|
|
75
|
+
|
|
76
|
+
## Create and attach a mount
|
|
77
|
+
|
|
78
|
+
The block form closes the filesystem context while returning ownership of the
|
|
79
|
+
detached mount:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
mount = Mountfd::FsContext.open("tmpfs") do |context|
|
|
83
|
+
context.set("size", "64M")
|
|
84
|
+
context.set_flag("noswap")
|
|
85
|
+
context.create!
|
|
86
|
+
context.mount(attrs: {nosuid: true, nodev: true})
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
mount.attach("/mnt/tmp")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Configuration failures include diagnostics read from the filesystem context:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
context = Mountfd::FsContext.new("tmpfs")
|
|
96
|
+
context.set("sizee", "64M")
|
|
97
|
+
# Mountfd::ConfigError: fsconfig: Invalid argument
|
|
98
|
+
# error: tmpfs: Unknown parameter 'sizee'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The high-level form performs the same lifecycle:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Mountfd.mount(
|
|
105
|
+
"tmpfs", "/mnt/tmp",
|
|
106
|
+
options: {size: "64M"},
|
|
107
|
+
attrs: {nosuid: true, nodev: true, atime: :noatime}
|
|
108
|
+
)
|
|
109
|
+
Mountfd.umount("/mnt/tmp")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Bind and idmapped mounts
|
|
113
|
+
|
|
114
|
+
`bind` uses `open_tree(OPEN_TREE_CLONE)`. Recursive attributes are applied to
|
|
115
|
+
the detached tree before it becomes visible:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Mountfd.bind("/src", "/dst", recursive: true, attrs: {rdonly: true})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Pass an existing user namespace or a mapping. Mapping ranges may use a hash or
|
|
122
|
+
an array of `[inside, outside, length]` triples:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Mountfd.bind(
|
|
126
|
+
"/data", "/container/data", recursive: true,
|
|
127
|
+
idmap: {
|
|
128
|
+
uid: {0 => [100_000, 65_536]},
|
|
129
|
+
gid: {0 => [100_000, 65_536]},
|
|
130
|
+
helper: :auto
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The triples are written to `uid_map`/`gid_map` in kernel order. When the caller
|
|
136
|
+
uses the initial user namespace, mapping an on-disk UID 100000 so it is reported
|
|
137
|
+
as UID 0 uses `{100_000 => [0, 1]}`. A container that itself maps root to host
|
|
138
|
+
UID 100000 normally uses `{0 => [100_000, 65_536]}` for both its user namespace
|
|
139
|
+
and mount.
|
|
140
|
+
|
|
141
|
+
`:auto` uses `newuidmap` and `newgidmap` for non-root callers when both are
|
|
142
|
+
installed. Requested ranges must also be delegated in `/etc/subuid` and
|
|
143
|
+
`/etc/subgid`.
|
|
144
|
+
|
|
145
|
+
## Mount discovery
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Mountfd.mounts # Array<Mountfd::MountInfo>
|
|
149
|
+
Mountfd.mount_at("/home") # MountInfo or nil
|
|
150
|
+
Mountfd.mounts_backend # :statmount or :mountinfo
|
|
151
|
+
Mountfd.mounts(ns: 1234) # parses /proc/1234/mountinfo
|
|
152
|
+
File.open("/proc/1234/ns/mnt") { Mountfd.mounts(ns: _1) } # statmount, Linux 6.11+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The mountinfo fallback decodes octal path escapes and handles the variable
|
|
156
|
+
optional-field section. A disappearing mount during `statmount` enumeration is
|
|
157
|
+
ignored as a normal race. An open mount namespace descriptor can be passed on
|
|
158
|
+
Linux 6.11 or newer; an integer namespace argument remains a process ID.
|
|
159
|
+
Generic attributes and propagation are normalized across both backends.
|
|
160
|
+
`source` or filesystem-specific `options` may be nil or empty when the running
|
|
161
|
+
kernel does not return the corresponding statmount field.
|
|
162
|
+
|
|
163
|
+
## Namespace helpers
|
|
164
|
+
|
|
165
|
+
Namespace changes affect the calling OS thread and are intended for a
|
|
166
|
+
single-threaded setup phase:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
Mountfd::Namespace.reexec_user! # robust entry path, including Ruby 3.4+
|
|
170
|
+
Mountfd::Namespace.unshare_user!(map_root: true)
|
|
171
|
+
Mountfd::Namespace.unshare_mount!(propagation: :private)
|
|
172
|
+
Mountfd.pivot_root(new_root, put_old)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`reexec_user!` restarts the current command through `unshare -Ur`; use it before
|
|
176
|
+
creating threads. The in-process `unshare_user!` is available when the Ruby
|
|
177
|
+
process has only one OS thread.
|
|
178
|
+
|
|
179
|
+
See `examples/` for an overlay mini-container, idmapped volume, read-only
|
|
180
|
+
sandbox with a writable tmpfs at `/tmp`, and `MOVE_MOUNT_BENEATH` atomic swap.
|
|
181
|
+
Set `MOUNTFD_LANDLOCK=1` when running `readonly_sandbox.rb` with the optional
|
|
182
|
+
`landlock` gem installed to restrict filesystem writes to `/tmp` (plus `/dev/null`)
|
|
183
|
+
as defense in depth. The exec-based examples use a supervising parent so their
|
|
184
|
+
temporary mount trees are removed after the command exits, including on a
|
|
185
|
+
nonzero exit status.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
bundle install
|
|
191
|
+
bundle exec rake test:unit
|
|
192
|
+
bundle exec rbs -I sig validate
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
System tests change mount namespaces and must run on Linux with user namespaces
|
|
196
|
+
enabled:
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
bundle exec rake test:system
|
|
200
|
+
bundle exec rake test:adversarial # 270-mount pagination and long statmount data
|
|
201
|
+
bundle exec rake test:ext4 # root plus loop-device access
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
For kernel-matrix testing, install `virtme-ng` and run:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
make -C tools/vm KVER=6.12.20
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The source-free idmap probe prints a Markdown table for the current kernel:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
bundle exec rake research:idmap_support
|
|
214
|
+
bundle exec rake benchmark:mounts # defaults to a namespace with 1000 mounts
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Scope
|
|
218
|
+
|
|
219
|
+
Mountfd intentionally does not wrap legacy `mount(2)`, FUSE mount helpers, or
|
|
220
|
+
systemd `.mount` units. Filesystem-specific `fsconfig` keys are passed to the
|
|
221
|
+
kernel without duplicating kernel validation.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
Mountfd is available under the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/extensiontask"
|
|
5
|
+
require "rspec/core/rake_task"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
Rake::ExtensionTask.new("mountfd") do |extension|
|
|
9
|
+
extension.lib_dir = "lib/mountfd"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
13
|
+
task.pattern = "spec/mountfd_spec.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RSpec::Core::RakeTask.new("spec:system") do |task|
|
|
17
|
+
task.pattern = "spec/system/**/*_spec.rb"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RSpec::Core::RakeTask.new("spec:ext4") do |task|
|
|
21
|
+
task.pattern = "spec/ext4/**/*_spec.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
YARD::Rake::YardocTask.new(:yard)
|
|
25
|
+
|
|
26
|
+
desc "Validate RBS signatures"
|
|
27
|
+
task :rbs do
|
|
28
|
+
sh "rbs", "-I", "sig", "validate"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
namespace :test do
|
|
32
|
+
task unit: :spec
|
|
33
|
+
task system: :compile do
|
|
34
|
+
ENV["MOUNTFD_SYSTEM"] = "1"
|
|
35
|
+
Rake::Task["spec:system"].invoke
|
|
36
|
+
end
|
|
37
|
+
task adversarial: :compile do
|
|
38
|
+
ENV["MOUNTFD_SYSTEM"] = ENV["MOUNTFD_EXTENSIVE"] = "1"
|
|
39
|
+
Rake::Task["spec:system"].invoke
|
|
40
|
+
end
|
|
41
|
+
task ext4: :compile do
|
|
42
|
+
ENV["MOUNTFD_EXT4"] = "1"
|
|
43
|
+
Rake::Task["spec:ext4"].invoke
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :research do
|
|
48
|
+
desc "Probe idmapped-mount support for source-free filesystems"
|
|
49
|
+
task idmap_support: :compile do
|
|
50
|
+
ruby "-Ilib", "tools/research_idmap.rb"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
namespace :benchmark do
|
|
55
|
+
desc "Compare statmount with mountinfo parsing in a 1000-mount namespace"
|
|
56
|
+
task mounts: :compile do
|
|
57
|
+
ruby "-Ilib", "benchmark/mounts.rb"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
namespace :gen do
|
|
62
|
+
desc "Generate mount constants from the installed Linux UAPI headers"
|
|
63
|
+
task :constants do
|
|
64
|
+
ruby "tools/generate_constants.rb"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
task spec: :compile
|
|
69
|
+
task default: "test:unit"
|
data/benchmark/mounts.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "mountfd"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
abort "Linux statmount support is required" unless Mountfd.features.include?(:statmount)
|
|
8
|
+
|
|
9
|
+
Mountfd::Namespace.reexec_user!
|
|
10
|
+
Mountfd::Namespace.unshare_mount!
|
|
11
|
+
count = Integer(ENV.fetch("MOUNTFD_BENCH_MOUNTS", "1000"))
|
|
12
|
+
iterations = Integer(ENV.fetch("MOUNTFD_BENCH_ITERATIONS", "10"))
|
|
13
|
+
|
|
14
|
+
measure = lambda do |label, &work|
|
|
15
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
|
+
work.call
|
|
17
|
+
puts format("%-20s %8.3f ms", label, (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1_000)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Dir.mktmpdir do |directory|
|
|
21
|
+
targets = count.times.map { File.join(directory, _1.to_s) }
|
|
22
|
+
targets.each { Dir.mkdir(_1); Mountfd.mount("tmpfs", _1) }
|
|
23
|
+
puts "#{Mountfd.mounts(backend: :statmount).length} visible mounts (#{count} created)"
|
|
24
|
+
measure.call("statmount/listmount") { iterations.times { Mountfd.mounts(backend: :statmount) } }
|
|
25
|
+
measure.call("parse mountinfo") do
|
|
26
|
+
iterations.times { Mountfd::MountInfoParser.parse(File.read("/proc/self/mountinfo")) }
|
|
27
|
+
end
|
|
28
|
+
ensure
|
|
29
|
+
targets&.reverse_each do |target|
|
|
30
|
+
Mountfd.umount(target)
|
|
31
|
+
rescue Errno::EINVAL, Errno::ENOENT
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Kernel notes
|
|
2
|
+
|
|
3
|
+
Results recorded on 2026-08-25 and 2026-08-26 while implementing the initial release.
|
|
4
|
+
|
|
5
|
+
| Environment | Result |
|
|
6
|
+
|---|---|
|
|
7
|
+
| macOS arm64, Ruby 4.0 | Native extension builds; `require` and unit tests pass; feature detection returns unsupported. |
|
|
8
|
+
| Linux arm64 container, Ruby 3.2/3.3/3.4 | Native extension builds; unit tests and RBS validation pass on every version. |
|
|
9
|
+
| Privileged Linux container | `fsopen(tmpfs)` → `fsconfig` → `fsmount` → `move_mount`, file I/O, and `umount2` pass in a private mount namespace. |
|
|
10
|
+
| Privileged Linux container | A misspelled tmpfs option returns `error: tmpfs: Unknown parameter 'sizee'`. |
|
|
11
|
+
| Privileged Linux container | `open_tree(CLONE)` plus `mount_setattr(MOUNT_ATTR_RDONLY)` rejects writes with `EROFS`. |
|
|
12
|
+
| Privileged Linux container | C-level user-namespace keeper returns a usable namespace fd and releases the keeper process. |
|
|
13
|
+
| Privileged Linux container | A tmpfs file owned by UID 100000 is reported as UID 0 through an idmapped bind using `100000 0 1`. |
|
|
14
|
+
| Linux 6.8.0-64 arm64 | `statmount` and mountinfo return the same visible mounts, including overmount filtering. |
|
|
15
|
+
| Linux 6.8.0-64 arm64 | `listmount` pagination passes with 270 new mounts; `statmount` buffer growth passes with more than 4 KiB of path data. |
|
|
16
|
+
| Linux 6.8.0-64 arm64 | 1000 explicit fd closes and `GC.stress` leave `/proc/self/fd` unchanged. |
|
|
17
|
+
| Linux 6.8.0-64 arm64 | Recursive read-only, attribute set/clear, propagation, detached discard, and atomic replacement tests pass. |
|
|
18
|
+
| Linux 6.8.0-64 arm64 | `MOVE_MOUNT_SET_GROUP`, close-error fd reuse, compacting-GC syscall arguments, and native binary-length validation pass. |
|
|
19
|
+
| Linux 6.8.0-64 arm64 | Invalid idmap fd/flag pairs are rejected before the syscall; the helper path passes through `posix_spawnp`. |
|
|
20
|
+
| Linux 6.8.0-64 arm64 | An ext4 loopback file owned by UID 1000 is UID 0 through an idmapped bind; an unmapped UID is 65534. |
|
|
21
|
+
| Linux 6.8.0-64 arm64, non-root | `newuidmap`/`newgidmap` create a namespace fd with delegated `100000:65536` sub-ID ranges. |
|
|
22
|
+
| Linux 6.8.0-64 arm64 | Safe source-free idmap probe: tmpfs yes; ramfs and hugetlbfs no/unavailable. |
|
|
23
|
+
| Linux 6.8.0-64 arm64 | 1000-mount benchmark, 10 iterations: statmount/listmount 30 ms; mountinfo parse 37 ms. |
|
|
24
|
+
| Linux 6.8.0-64 arm64, Ruby 3.4 | Adversarial suite: 24 examples, 0 failures, 2 expected pending. |
|
|
25
|
+
| Linux 6.8.0-64 arm64, Ruby 3.4, ASan/UBSan | Unit and system suites plus repeated syscall/GC-compaction stress pass without sanitizer findings. The intentional SIGSTOP helper test runs separately because ASan cannot supervise the stopped fork child. |
|
|
26
|
+
| Linux 5.10.0 arm64, virtme-ng/QEMU | System suite: 21 examples, 0 failures, 13 expected pending; `mount_setattr` and exclusive creation report `UnsupportedError`. |
|
|
27
|
+
| Linux 5.15.0 arm64, virtme-ng/QEMU | System suite: 21 examples, 0 failures, 8 expected pending; `MOVE_MOUNT_SET_GROUP` and ext4 loopback idmap pass. |
|
|
28
|
+
| Linux 6.1.0 arm64, virtme-ng/QEMU | System suite: 17 examples, 0 failures, 8 expected pending. |
|
|
29
|
+
| Linux 6.6.0 arm64, virtme-ng/QEMU | System suite: 17 examples, 0 failures, 6 expected pending; exclusive filesystem-context creation passes. |
|
|
30
|
+
| Linux 6.12.20 arm64, virtme-ng/QEMU | System suite: 17 examples, 0 failures, 4 expected pending; namespace-selected `listmount`/`statmount` pass. |
|
|
31
|
+
| Linux 5.10/5.15/6.1 arm64 | Source-free idmap probe: tmpfs, ramfs, and hugetlbfs no/unavailable. |
|
|
32
|
+
| Linux 6.6/6.12.20 arm64 | Source-free idmap probe: tmpfs yes; ramfs and hugetlbfs no/unavailable. |
|
|
33
|
+
| Linux 6.12.0 arm64, QEMU/TCG | A static initramfs probe passes `NS_GET_MNTNS_ID`, namespace-selected `listmount`, and namespace-selected `statmount`. |
|
|
34
|
+
| Linux 6.8.0-64 arm64 | The read-only sandbox preserves command status and removes its temporary tree; the idmapped-volume and overlay-container examples pass on ext4-backed test data. |
|
|
35
|
+
|
|
36
|
+
The checked-in VM harness remains the source of truth for older-kernel
|
|
37
|
+
regressions. The tested Ubuntu 6.12.0 image panics in netfs/9p before userspace,
|
|
38
|
+
so its new namespace ABI was isolated with a static initramfs; the full Ruby
|
|
39
|
+
suite passes on the 6.12.20 stable update. The harness disables PSI only for
|
|
40
|
+
5.10 because that kernel panics while current Ubuntu userspace initializes PSI.
|
|
41
|
+
Re-run the matrix before publishing a release.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
make -C tools/vm KVER=5.15
|
|
45
|
+
make -C tools/vm KVER=6.1
|
|
46
|
+
make -C tools/vm KVER=6.6
|
|
47
|
+
make -C tools/vm KVER=6.12.20
|
|
48
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mountfd"
|
|
4
|
+
|
|
5
|
+
target = File.expand_path(ARGV.fetch(0) { abort "usage: #{$PROGRAM_NAME} TARGET" })
|
|
6
|
+
Mountfd.replace(target) do
|
|
7
|
+
Mountfd::FsContext.open("tmpfs") do |context|
|
|
8
|
+
context.set("size", "64M")
|
|
9
|
+
context.create!
|
|
10
|
+
context.mount(attrs: {nosuid: true, nodev: true})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "mountfd"
|
|
5
|
+
|
|
6
|
+
source, target, outside = ARGV
|
|
7
|
+
abort "usage: #{$PROGRAM_NAME} SOURCE TARGET OUTSIDE_UID" unless outside
|
|
8
|
+
outside = Integer(outside)
|
|
9
|
+
FileUtils.mkdir_p(target)
|
|
10
|
+
|
|
11
|
+
Mountfd.bind(
|
|
12
|
+
File.expand_path(source), File.expand_path(target), recursive: true,
|
|
13
|
+
idmap: {
|
|
14
|
+
uid: {outside => [0, 65_536]},
|
|
15
|
+
gid: {outside => [0, 65_536]}
|
|
16
|
+
}
|
|
17
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "mountfd"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
lower = File.expand_path(ARGV.fetch(0) { abort "usage: #{$PROGRAM_NAME} ROOTFS [COMMAND ...]" })
|
|
9
|
+
command = ARGV.drop(1)
|
|
10
|
+
command = ["/bin/sh"] if command.empty?
|
|
11
|
+
|
|
12
|
+
Mountfd::Namespace.reexec_user!
|
|
13
|
+
directory = ENV["MOUNTFD_CONTAINER_ROOT"]
|
|
14
|
+
unless directory
|
|
15
|
+
status = Dir.mktmpdir("mountfd-container") do |root|
|
|
16
|
+
command = begin
|
|
17
|
+
File.binread("/proc/self/cmdline").split("\0")
|
|
18
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
19
|
+
[RbConfig.ruby, $PROGRAM_NAME, *ARGV]
|
|
20
|
+
end
|
|
21
|
+
command = [RbConfig.ruby, $PROGRAM_NAME, *ARGV] if command.empty?
|
|
22
|
+
pid = Process.spawn({"MOUNTFD_CONTAINER_ROOT" => root}, *command)
|
|
23
|
+
Process.wait2(pid).last
|
|
24
|
+
end
|
|
25
|
+
exit(status.exitstatus || 128 + status.termsig)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Mountfd::Namespace.unshare_mount!
|
|
29
|
+
upper, work, root = %w[upper work root].map { |name| File.join(directory, name) }
|
|
30
|
+
FileUtils.mkdir_p([upper, work, root])
|
|
31
|
+
Mountfd.mount(
|
|
32
|
+
"overlay", root,
|
|
33
|
+
options: {lowerdir: lower, upperdir: upper, workdir: work},
|
|
34
|
+
attrs: {nosuid: true, nodev: true}
|
|
35
|
+
)
|
|
36
|
+
Dir.chdir(root)
|
|
37
|
+
Mountfd.pivot_root(".", ".")
|
|
38
|
+
Mountfd.umount(".")
|
|
39
|
+
Dir.chdir("/")
|
|
40
|
+
exec(*command)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mountfd"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "landlock" if ENV["MOUNTFD_LANDLOCK"] == "1"
|
|
7
|
+
|
|
8
|
+
rootfs = File.expand_path(ARGV.fetch(0) { abort "usage: #{$PROGRAM_NAME} ROOTFS [COMMAND ...]" })
|
|
9
|
+
command = ARGV.drop(1)
|
|
10
|
+
command = ["/bin/sh"] if command.empty?
|
|
11
|
+
abort "#{rootfs}/tmp must exist" unless File.directory?(File.join(rootfs, "tmp"))
|
|
12
|
+
|
|
13
|
+
Mountfd::Namespace.reexec_user!
|
|
14
|
+
root = ENV["MOUNTFD_SANDBOX_ROOT"]
|
|
15
|
+
unless root
|
|
16
|
+
status = Dir.mktmpdir("mountfd-sandbox") do |directory|
|
|
17
|
+
command = begin
|
|
18
|
+
File.binread("/proc/self/cmdline").split("\0")
|
|
19
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
20
|
+
[RbConfig.ruby, $PROGRAM_NAME, *ARGV]
|
|
21
|
+
end
|
|
22
|
+
command = [RbConfig.ruby, $PROGRAM_NAME, *ARGV] if command.empty?
|
|
23
|
+
pid = Process.spawn({"MOUNTFD_SANDBOX_ROOT" => directory}, *command)
|
|
24
|
+
Process.wait2(pid).last
|
|
25
|
+
end
|
|
26
|
+
exit(status.exitstatus || 128 + status.termsig)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Mountfd::Namespace.unshare_mount!
|
|
30
|
+
Mountfd.bind(rootfs, root, recursive: true, attrs: {rdonly: true})
|
|
31
|
+
Mountfd.mount("tmpfs", File.join(root, "tmp"), attrs: {nosuid: true, nodev: true})
|
|
32
|
+
Dir.chdir(root)
|
|
33
|
+
Mountfd.pivot_root(".", ".")
|
|
34
|
+
Mountfd.umount(".")
|
|
35
|
+
Dir.chdir("/")
|
|
36
|
+
if ENV["MOUNTFD_LANDLOCK"] == "1"
|
|
37
|
+
Landlock.restrict!(read: ["/"], write: ["/tmp", "/dev/null"], execute: ["/"], allow_all_known: true)
|
|
38
|
+
end
|
|
39
|
+
exec(*command)
|