• 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]:
    runtime/debug.Stack(0x400018fbd8, 0xaaaab1b68260, 0xaaaab21de220)
    	/usr/go/src/runtime/debug/stack.go:24 +0x88
    
    github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*LinuxFactory).StartInitialization.func2(0x400018fea0) /root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/factory_linux.go:370
    +0x40 panic(0xaaaab1b68260, 0xaaaab21de220) /usr/go/src/runtime/panic.go:513 +0x18c github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*linuxSetnsInit).Init(0x400012d9c0, 0x0, 0x0) /root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/setns_init_linux.go:91 +0x434 github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer.(*LinuxFactory).StartInitialization(0x4000164090, 0x0, 0x0) /root/go/src/github.com/kata-containers/agent/vendor/github.com/opencontainers/runc/libcontainer/factory_linux.go: 380 +0x2ec main.init.0() /root/go/src/github.com/kata-containers/agent/agent.go:1506 +0x88 " 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}
            ", 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 权限
  • 相关阅读:
    Jquery实现无刷新DropDownList联动
    Mvc 提交表单的4种方法全程详解
    Sql Like 通配符 模糊查询技巧及特殊字符
    SQL 语句递归查询 With AS 查找所有子节点
    最常用的五类CSS选择器
    取出分组后每组的第一条记录(不用group by)按时间排序
    SpringCloud中接收application/json格式的post请求参数并转化为实体类
    SpringCloud负载均衡笔记
    iview-admin打包笔记
    SpringCloud之最大的坑
  • 原文地址:https://www.cnblogs.com/dream397/p/13908748.html
Copyright © 2020-2023  润新知