kata agent

root@ubuntu:/home/ubuntu# kata-runtime exec e12a7db6fb05df044a59a19bb03c39fe7752e4d684a8e2e58822b88606d3ac3e
rpc error: code = Internal desc = Could not run process: container_linux.go:349: starting container process caused "panic from initialization: runtime error: index out of range, goroutine 1 [running, locked to thread]:\nruntime/debug.Stack(0x400018fbd8, 0xaaaab1b68260, 0xaaaab21de220)\n\t/usr/go/src/runtime/debug/stack.go:24 +0x88\n
github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*LinuxFactory).StartInitialization.func2(0x400018fea0)\n\t/root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/factory_linux.go:370
+0x40\npanic(0xaaaab1b68260, 0xaaaab21de220)\n\t/usr/go/src/runtime/panic.go:513 +0x18c\ngithub.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*linuxSetnsInit).Init(0x400012d9c0, 0x0, 0x0)\n\t/root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/setns_init_linux.go:91 +0x434\ngithub.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*LinuxFactory).StartInitialization(0x4000164090, 0x0, 0x0)\n\t/root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/factory_linux.go: 380 +0x2ec\nmain.init.0()\n\t/root/go/src/github.com/kata-containers/agent/agent.go:1506 +0x88\n" root@ubuntu:/home/ubuntu#

 

 

 

【kubernetes/k8s源码分析】kata container agent create container 源码分析

https://blog.csdn.net/zhonglinzhang/article/details/101212033

linuxStandardInit.Init()(github.com/opencontainers/runc/libcontainer/standard_init_linux.go#47):

func (l *linuxStandardInit) Init() error {
    // 这里比较重要的是这个函数,此时各个 Namespace 虽然都挂载完毕了,但是当前的进程的视角里根目录和容器外是一样的
    // 因此这个方法会挂载设备,bind mount,然后将当前根目录切换到容器的根目录下。
	if err := prepareRootfs(l.pipe, l.config); err != nil {
		return err
	}

	// 设置 root (/) 为只读
	if l.config.Config.Namespaces.Contains(configs.NEWNS) {
		if err := finalizeRootfs(l.config.Config); err != nil {
			return err
		}
	}

	// 在完成一系列容器内的环境准备之后,通过 execve 执行容器内的 entrypoint
	if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
		return newSystemErrorWithCause(err, "exec user process")
	}
	return nil
}

总结:

  • runc init 一个会有三个进程
    • 第一个进程读取 bootstrapData,并完成第二个进程的 user map 的设置
    • 第二个进程完成 namespace 的设置
    • 第三个进程完成 CGROUP namesapce 的设置,并读取了 0x80 的同步信息。最后进入 go 代码。go 代码读取 container config,进行容器内环境准备,最后执行容器的 entrypoint
      • 47 func (l *linuxStandardInit) Init() error {
         48         runtime.LockOSThread()
         49         defer runtime.UnlockOSThread()
         50         if !l.config.Config.NoNewKeyring {
         51                 if err := label.SetKeyLabel(l.config.ProcessLabel); err != nil {
         52                         return err
         53                 }
         54                 defer label.SetKeyLabel("")
         55                 ringname, keepperms, newperms := l.getSessionRingParams()
         56 
         57                 // Do not inherit the parent's session keyring.
         58                 if sessKeyId, err := keys.JoinSessionKeyring(ringname); err != nil {
         59                         // If keyrings aren't supported then it is likely we are on an
         60                         // older kernel (or inside an LXC container). While we could bail,
         61                         // the security feature we are using here is best-effort (it only
         62                         // really provides marginal protection since VFS credentials are
         63                         // the only significant protection of keyrings).
         64                         //
         65                         // TODO(cyphar): Log this so people know what's going on, once we
         66                         //               have proper logging in 'runc init'.
         67                         if errors.Cause(err) != unix.ENOSYS {
         68                                 return errors.Wrap(err, "join session keyring")
         69                         }
         70                 } else {
         71                         // Make session keyring searcheable. If we've gotten this far we
         72                         // bail on any error -- we don't want to have a keyring with bad
         73                         // permissions.
         74                         if err := keys.ModKeyringPerm(sessKeyId, keepperms, newperms); err != nil {
         75                                 return errors.Wrap(err, "mod keyring permissions")
         76                         }
         77                 }
         78         }
         79 
         80         if err := setupNetwork(l.config); err != nil {
         81                 return err
         82         }
         83         if err := setupRoute(l.config.Config); err != nil {
         84                 return err
         85         }
        
        
        
        
         86 
         87         label.Init()
         88         if err := prepareRootfs(l.pipe, l.config); err != nil {
         89                 return err
         90         }
         91         // Set up the console. This has to be done *before* we finalize the rootfs,
         92         // but *after* we've given the user the chance to set up all of the mounts
         93         // they wanted.
         94         if l.config.CreateConsole {
         95                 if err := setupConsole(l.consoleSocket, l.config, true); err != nil {
         96                         return err
         97                 }
         98                 if err := system.Setctty(); err != nil {
         99                         return errors.Wrap(err, "setctty")
        100                 }
        101         }
        102 
        103         // Finish the rootfs setup.
        104         if l.config.Config.Namespaces.Contains(configs.NEWNS) {
        105                 if err := finalizeRootfs(l.config.Config); err != nil {
        106                         return err
        107                 }
        108         }
        109 
        110         if hostname := l.config.Config.Hostname; hostname != "" {
        111                 if err := unix.Sethostname([]byte(hostname)); err != nil {
        112                         return errors.Wrap(err, "sethostname")
        113                 }
        114         }
        115         if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
        116                 return errors.Wrap(err, "apply apparmor profile")
        117         }
        118 
        119         for key, value := range l.config.Config.Sysctl {
        120                 if err := writeSystemProperty(key, value); err != nil {
        121                         return errors.Wrapf(err, "write sysctl key %s", key)
        122                 }
        123         }
        124         for _, path := range l.config.Config.ReadonlyPaths {
        125                 if err := readonlyPath(path); err != nil {
        126                         return errors.Wrapf(err, "readonly path %s", path)
        127                 }
        128         }
        129         for _, path := range l.config.Config.MaskPaths {
        130                 if err := maskPath(path, l.config.Config.MountLabel); err != nil {
        131                         return errors.Wrapf(err, "mask path %s", path)
        132                 }
        133         }
        134         pdeath, err := system.GetParentDeathSignal()
        135         if err != nil {
        136                 return errors.Wrap(err, "get pdeath signal")
        137         }
        138         if l.config.NoNewPrivileges {
        139                 if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
        140                         return errors.Wrap(err, "set nonewprivileges")
        141                 }
        142         }
        143         // Tell our parent that we're ready to Execv. This must be done before the
        144         // Seccomp rules have been applied, because we need to be able to read and
        145         // write to a socket.
        146         if err := syncParentReady(l.pipe); err != nil {
        147                 return errors.Wrap(err, "sync ready")
        148         }
        149         if err := label.SetProcessLabel(l.config.ProcessLabel); err != nil {
        150                 return errors.Wrap(err, "set process label")
        151         }
        152         defer label.SetProcessLabel("")
        153         // Without NoNewPrivileges seccomp is a privileged operation, so we need to
        154         // do this before dropping capabilities; otherwise do it as late as possible
        155         // just before execve so as few syscalls take place after it as possible.
        156         if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
        157                 if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
        158                         return err
        159                 }
        160         }
        161         if err := finalizeNamespace(l.config); err != nil {
        162                 return err
        163         }
        164         // finalizeNamespace can change user/group which clears the parent death
        165         // signal, so we restore it here.
        166         if err := pdeath.Restore(); err != nil {
        167                 return errors.Wrap(err, "restore pdeath signal")
        168         }
        169         // Compare the parent from the initial start of the init process and make
        170         // sure that it did not change.  if the parent changes that means it died
        171         // and we were reparented to something else so we should just kill ourself
        172         // and not cause problems for someone else.
        173         if unix.Getppid() != l.parentPid {
        174                 return unix.Kill(unix.Getpid(), unix.SIGKILL)
        175         }
        176         // Check for the arg before waiting to make sure it exists and it is
        177         // returned as a create time error.
        178         name, err := exec.LookPath(l.config.Args[0])
        179         if err != nil {
        180                 return err
        181         }
        182         // Close the pipe to signal that we have completed our init.
        183         l.pipe.Close()
        184         // Wait for the FIFO to be opened on the other side before exec-ing the
        185         // user process. We open it through /proc/self/fd/$fd, because the fd that
        186         // was given to us was an O_PATH fd to the fifo itself. Linux allows us to
        187         // re-open an O_PATH fd through /proc.
        188         fd, err := unix.Open(fmt.Sprintf("/proc/self/fd/%d", l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
        189         if err != nil {
        190                 return newSystemErrorWithCause(err, "open exec fifo")
        191         }
        192         if _, err := unix.Write(fd, []byte("0")); err != nil {
        193                 return newSystemErrorWithCause(err, "write 0 exec fifo")
        194         }
        195         // Close the O_PATH fifofd fd before exec because the kernel resets
        196         // dumpable in the wrong order. This has been fixed in newer kernels, but
        197         // we keep this to ensure CVE-2016-9962 doesn't re-emerge on older kernels.
        198         // N.B. the core issue itself (passing dirfds to the host filesystem) has
        199         // since been resolved.
        200         // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318
        201         unix.Close(l.fifoFd)
        202         // Set seccomp as close to execve as possible, so as few syscalls take
        203         // place afterward (reducing the amount of syscalls that users need to
        204         // enable in their seccomp profiles).
        205         if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
        206                 if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
        207                         return newSystemErrorWithCause(err, "init seccomp")
        208                 }
        209         }
        210         if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
        211                 return newSystemErrorWithCause(err, "exec user process")
        212         }
        213         return nil
        214 }

          调用Init

          •         i, err := newContainerInit(it, pipe, consoleSocket, fifofd)
                    if err != nil {
                            return err
                    }
            
                    // If Init succeeds, syscall.Exec will not return, hence none of the defers will be called.
                    return i.Init()
            }

             

      • // Shared function between CreateContainer and ExecProcess, because those expect
        // a process to be run.
        func (a *agentGRPC) execProcess(ctr *container, proc *process, createContainer bool) (err error) {
                if ctr == nil {
                        return grpcStatus.Error(codes.InvalidArgument, "Container cannot be nil")
                }
        
                if proc == nil {
                        return grpcStatus.Error(codes.InvalidArgument, "Process cannot be nil")
                }
        
                // This lock is very important to avoid any race with reaper.reap().
                // Indeed, if we don't lock this here, we could potentially get the
                // SIGCHLD signal before the channel has been created, meaning we will
                // miss the opportunity to get the exit code, leading WaitProcess() to
                // wait forever on the new channel.
                // This lock has to be taken before we run the new process.
                a.sandbox.subreaper.lock()
                defer a.sandbox.subreaper.unlock()
        
                if createContainer {
                        err = ctr.container.Start(&proc.process)
                } else {
                        err = ctr.container.Run(&(proc.process))
                }
                if err != nil {
                        return grpcStatus.Errorf(codes.Internal, "Could not run process: %v", err)
                }

        vendor/github.com/opencontainers/runc/libcontainer/container_linux.go +233

        • func (c *linuxContainer) Start(process *Process) error {
                  c.m.Lock()
                  defer c.m.Unlock()
                  if process.Init {
                          if err := c.createExecFifo(); err != nil {
                                  return err
                          }
                  }
                  if err := c.start(process); err != nil {
                          if process.Init {
                                  c.deleteExecFifo()
                          }
                          return err
                  }
                  return nil
          }
          
          func (c *linuxContainer) Run(process *Process) error {
                  if err := c.Start(process); err != nil {
                          return err
                  }
                  if process.Init {
                          return c.exec()
                  }
                  return nil
          }

           

        • newParentProcess 函数
          
          创建一对pipe,parentPipe和childPipe,作为 start 进程与容器内部 init 进程通信管道
          创建一个命令模版作为 Parent 进程启动的模板
          newInitProcess 封装 initProcess。主要工作为添加初始化类型环境变量,将namespace、uid/gid 映射等信息使用 bootstrapData 封装为一个 io.Reader
                 initProcess 实现了 parentProcess 接口

           

        • func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
              parentInitPipe, childInitPipe, err := utils.NewSockPair("init")
              if err != nil {
                  return nil, newSystemErrorWithCause(err, "creating new init pipe")
              }
              messageSockPair := filePair{parentInitPipe, childInitPipe}
           
              parentLogPipe, childLogPipe, err := os.Pipe()
              if err != nil {
                  return nil, fmt.Errorf("Unable to create the log pipe:  %s", err)
              }
              logFilePair := filePair{parentLogPipe, childLogPipe}
           
              cmd, err := c.commandTemplate(p, childInitPipe, childLogPipe)
              if err != nil {
                  return nil, newSystemErrorWithCause(err, "creating new command template")
              }
              if !p.Init {
                  return c.newSetnsProcess(p, cmd, messageSockPair, logFilePair)
              }
           
              // We only set up fifoFd if we're not doing a `runc exec`. The historic
              // reason for this is that previously we would pass a dirfd that allowed
              // for container rootfs escape (and not doing it in `runc exec` avoided
              // that problem), but we no longer do that. However, there's no need to do
              // this for `runc exec` so we just keep it this way to be safe.
              if err := c.includeExecFifo(cmd); err != nil {
                  return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
              }
              return c.newInitProcess(p, cmd, messageSockPair, logFilePair)

           

        • agent.go

        • func main() {
                  defer handlePanic()
          
                  err := realMain()
                  if err != nil {
                          agentLog.WithError(err).Error("agent failed")
                          os.Exit(1)
                  }
          
                  agentLog.Debug("agent exiting")
          
                  os.Exit(0)
          }

           

        • initProcess start 函数
          
               创建新的进程。而此时新的进程使用 /proc/self/exec 为执行入口,参数为 init,会在 main 函数调用之前执行,所以在新的进程中 func init() 会直接调用,而不会去执行main函数
          
          func (p *initProcess) start() error {
              defer p.messageSockPair.parent.Close()
              err := p.cmd.Start()
              p.process.ops = p
              // close the write-side of the pipes (controlled by child)
              p.messageSockPair.child.Close()
              p.logFilePair.child.Close()
              if err != nil {
                  p.process.ops = nil
                  return newSystemErrorWithCause(err, "starting init process command")
              }
                   cmd 如最后命令所示,Path填充为 /proc/self/exe(本身 agent)。参数字段 Args 为 init,
          表示对容器进行初始化,调用的为 agent init
          agent 最后直接复用 runc 代码

           

        • func init() {
                  if len(os.Args) > 1 && os.Args[1] == "init" {
                          runtime.GOMAXPROCS(1)
                          runtime.LockOSThread()
                          factory, _ := libcontainer.New("")
                          if err := factory.StartInitialization(); err != nil {
                                  agentLog.WithError(err).Error("init failed")
                          }
                          panic("--this line should have never been executed, congratulations--")
                  }
          }
        • runc 启动容器过程分析(附 CVE-2019-5736 实现过程)

        • 环境

          OCI runtime spec 地址:https://github.com/opencontainers/runtime-spec
          runc 地址:https://github.com/opencontainers/runc/
          Commit:f414f497b50a61750ea3af9fccf998a3db687cea
          系统版本:Fedora Release 28
          内核版本:4.17.9-200.fc28.x86_64

          runc 介绍

          runc 实现了 OCI 的容器标准,能够管理容器的生命周期。runc 的详细功能请参考 帮助文档

          runc 不是基于 server 形式的,所以所有的配置和状态都会存储在本地文件系统中(以下均为使用 docker 时的默认路径):

          • 容器配置:/run/docker/libcontainerd/{cnotainer-id}/config.json
          • 容器 init 进程的标准输入输出流:/run/docker/libcontainerd/{cnotainer-id}/{init-stdin,init-stdout,init-stderr}
          • 容器状态信息:/run/runc/*/state.json

          runc 创建容器时会将状态记录到 state.json 中,所有查询都是从 state.json 中取得容器基本信息,然后再从系统中获取容器实时状态。

          docker 的调用链如下:

          docker-client -> dockerd -> docker-containerd -> docker-containerd-shim -> runc(容器外) -> runc(容器内) -> containter-entrypoint
          

          runc 启动容器过程

          runc 在被 docker-containerd-shim 调用时,参数中会指定容器的配置路径(即 config.json 的位置),同时容器的根路径也已经准备完毕,因此 runc 不会有跟镜像相关的概念。容器的启动过程分析直接从 runc run 开始,即 docker 调用链中的 runc(容器外)这个时间点。

          runc(容器外)环境准备

          读取 config.json(github.com/opencontainers/runc/run.go#65):

          // 读取 config.json
          spec, err := setupSpec(context)
          if err != nil {
          	return err
          }
          // 启动容器
          status, err := startContainer(context, spec, CT_ACT_RUN, nil)
          if err == nil {
          	os.Exit(status)
          }
          return err
          

          startContainer 创建容器信息,并启动(github.com/opencontainers/runc/utils_linux.go#396):

          func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
              // 通过 spec 创建容器结构,在 createContainer 中将 spec 转换为了 runc 的 container config
          	container, err := createContainer(context, id, spec)
          	if err != nil {
          		return -1, err
          	}
              // 构建 runner 启动容器
          	r := &runner{
          		// 容器
          		container:       container,
          		// 即 CT_ACT_RUN
          		action:          action,
          		// 用于设置 process.Init 字段
          		init:            true,
          	}
          	return r.run(spec.Process)
          }
          

          r.run() 启动容器(github.com/opencontainers/runc/utils_linux.go#268):

          func (r *runner) run(config *specs.Process) (int, error) {
          	// 根据 config 构建容器进程,此处 r.init 为 true
          	process, err := newProcess(*config, r.init)
          	if err != nil {
          		r.destroy()
          		return -1, err
          	}
          
              // 根据 action 调用 container 的对应方法
          	switch r.action {
          	case CT_ACT_CREATE:
          		err = r.container.Start(process)
          	case CT_ACT_RESTORE:
          		err = r.container.Restore(process, r.criuOpts)
              case CT_ACT_RUN:
                  // 此处调用的是这个方法
          		err = r.container.Run(process)
          	default:
          		panic("Unknown action")
          	}
          }
          

          container 是由 createContainer() 方法创建,根据创建链路 createContainer() -> loadFactory() -> libcontainer.New() 确认容器由 LinuxFactory.Create() 创建:

          // github.com/opencontainers/runc/libcontainer/factory_linux.go#132
          func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
          	l := &LinuxFactory{
                  // 指向当前的 exe 程序,即 runc 本身
                  InitPath:  "/proc/self/exe",
                  // os.Args[0] 是当前 runc 的路径,本质上和 InitPath 是一样的,即 runc init
          		InitArgs:  []string{os.Args[0], "init"},
          	}
          	return l, nil
          }
          
          // github.com/opencontainers/runc/libcontainer/factory_linux.go#189
          func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
              // 创建 linux 容器结构
          	c := &linuxContainer{
                  // 容器 ID
                  id:            id,
                  // 容器状态文件存放目录,默认是 /run/runc/{容器 id}/
                  root:          containerRoot,
                  // 容器配置
                  config:        config,
                  // 即 /proc/self/exe,就是 runc
                  initPath:      l.InitPath,
                  // 即 runc init
          		initArgs:      l.InitArgs,
          	}
          	return c, nil
          }
          

          所以整个容器的启动逻辑在 linuxContainer.Run() 里,调用链是 linuxContainer.Run() -> linuxContainer.Start() -> linuxContainer.start():

          // github.com/opencontainers/runc/libcontainer/container_linux.go#334
          func (c *linuxContainer) start(process *Process) error {
              // process 是容器的 entrypoint,此处创建的是 entrypoint 的父进程
          	parent, err := c.newParentProcess(process)
          	if err != nil {
          		return newSystemErrorWithCause(err, "creating new parent process")
              }
              // 启动父进程
          	if err := parent.start(); err != nil {
          		// terminate the process to ensure that it properly is reaped.
          		if err := ignoreTerminateErrors(parent.terminate()); err != nil {
          			logrus.Warn(err)
          		}
          		return newSystemErrorWithCause(err, "starting container process")
          	}
          }
          
          func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
              // 创建用于父子进程通信的 pipe
          	parentPipe, childPipe, err := utils.NewSockPair("init")
          	if err != nil {
          		return nil, newSystemErrorWithCause(err, "creating new init pipe")
              }
              // 创建父进程的 cmd
          	cmd, err := c.commandTemplate(p, childPipe)
          	if err != nil {
          		return nil, newSystemErrorWithCause(err, "creating new command template")
          	}
          	if !p.Init {
                  // 由于 p.Init 为 true,所以不会执行到这里
          		return c.newSetnsProcess(p, cmd, parentPipe, childPipe)
          	}
          
              // 返回标准 init 进程
          	return c.newInitProcess(p, cmd, parentPipe, childPipe)
          }
          
          func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
              // 这里可以看到 cmd 就是 runc init
          	cmd := exec.Command(c.initPath, c.initArgs[1:]...)
              cmd.Args[0] = c.initArgs[0]
              // 将设置给容器 entrypoint 的 std 流给了 runc init 命令,这些流最终会通过 runc init 传递给 entrypoint 
          	cmd.Stdin = p.Stdin
          	cmd.Stdout = p.Stdout
              cmd.Stderr = p.Stderr
              
              // 这个 childPipe 用于跟父进程通信(父进程就是当前这个 runc 进程)
              cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe)
              // 通过环境变量 _LIBCONTAINER_INITPIPE 把 fd 号传递给 runc init,由于 std 流会占用前三个 fd 编号(0,1,2)
              // 所以 fd 要加上 3(stdioFdCount)
              cmd.Env = append(cmd.Env,
          		fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
          	)
          	return cmd, nil
          }
          
          func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
              // 这里通过环境变量 _LIBCONTAINER_INITTYPE 设置 init 类型为 standard(initStandard)
          	cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
          	nsMaps := make(map[configs.NamespaceType]string)
          	for _, ns := range c.config.Namespaces {
          		if ns.Path != "" {
          			nsMaps[ns.Type] = ns.Path
          		}
          	}
              _, sharePidns := nsMaps[configs.NEWPID]
              // 构造 namespace 设置,然后序列化成字节数据
          	data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
          	if err != nil {
          		return nil, err
          	}
          	init := &initProcess{
          		cmd:             cmd,
          		childPipe:       childPipe,
          		parentPipe:      parentPipe,
          		manager:         c.cgroupManager,
                  intelRdtManager: c.intelRdtManager,
                  
          		config:          c.newInitConfig(p),
          		container:       c,
          		process:         p,
          		bootstrapData:   data,
          		sharePidns:      sharePidns,
          	}
          	c.initProcess = init
          	return init, nil
          }
          

          在 linuxContainer.start() 中,创建了一个命令是 runc init 的初始化进程(initProcess),并启动了该进程,这里是 runc(容器外)的最核心的逻辑:

          // github.com/opencontainers/runc/libcontainer/process_linux.go#262
          func (p *initProcess) start() error {
              defer p.parentPipe.Close()
              // 启动了 cmd,即启动了 runc init
          	err := p.cmd.Start()
          	p.process.ops = p
          	p.childPipe.Close()
          	if err != nil {
          		p.process.ops = nil
          		return newSystemErrorWithCause(err, "starting init process command")
          	}
          
              // 将 bootstrapData 写入到 parent pipe 中,此时 runc init 可以从 child pipe 里读取到这个数据
          	if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {
          		return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
              }
              
              // 获取子进程的 PID,即 runc init 的 PID
              childPid, err := p.getChildPid()
          	if err != nil {
          		return newSystemErrorWithCause(err, "getting the final child's pid from pipe")
          	}
          
          	// 如果子容器的配置中要求创建新的 CGROUP Namespace,那么这里还要向 parent pipe 写入一个字节的数据 0x80(createCgroupns)
          	if p.config.Config.Namespaces.Contains(configs.NEWCGROUP) && p.config.Config.Namespaces.PathOf(configs.NEWCGROUP) == "" {
          		if _, err := p.parentPipe.Write([]byte{createCgroupns}); err != nil {
          			return newSystemErrorWithCause(err, "sending synchronization value to init process")
          		}
          	}
          
          	// 等待 runc init 退出
          	if err := p.waitForChildExit(childPid); err != nil {
          		return newSystemErrorWithCause(err, "waiting for our first child to exit")
          	}
              
              // 向 parent pipe 中写入 container config,也就是把容器配置传递给了 runc init
              // 为什么 runc init 都退出了,还要往里面写配置?==》这个问题下面说到 runc init 的时候再解释
          	if err := p.sendConfig(); err != nil {
          		return newSystemErrorWithCause(err, "sending config to init process")
          	}
          	var (
          		sentRun    bool
          		sentResume bool
          	)
              // 从 parent pipe 中读取来自 runc init 的同步消息
          	ierr := parseSync(p.parentPipe, func(sync *syncT) error {
          		...
          		return nil
          	})
          	return nil
          }
          

          总结:

          • runc 被 docker-containerd-shim 调用后,从 config.json 中读取 container spec,并转换成内部 config
          • 这个 runc 在外部运行,拥有 root 权限
          • runc 启动了一个子进程,runc init,然后通过 pipe 将 bootstrapData(含有 namespace 信息),0x80(NEWCGROUP),容器 config 传输给 runc init,并开始等待 runc init 的同步消息

          runc(容器内)启动过程

          原则上来说,容器外的 runc 启动的 runc init 仍然是在容器外部的,但是它会逐步的限制自身的 namespace 来构建容器环境,因此这里直接算作容器内的 runc。

          runc init 命令启动:

          package main
          
          import (
          	"os"
          	"runtime"
          
              "github.com/opencontainers/runc/libcontainer"
              // 这个包非常重要,是 runc init 启动的基石
          	_ "github.com/opencontainers/runc/libcontainer/nsenter"
          	"github.com/urfave/cli"
          )
          
          func init() {
          	if len(os.Args) > 1 && os.Args[1] == "init" {
          		runtime.GOMAXPROCS(1)
          		runtime.LockOSThread()
          	}
          }
          
          var initCommand = cli.Command{
          	Name:  "init",
          	Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
          	Action: func(context *cli.Context) error {
                  // 构造了一个空的 factory
                  factory, _ := libcontainer.New("")
                  // 初始化容器环境
          		if err := factory.StartInitialization(); err != nil {
          			os.Exit(1)
          		}
          		panic("libcontainer: container init failed to exec")
          	},
          }
          

          由于 nsenter 包被匿名引入,而且利用了 GCC 构造器特性,导致 go 的代码最后才会执行,因此先看 nsenter 包的代码(github.com/opencontainers/runc/libcontainer/nsenter/nsenter.go):

          // +build linux,!gccgo
          
          package nsenter
          
          /*
          #cgo CFLAGS: -Wall
          extern void nsexec();
          void __attribute__((constructor)) init(void) {
          	nsexec();
          }
          */
          import "C"
          

          这个代码利用了 GCC 的 constructor 特性,init 会在 runtimel.main()(不是 main.main()) 函数之前执行, 这样保证了启动时是单线程的,这一点很重要。因为 linux 不允许在多线程中通过 setns 设置 user namespace。

          这个初始化函数调用了 nsexec()(github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c#540):

          void nsexec(void)
          {
          	int pipenum;
          	jmp_buf env;
          	int sync_child_pipe[2], sync_grandchild_pipe[2];
          	struct nlconfig_t config = { 0 };
          
          	// 从环境变量 _LIBCONTAINER_INITPIPE 中取得 child pipe 的 fd 编号
          	pipenum = initpipe();
              if (pipenum == -1)
                  // 由于正常启动的 runc 是没有这个环境变量的,所以这里会直接返回,然后就开始正常的执行 go 程序了
          		return;
          
              // 确保当前的二进制文件是已经复制过的,用来规避 CVE-2019-5736 漏洞
              // ensure_cloned_binary 中使用了两种方法:
              // - 使用 memfd,将二进制文件写入 memfd,然后重启 runc
              // - 复制二进制文件到临时文件,然后重启 runc
          	if (ensure_cloned_binary() < 0)
          		bail("could not ensure we are a cloned binary");
          
          	// 从 child pipe 中读取 namespace config
          	nl_parse(pipenum, &config);
          
          	// 设置 oom score,这个只能在特权模式下设置,所以在这里就要修改完成
          	update_oom_score_adj(config.oom_score_adj, config.oom_score_adj_len);
          
          	// 设置不可 dump
          	if (config.namespaces) {
          		if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
          			bail("failed to set process as non-dumpable");
          	}
          
          	// 创建和子进程通信的 pipe,为什么有这个 pipe,下面解释
          	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0)
          		bail("failed to setup sync pipe between parent and child");
          
          	// 创建和孙进程通信的 pipe,为什么有这个 pipe,下面解释
          	if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
                  bail("failed to setup sync pipe between parent and grandchild");
              
              // setjmp 将当前执行位置的环境保存下来,用于多进程环境下的程序跳转
              // 第一次执行的时候 setjmp 返回 0,对应 JUMP_PARENT
          	switch (setjmp(env)) {
          	case JUMP_PARENT:{
          			int len;
          			pid_t child, first_child = -1;
          			bool ready = false;
          
          			/* For debugging. */
          			prctl(PR_SET_NAME, (unsigned long)"runc:[0:PARENT]", 0, 0, 0);
          
                      // clone_parent 创建了和当前进程完全一致的一个进程(子进程)
                      // 在 clone_parent 中,通过 longjmp() 跳转到 env 保存的位置
                      // 并且 setjmp 返回值为 JUMP_CHILD
                      // 这样这个子进程就会根据 switch 执行到 JUMP_CHILD 分支
                      // 而当前 runc init 和 子 runc init 之间通过上面创建的
                      // sync_child_pipe 进行同步通信
          			child = clone_parent(&env, JUMP_CHILD);
          			if (child < 0)
          				bail("unable to fork: child_func");
          
                      // 通过 sync_child_pipe 循环读取来自子进程的消息
          			while (!ready) {
          				enum sync_t s;
          				int ret;
          
          				syncfd = sync_child_pipe[1];
          				close(sync_child_pipe[0]);
          
          				if (read(syncfd, &s, sizeof(s)) != sizeof(s))
          					bail("failed to sync with child: next state");
          
          				switch (s) {
          				case SYNC_ERR:
          					/* We have to mirror the error code of the child. */
          					if (read(syncfd, &ret, sizeof(ret)) != sizeof(ret))
          						bail("failed to sync with child: read(error code)");
          
          					exit(ret);
          				case SYNC_USERMAP_PLS:
          					// 这里设置 user map,因为子进程修改自身的 user namespace 之后,就没有权限再设置 user map 了
          
          					if (config.is_rootless_euid && !config.is_setgroup)
          						update_setgroups(child, SETGROUPS_DENY);
          
          					/* Set up mappings. */
          					update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len);
          					update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len);
          
                              // 向子进程发送 SYNC_USERMAP_ACK,表示处理完成
          					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;
          				case SYNC_RECVPID_PLS:{
          						first_child = child;
                                  // 接收孙进程(还是 runc init)的 pid
          						/* Get the init_func pid. */
          						if (read(syncfd, &child, sizeof(child)) != sizeof(child)) {
          							kill(first_child, SIGKILL);
          							bail("failed to sync with child: read(childpid)");
          						}
          
          						// 向子进程发送 SYNC_RECVPID_ACK,表示处理完成
          						s = SYNC_RECVPID_ACK;
          						if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
          							kill(first_child, SIGKILL);
          							kill(child, SIGKILL);
          							bail("failed to sync with child: write(SYNC_RECVPID_ACK)");
          						}
          
                                  // 通过容器外传进来的 child pipe 把子和孙进程 PID,写回去,然后让容器外的 runc 接管 PID
                                  // 这个是因为 clone_parent 的时候参数传了 CLONE_PARENT,导致子孙的父进程都是容器外的那个 runc
                                  // 所以当前进程无法接管这些 PID
          						len = dprintf(pipenum, "{\"pid\": %d, \"pid_first\": %d}\n", child, first_child);
          						if (len < 0) {
          							kill(child, SIGKILL);
          							bail("unable to generate JSON for child pid");
          						}
          					}
          					break;
                          case SYNC_CHILD_READY:
                              // 子进程已经处理完了所有事情,退出循环
          					ready = true;
          					break;
          				default:
          					bail("unexpected sync value: %u", s);
          				}
          			}
          
                      // 通过 sync_grandchild_pipe 循环读取来自孙进程的消息
          			ready = false;
          			while (!ready) {
          				enum sync_t s;
          				int ret;
          
          				syncfd = sync_grandchild_pipe[1];
          				close(sync_grandchild_pipe[0]);
          
          				s = SYNC_GRANDCHILD;
          				if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
          					kill(child, SIGKILL);
          					bail("failed to sync with child: write(SYNC_GRANDCHILD)");
          				}
          
          				if (read(syncfd, &s, sizeof(s)) != sizeof(s))
          					bail("failed to sync with child: next state");
          
          				switch (s) {
          				case SYNC_ERR:
          					if (read(syncfd, &ret, sizeof(ret)) != sizeof(ret))
          						bail("failed to sync with child: read(error code)");
          
          					exit(ret);
                          case SYNC_CHILD_READY:
                              // 等待孙进程准备完成
          					ready = true;
          					break;
          				default:
          					bail("unexpected sync value: %u", s);
          				}
                      }
                      // 退出。很明显,当前 runc init 退出的时候,子 runc init 一定也退出了,但是孙 runc init 还没有退出
                      // 这也是为什么容器外的 runc 等待子进程退出,却又向 pipe 里写数据的原因,因为孙 runc init 还在等着容器配置
                      // 进程正常退出(不给 go 代码执行的机会)
          			exit(0);
          		}
          	case JUMP_CHILD:{
          			pid_t child;
          			enum sync_t s;
          
          			/* We're in a child and thus need to tell the parent if we die. */
          			syncfd = sync_child_pipe[0];
          			close(sync_child_pipe[1]);
          
          			/* For debugging. */
          			prctl(PR_SET_NAME, (unsigned long)"runc:[1:CHILD]", 0, 0, 0);
          
          			// 通过 setns 加入现有的 namespace
          			if (config.namespaces)
          				join_namespaces(config.namespaces);
          
                      // 如果 clone flag 里有 CLONE_NEWUSER,说明需要创建新的 user namespace,此处调用 unshare 进行了处理
          			if (config.cloneflags & CLONE_NEWUSER) {
          				if (unshare(CLONE_NEWUSER) < 0)
          					bail("failed to unshare user namespace");
          				config.cloneflags &= ~CLONE_NEWUSER;
          
          				if (config.namespaces) {
          					if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
          						bail("failed to set process as dumpable");
                          }
                          
                          // 等待父 runc init 配置 user map
          				s = SYNC_USERMAP_PLS;
          				if (write(syncfd, &s, sizeof(s)) != sizeof(s))
          					bail("failed to sync with parent: write(SYNC_USERMAP_PLS)");
          
          				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);
          
          				if (config.namespaces) {
          					if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
          						bail("failed to set process as dumpable");
          				}
          
          				// 设置当前进程的 uid 为 0,即容器内的 root
          				if (setresuid(0, 0, 0) < 0)
          					bail("failed to become root in user namespace");
                      }
                      
          			// unshare 其他需要新建的 namespace
          			if (unshare(config.cloneflags & ~CLONE_NEWCGROUP) < 0)
          				bail("failed to unshare namespaces");
          
          			// 创建孙进程,当前进程已经完成了 namespace 的设置,孙进程会继承这些设置
          			child = clone_parent(&env, JUMP_INIT);
          			if (child < 0)
          				bail("unable to fork: init_func");
          
          			// 将孙进程 PID 传给父 runc init
          			s = SYNC_RECVPID_PLS;
          			if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
          				kill(child, SIGKILL);
          				bail("failed to sync with parent: write(SYNC_RECVPID_PLS)");
          			}
          			if (write(syncfd, &child, sizeof(child)) != sizeof(child)) {
          				kill(child, SIGKILL);
          				bail("failed to sync with parent: write(childpid)");
          			}
          
          			if (read(syncfd, &s, sizeof(s)) != sizeof(s)) {
          				kill(child, SIGKILL);
          				bail("failed to sync with parent: read(SYNC_RECVPID_ACK)");
          			}
          			if (s != SYNC_RECVPID_ACK) {
          				kill(child, SIGKILL);
          				bail("failed to sync with parent: SYNC_RECVPID_ACK: got %u", s);
          			}
          
                      // 发送 SYNC_CHILD_READY 给父 runc init
          			s = SYNC_CHILD_READY;
          			if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
          				kill(child, SIGKILL);
          				bail("failed to sync with parent: write(SYNC_CHILD_READY)");
          			}
          
                      // 子 runc init 的工作到此结束,进程正常退出(不给 go 代码执行的机会)
          			exit(0);
          		}
          
          	case JUMP_INIT:{
          			// 孙 runc init 是真正启动容器 entrypoint 的进程,并且在启动之前,进行最后的环境准备工作
          			enum sync_t s;
          
          			/* We're in a child and thus need to tell the parent if we die. */
          			syncfd = sync_grandchild_pipe[0];
          			close(sync_grandchild_pipe[1]);
          			close(sync_child_pipe[0]);
          			close(sync_child_pipe[1]);
          
          			/* For debugging. */
          			prctl(PR_SET_NAME, (unsigned long)"runc:[2:INIT]", 0, 0, 0);
          
          			if (read(syncfd, &s, sizeof(s)) != sizeof(s))
          				bail("failed to sync with parent: read(SYNC_GRANDCHILD)");
          			if (s != SYNC_GRANDCHILD)
          				bail("failed to sync with parent: SYNC_GRANDCHILD: got %u", s);
          
          			if (setsid() < 0)
          				bail("setsid failed");
          
          			if (setuid(0) < 0)
          				bail("setuid failed");
          
          			if (setgid(0) < 0)
          				bail("setgid failed");
          
          			if (!config.is_rootless_euid && config.is_setgroup) {
          				if (setgroups(0, NULL) < 0)
          					bail("setgroups failed");
          			}
          
          			// 等待来自容器外 runc 的 child pipe 的关于 cgroup namespace 的消息 0x80(CREATECGROUPNS)
          			if (config.cloneflags & CLONE_NEWCGROUP) {
          				uint8_t value;
          				if (read(pipenum, &value, sizeof(value)) != sizeof(value))
          					bail("read synchronisation value failed");
          				if (value == CREATECGROUPNS) {
          					if (unshare(CLONE_NEWCGROUP) < 0)
          						bail("failed to unshare cgroup namespace");
          				} else
          					bail("received unknown synchronisation value");
          			}
          
                      // 发送孙进程准备完成的消息给祖父 runc init
          			s = SYNC_CHILD_READY;
          			if (write(syncfd, &s, sizeof(s)) != sizeof(s))
          				bail("failed to sync with patent: write(SYNC_CHILD_READY)");
          
          			/* Close sync pipes. */
          			close(sync_grandchild_pipe[0]);
          
          			/* Free netlink data. */
          			nl_free(&config);
          
                      // 此时,父 / 祖父 runc init 都退出了(可能会有时差)
                      // 但是当前进程是不能直接退出的,所以这里单纯的 return,然后开始执行 go 代码
          			return;
          		}
          	default:
          		bail("unexpected jump value");
          	}
          
          	/* Should never be reached. */
          	bail("should never be reached");
          }
          

          在 namespace 初始化完成后,会通过调用链 LinuxFactory.StartInitialization() -> newContainerInit() 创建容器初始化结构 linuxStandardInit(github.com/opencontainers/runc/libcontainer/init_linux.go#47):

          func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) {
          	var config *initConfig
              // 此处从 child pipe 中读取了 container config
          	if err := json.NewDecoder(pipe).Decode(&config); err != nil {
          		return nil, err
          	}
          	if err := populateProcessEnvironment(config.Env); err != nil {
          		return nil, err
              }
              // t 为 standard,来自于环境变量 _LIBCONTAINER_INITTYPE
          	switch t {
          	case initSetns:
          		return &linuxSetnsInit{
          			pipe:          pipe,
          			consoleSocket: consoleSocket,
          			config:        config,
          		}, nil
          	case initStandard:
          		return &linuxStandardInit{
          			pipe:          pipe,
          			consoleSocket: consoleSocket,
          			parentPid:     unix.Getppid(),
          			config:        config,
          			fifoFd:        fifoFd,
          		}, nil
          	}
          	return nil, fmt.Errorf("unknown init type %q", t)
          }
          

          然后执行 linuxStandardInit.Init()(github.com/opencontainers/runc/libcontainer/standard_init_linux.go#47):

          func (l *linuxStandardInit) Init() error {
              // 这里比较重要的是这个函数,此时各个 Namespace 虽然都挂载完毕了,但是当前的进程的视角里根目录和容器外是一样的
              // 因此这个方法会挂载设备,bind mount,然后将当前根目录切换到容器的根目录下。
          	if err := prepareRootfs(l.pipe, l.config); err != nil {
          		return err
          	}
          
          	// 设置 root (/) 为只读
          	if l.config.Config.Namespaces.Contains(configs.NEWNS) {
          		if err := finalizeRootfs(l.config.Config); err != nil {
          			return err
          		}
          	}
          
          	// 在完成一系列容器内的环境准备之后,通过 execve 执行容器内的 entrypoint
          	if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
          		return newSystemErrorWithCause(err, "exec user process")
          	}
          	return nil
          }
          

          总结:

          • runc init 一个会有三个进程
            • 第一个进程读取 bootstrapData,并完成第二个进程的 user map 的设置
            • 第二个进程完成 namespace 的设置
            • 第三个进程完成 CGROUP namesapce 的设置,并读取了 0x80 的同步信息。最后进入 go 代码。go 代码读取 container config,进行容器内环境准备,最后执行容器的 entrypoint

          CVE-2019-5736 过程分析

          链接:https://seclists.org/oss-sec/2019/q1/119

          通过构造一个恶意的容器,替换掉 runc 执行程序。runc 被再次执行时,恶意代码即可拿到 root 权限。

          过程:

          1. 在 runc init 的最后一个阶段,runc 会加载容器的 entrypoint
          2. 我们伪造一个容器,它具备以下两个要素:
            • entrypoint 链接到 /proc/self/exe
            • 含有恶意代码的 libc.so(或者其他任意 so,只要会被 runc 加载就行)
          3. 当 runc init 最后通过 execve 启动 entrypoint 时,由于 entrypoint 指向了 /proc/self/exe,那么实际上就等于执行了 runc 自身
          4. runc init 被替换,但是容器内的 runc 启动了,由于现在 rootfs 已经是容器的 rootfs 了,所以 so 会从容器内加载,这样就会加载到含有恶意代码的 libc.so
          5. libc.so 的恶意代码在 constructor 里,所以一加载这个 so,这个代码就会执行。恶意代码通过 open 系统调用去只读形式打开 /proc/self/exe(只能以只读形式,因为 runc 在运行),这个时候就会有一个对应的 fd 保留下来
          6. 恶意代码这个时候通过 execve 去执行容器内的一个程序,这样不会导致 PID 发生变化,但是程序改变了,并且 fd 继续保留了下来
          7. 程序的工作就是找到 fd 编号,就在 /proc/self/fd/ 中,然后再以写的方式重新打开这个 fd(这个时候因为 runc 已经退出了,所以可以以写的方式打开)。然后写入包含恶意代码的 runc。
          8. 在下次宿主机上的 runc 再被执行时,这个恶意代码即可执行,并且拥有 runc 的权限,即 root 权限

posted on 2020-11-01 10:30  tycoon3  阅读(355)  评论(0编辑  收藏  举报

导航