4. rootでruncにUserNSを作らせる場合 (dockerd --userns-remap など)
● 主要な部分は libcontainer/nsenter/nsexec.c に集中
● nsexecにてchildがやること: libcontainer/nsenter/nsexec.c:nsexec():JUMP_CHILD
○ unshare(CLONE_NEWUSER) を用いてUserNSを作成
○ parentとの通信用のFDに SYNC_USERMAP_PLS を書き込み、uid_map・gid_mapの設定を要求
○ SYNC_USERMAP_ACKを待ち、setresuid(0)してNS内でrootに昇格
case JUMP_CHILD:{
...
if (config.cloneflags & CLONE_NEWUSER) {
if (unshare(CLONE_NEWUSER) < 0)
bail("failed to unshare user namespace");
config.cloneflags &= ~CLONE_NEWUSER;
/*
* We don't have the privileges to do any mapping here (see the
* clone_parent rant). So signal our parent to hook us up.
*/
/* Switching is only necessary if we joined namespaces. */
if (config.namespaces) {
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
bail("failed to set process as dumpable");
}
s = SYNC_USERMAP_PLS;
if (write(syncfd, &s, sizeof(s)) != sizeof(s))
bail("failed to sync with parent: write(SYNC_USERMAP_PLS)");
/* ... wait for mapping ... */
if (read(syncfd, &s, sizeof(s)) != sizeof(s))
bail("failed to sync with parent: read(SYNC_USERMAP_ACK)");
if (s != SYNC_USERMAP_ACK)
bail("failed to sync with parent: SYNC_USERMAP_ACK: got %u", s);
/* Switching is only necessary if we joined namespaces. */
if (config.namespaces) {
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
bail("failed to set process as dumpable");
}
/* Become root in the namespace proper. */
if (setresuid(0, 0, 0) < 0)
bail("failed to become root in user namespace");
}
...
● nsexecにてparentがやること:
libcontainer/nsenter/nsexec.c:nsexec():JUMP_PARENT:SYNC_USERMAP_PLS
○ SYNC_USERMAP_PLS が来たら、update_uidmap(); update_gidmap(); して SYNC_USERMAP_ACK
を応答
○ update_uidmap(); update_gidmap(); は単に /proc/PID/uid_map、/proc/PID/gid_map に書き
込むだけ
○ parentはrootで動作しているので、setgroupsを無効化したり、newuidmap・newgidmap SUIDバイ
ナリを呼び出したりしなくてよい
case SYNC_USERMAP_PLS:
/*
* Enable setgroups(2) if we've been asked to. But we also
* have to explicitly disable setgroups(2) if we're
* creating a rootless container for single-entry mapping.
* i.e. config.is_setgroup == false.
* (this is required since Linux 3.19).
*
* For rootless multi-entry mapping, config.is_setgroup shall be true and
* newuidmap/newgidmap shall be used.
*/
if (config.is_rootless_euid && !config.is_setgroup)
update_setgroups(child, SETGROUPS_DENY);
5. /* Set up mappings. */
update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len);
update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len);
s = SYNC_USERMAP_ACK;
if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
kill(child, SIGKILL);
bail("failed to sync with child: write(SYNC_USERMAP_ACK)");
}
break;
● UserNSに入ったchildはデバイスノードをmknodできないので、ホストからbind-mount する:
libcontainer/rootfs_linux.go:createDevices()
func createDevices(config *configs.Config) error {
useBindMount := system.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER)
oldMask := unix.Umask(0000)
for _, node := range config.Devices {
// containers running in a user namespace are not allowed to mknod
// devices so we can just bind mount it from the host.
if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil {
unix.Umask(oldMask)
return err
}
}
unix.Umask(oldMask)
return nil
}
非rootでruncにUserNSを作らせる場合 (本来の “rootless runc”)
● git grep RootlessEUID 、 git grep -i rootless_euid 、git grep ‘os.Geteuid() != 0’ すると、
非rootでruncを動作させる場合のフローが見えてくる
● parentはrootを持っていないので、/proc/PID/uid_map、/proc/PID/gid_map にそれぞれ1エントリしか書
き込めない
○ 複数エントリがconfigで指定されている場合は、SUIDビットまたはfile capabilityがついたnewuidmap
、newgidmapバイナリを呼び出して対応表を書き込む
○ 単一のエントリしか指定されていない場合は、/proc/PID/setgroups に “deny” を書き込んでか
ら、/proc/PID/uid_map、/proc/PID/gid_map に書き込む
■ これが本来の”rootless runc”であるが、利用事例は稀
● Cgroup Managerがrootlessモードになる rootless_linux.go:shouldUseRootlessCgroupManager()
func shouldUseRootlessCgroupManager(context *cli.Context) (bool, error) {
...
if os.Geteuid() != 0 {
return true, nil
}
if !system.RunningInUserNS() {
// euid == 0 , in the initial ns (i.e. the real root)
return false, nil
}
// euid = 0, in a userns.
// As we are unaware of cgroups path, we can't determine whether we have the full
// access to the cgroups path.
// Either way, we can safely decide to use the rootless cgroups manager.
return true, nil
}
● rootlessモードのCgroup managerは、パーミッション関連のエラーを無視する:
libcontainer/cgroups/fs/apply_raw.go:*Manager.Apply()
○ Cgroupを使いたいなら、予めrootでchmod・chownしておく必要がある
■ Cgroup v1では非推奨
func (m *Manager) Apply(pid int) (err error) {
...
for _, sys := range m.getSubsystems() {
...
if err := sys.Apply(d); err != nil {
// In the case of rootless (including euid=0 in userns), where an explicit cgroup path
6. hasn't
// been set, we don't bail on error in case of permission problems.
// Cases where limits have been set (and we couldn't create our own
// cgroup) are handled by Set.
if isIgnorableError(m.Rootless, err) && m.Cgroups.Path == "" {
delete(m.Paths, sys.Name())
continue
}
return err
}
}
return nil
}
● checkpointやAppArmorは使えない
● UserNSと一緒にNetNSもunshareしたいなら工夫が必要 (「既存UserNS内でruncを実行する場合」参照)
既存UserNS内でruncを実行する場合 (Rootless Dockerなど)
● runcの外側で予めUserNSを作っておく必要がある
○ Docker、k3s、BuildKitなどのRootlessモードではRootlessKitが使われる
○ PodmanのRootlessモードではPodman自身がUserNSを作成する
○ UserNSと一緒にNetNSもunshareしたいなら工夫が必要
■ NetNSをunshareしないと、コンテナ内のプロセスからコンテナ外の抽象UNIXソケットにア
クセスできてしまう
● →containerdのbreakoutに繋がる
■ 方法1: SUIDバイナリでNetNSを設定 (lxc-user-nic)
■ 方法2: NetNS内にTAPデバイスを作り、ユーザモードでTCP/IPをエミュレート (slirp4netns、
VPNKit)
■ RootlessKit系はlxc-user-nic、slirp4netns、VPNKitに対応
■ Podmanはslirp4netnsに対応
● git grep RunningInUserNS すると、非rootでruncを動作させる場合のフローが見えてくる
○ cgroup、checkpoint、AppArmorが使えない以外は、普通にrootでruncを動作させる場合とあまり変
わらない
runcを既存UserNSにjoinさせる場合
● 既存のUserNSのpathを指定して、nsenterさせることができる
{
...
"namespaces": [
{
"type": "user",
"path": "/proc/42/ns/user"
},
...
}
● 利用事例は稀
○ podman run --userns container:foo などで利用されている