已开启
lxcfs-tools代码安全扫描问题 #13
yangna1310创建于  9月14日
yangna1310
yangna1310成员
9月14日 创建

【缺陷描述】:请补充详细的缺陷问题现象描述

lxcfs-tools代码安全扫描问题

一、缺陷信息

发现 20 个问题(🔴 致命 0 · 🟡 重要 6 · 🟢 建议 11 · ⚠️ 疑似 3)

未发现可直接导致宿主机 root 命令执行或宿主机任意文件读写的 🔴 致命问题(工具无向宿主机路径写入用户可控内容的操作,所有 exec.Command 均以参数数组形式调用、不经 shell,无命令注入)。但 6 个 🟡 重要问题中:① getRuncContainerIDAndPid 的 out[0] 越界 panic 可在常见环境(docker/runc 主机无运行容器)下触发管理工具崩溃;② doMount 以未净化的 Rootfs(源自 OCI spec,容器创建者可控)做字符串拼接挂载路径,存在容器命名空间内挂载目标穿越;③ 日志文件符号链接跟随;④ remount --all/umount --all 超时后 goroutine 与 fork 子进程泄漏并可能造成挂载状态不一致;⑤ 容器 ID 子串匹配导致误操作容器(混淆代理);⑥ execCommond 在读流出错时 os.Exit 孤儿化子进程。建议优先修复 🟡 类问题。


🟡 [重要] remountcmd.go:232 — getRuncContainerIDAndPid 未校验切片长度致越界 panic(DoS)

CWE: CWE-129(数组索引不当)/ CWE-754(对异常条件的不当检查)
规则: R01-内存安全(切片边界)/ R05-错误处理
描述: getRuncContainerIDAndPid() 调用 execCommond("runc", ...) 后直接访问 out[0],未校验 len(out) > 0。execCommond 初始化 res := []string{}(空切片),当 runc list --format json 输出为空(无运行容器、某些 runc 版本对空列表不输出 [])或输出末行无换行符时,res 保持为空,out[0] 触发 runtime error: index out of range [0] panic,工具进程崩溃。该路径在 isula 未安装(exec.LookPath("isula") 失败)时作为回退被 getContainerIDAndPid 调用,是 docker/runc 主机的唯一路径。
影响: 管理员执行 lxcfs-tools remount --all / umount --all(在无运行容器或 runc 输出异常时)触发 panic,工具崩溃,重挂/卸载操作中断(DoS)。
CVSS: 4.0(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)
数据来源: runc list --format json stdout(受主机容器运行状态与 runc 版本影响)→ execCommond 返回 []string。
风险传播路径: remount --all → remountAll → getContainerIDAndPid(isula 缺失分支)→ getRuncContainerIDAndPid → execCommond 返回空切片 → out[0] panic。

调用栈

remountcmd.go:53  recmountContainer.Action (--all)
  -> remountcmd.go:72  remountAll(initMountns, initUserns)
     -> remountcmd.go:185  getContainerIDAndPid()
        -> remountcmd.go:254  getRuncContainerIDAndPid()   // isula 未安装时
           -> remountcmd.go:228  execCommond("runc", [...])
              -> remountcmd.go:338  exec.Command("runc", params...).StdoutPipe / ReadString
                 -> remountcmd.go:232  if out[0] == ""   // panic: out 为空切片

另一入口:umountcmd.go:88 umountAll → getContainerIDAndPid → 同链。

问题代码

// 文件: remountcmd.go:227-234
func getRuncContainerIDAndPid() ([]string, error) {
	out, err := execCommond("runc", []string{"--root", runcStateDir, "list", "--format", "json"})
	if err != nil {
		return nil ,err
	}
	if out[0] == "" {   // 未校验 len(out) == 0 → panic
		return out, nil
	}
	...
}

调用链分析

层次 函数 文件:行号 角色
1 recmountContainer.Action remountcmd.go:53 CLI 入口,--all 分支
2 remountAll remountcmd.go:183 遍历容器前先获取 ID+PID 列表
3 getContainerIDAndPid remountcmd.go:247 isula 缺失时回退到 runc
4 getRuncContainerIDAndPid remountcmd.go:227 解析 runc list 输出
5 execCommond remountcmd.go:338 读 stdout 行,空输出返回空切片
6 out[0] remountcmd.go:232 越界 panic 点

修复建议

func getRuncContainerIDAndPid() ([]string, error) {
	out, err := execCommond("runc", []string{"--root", runcStateDir, "list", "--format", "json"})
	if err != nil {
		return nil, err
	}
	if len(out) == 0 || out[0] == "" {
		return out, nil   // 先校验长度,再访问下标
	}
	// ...
}

附带修复 execCommond 末行丢失问题(见调用链同源 🟡-6):ReadString 返回 io.EOF 且 line != "" 时应先 append 再 break。


🟡 [重要] libmount/container_work.go:103,106 — Rootfs 字符串拼接挂载路径,未净化致挂载目标穿越

CWE: CWE-22(路径穿越)/ CWE-73(外部控制文件路径)
规则: R02-注入(路径遍历)/ R01-文件边界
描述: doMount() 以 mount.Rootfs + lxcfsPath(字符串拼接,非 filepath.Join)构造 bind mount 的源与目标,随后 syscall.MS_BIND 再 MS_REMOUNT|MS_RDONLY。Rootfs 经 JSON 管道来自父进程:hook 路径下源自 stdin 的 OCI State.Root(容器创建者/镜像配置可控),CLI remount 路径下为 ""。lxcfsPath = "/var/lib/lxc/lxcfs/cgroup"。当 Rootfs 含 .. 段(如 /var/lib/isulad/engines/<id>/rootfs/../../../tmp),拼接结果经内核路径解析后逃逸预期 rootfs 边界,bind/remount 目标被重定向。同文件 doMount 循环 syscall.Mount(mount.SrcPaths[i], mount.DestPaths[i], ...) 中 DestPaths[i] 在 hook 路径下由 execmount.go:49 fmt.Sprintf("%s/proc/%s", rootfs, ...) 构造,同样以未净化的 rootfs 拼接。grep 全项目确认业务代码 0 处 filepath.Join,路径构造系统性依赖字符串拼接。
影响: 容器侧可控 Root/rootfs → 容器 mount 命名空间内挂载目标被重定向至预期 rootfs 之外(如宿主机挂载进容器的卷路径、容器内任意路径),可能覆盖容器内关键文件(如 /etc/crontab、二进制)为 lxcfs 只读 bind,造成容器内文件篡改/拒绝服务。因操作发生在容器自身 mount 命名空间内(nsenter 仅进入 mnt ns),不直接构成宿主机逃逸,故定为重要。
CVSS: 5.0(Medium,AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L)
数据来源: hook 路径 state.Root(OCI spec stdin,utils.ParseHookState 解析)→ hooks/lxcfs-hook/main.go:71 rootfs = state.Root → prestartMountHook → libmount.NsExecMount(pid, rootfs, ...) → nsexec.exec JSON 编码 Mount.Rootfs → 子进程 doMount。
风险传播路径: 容器 OCI spec root → hook stdin → ParseHookState → rootfs → NsExecMount → JSON 管道 → WorkInContainer/doMount → Rootfs + lxcfsPath(无净化)→ syscall.Mount 目标穿越。

调用栈

hooks/lxcfs-hook/main.go:59  utils.ParseHookState(os.Stdin)   // state.Root 可控
  -> hooks/lxcfs-hook/main.go:71  rootfs = state.Root
     -> hooks/lxcfs-hook/main.go:73  prestartMountHook(nsPID, rootfs)
        -> hooks/lxcfs-hook/execmount.go:49  fmt.Sprintf("%s/proc/%s", rootfs, value.Name())  // DestPaths 拼接
           -> hooks/lxcfs-hook/execmount.go:53  libmount.NsExecMount(strconv.Itoa(pid), rootfs, valueMountPaths, valuePaths)
              -> libmount/libmount.go:30  driver.Mount(pid, mount)   // mount.Rootfs = rootfs
                 -> libmount/nsexec/nsexec.go:147  (*nsexecDriver).Mount  -> ns.exec(nsPaths, MountMsg, mount)
                    -> libmount/nsexec/nsexec.go:120  WriteJSON(parent, data)   // Rootfs 经 JSON 传子进程
                       -> libmount/container_work.go:96  doMount(pipe)  // JSON 解码 mount.Rootfs
                          -> libmount/container_work.go:103  syscall.Mount(mount.Rootfs+lxcfsPath, mount.Rootfs+lxcfsPath, ...)  // 穿越点
                          -> libmount/container_work.go:110  syscall.Mount(mount.SrcPaths[i], mount.DestPaths[i], ...)  // DestPaths 含 rootfs

问题代码

// 文件: libmount/container_work.go:102-113
// remount lxcfs cgroup path readonly
if err := syscall.Mount(mount.Rootfs+lxcfsPath, mount.Rootfs+lxcfsPath, "none", syscall.MS_BIND, ""); err != nil {
    return err
}
if err := syscall.Mount(mount.Rootfs+lxcfsPath, mount.Rootfs+lxcfsPath, "none", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY, ""); err != nil {
    return err
}
for i := 0; i < len(mount.SrcPaths) && i < len(mount.DestPaths); i++ {
    if err := syscall.Mount(mount.SrcPaths[i], mount.DestPaths[i], "none", syscall.MS_BIND, ""); err != nil {  // DestPaths 源自 rootfs 拼接
        return err
    }
}
// 文件: hooks/lxcfs-hook/execmount.go:48-50
for _, value := range lxcfssubpath {
    valuePaths = append(valuePaths, fmt.Sprintf("%s/proc/%s", rootfs, value.Name()))   // 字符串拼接,非 filepath.Join
    valueMountPaths = append(valueMountPaths, fmt.Sprintf("/var/lib/lxc/lxcfs/proc/%s", value.Name()))
}

调用链分析

层次 函数 文件:行号 角色
1 ParseHookState hooks/.../utils/utils.go:104 从 stdin 解析 OCI State,Root 可控
2 prestartMountHook hooks/.../execmount.go:27 以 rootfs 拼接 DestPaths
3 NsExecMount libmount/libmount.go:21 包装 driver.Mount,mount.Rootfs = rootfs
4 nsexecDriver.Mount → exec libmount/nsexec/nsexec.go:147,90 JSON 编码 Mount 经 socketpair 传子进程
5 doMount libmount/container_work.go:96 子进程(已 setns 容器 mnt ns)解码并执行挂载
6 Rootfs+lxcfsPath libmount/container_work.go:103,106 字符串拼接,.. 未净化 → 挂载目标穿越

修复建议

// 修复:1) 对 Rootfs 做净化(Clean + 拒绝 .. 且必须为绝对路径);2) 全部改用 filepath.Join。
import "path/filepath"

func doMount(pipe *os.File) error {
	var mount nsexec.Mount
	if err := json.NewDecoder(pipe).Decode(&mount); err != nil {
		return err
	}
	// 净化 Rootfs:必须绝对、不含 ..
	rootfs := filepath.Clean(mount.Rootfs)
	if rootfs != "/" && !filepath.IsAbs(rootfs) {
		return fmt.Errorf("invalid rootfs: %q", mount.Rootfs)
	}
	if strings.Contains(rootfs, "..") {
		return fmt.Errorf("rootfs %q contains parent reference", mount.Rootfs)
	}
	cgrpPath := filepath.Join(rootfs, lxcfsPath)   // 替代 rootfs+lxcfsPath
	if err := syscall.Mount(cgrpPath, cgrpPath, "none", syscall.MS_BIND, ""); err != nil {
		return err
	}
	if err := syscall.Mount(cgrpPath, cgrpPath, "none", syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY, ""); err != nil {
		return err
	}
	for i := 0; i < len(mount.SrcPaths) && i < len(mount.DestPaths); i++ {
		// 同样对 DestPaths 做 Clean + 边界校验
		dst := filepath.Clean(mount.DestPaths[i])
		if err := syscall.Mount(mount.SrcPaths[i], dst, "none", syscall.MS_BIND, ""); err != nil {
			return err
		}
	}
	return nil
}

同时在 execmount.go / remountcmd.go 所有路径构造处统一改用 filepath.Join,并在 hook 入口对 state.Root 做白名单校验。


🟡 [重要] hooks/lxcfs-hook/main.go:38 — 日志文件符号链接跟随(root 追加写)

CWE: CWE-59(链接跟随不当)
规则: R06-隐私与数据 / R01-文件边界
描述: setupLog() 以 os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0600) 打开 --log 指定路径,OpenFile 默认跟随符号链接。logfile 来自 hookspec.json 的 args(--log /var/log/lxcfs-hook.log,由 RPM %post 脚本或管理员配置)。若该路径为指向敏感文件(如 /etc/crontab、/etc/sudoers)的符号链接,或所在目录可被低权限用户写入而遭预置符号链接,hook 以 root 身份将日志内容以 O_APPEND 追加到该敏感文件。
影响: 管理员误配置 --log 指向符号链接,或共享目录符号链接攻击 → root 向敏感文件追加日志内容(日志内容非完全可控,但仍可污染文件结构,如 cron 文件追加非 cron 格式行致 cron 告警)。
CVSS: 4.5(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: hookspec.json args 数组中的 --log 值 → flag.String("log", ...) → setupLog。
风险传播路径: hookspec.json args → lxcfs-hook --log <path> → flag.Parse → setupLog → os.OpenFile(O_APPEND, 跟随 symlink) → root 追加写目标文件。

调用栈

hooks/lxcfs-hook/main.go:56  flag.String("log", "", ...)
  -> hooks/lxcfs-hook/main.go:58  setupLog(*flLogfile)
     -> hooks/lxcfs-hook/main.go:38  os.OpenFile(logfile, O_CREATE|O_WRONLY|O_APPEND|O_SYNC, 0600)  // 跟随 symlink
        -> hooks/lxcfs-hook/main.go:42  logrus.SetOutput(f)  // 后续日志追加写入目标

问题代码

// 文件: hooks/lxcfs-hook/main.go:34-44
func setupLog(logfile string) {
	logrus.SetLevel(logrus.DebugLevel)
	logrus.SetOutput(os.Stdout)
	if logfile != "" {
		f, err := os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0600)
		if err != nil {
			return
		}
		logrus.SetOutput(f)
		return
	}
	// ...
}

修复建议

// 修复:使用 O_NOFOLLOW 拒绝符号链接,并校验父目录属主为 root。
fd, err := syscall.Open(logfile, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_APPEND|syscall.O_SYNC|syscall.O_NOFOLLOW, 0600)
if err != nil {
	return fmt.Errorf("open log file %q failed: %v", logfile, err)
}
logfile_ := os.NewFile(uintptr(fd), logfile)
// 进一步:os.Lstat 校验非 symlink、属主 root、父目录不可被其他用户写入
if fi, err := os.Lstat(logfile); err == nil && fi.Mode()&os.ModeSymlink != 0 {
	return fmt.Errorf("log file %q is a symlink", logfile)
}
logrus.SetOutput(logfile_)

🟡 [重要] remountcmd.go:197-211 / umountcmd.go:100-114 — 超时后 goroutine 与 fork 子进程泄漏 + 挂载状态不一致

CWE: CWE-404(资源泄漏)/ CWE-662(同步不当)/ CWE-833(死锁/竞态)
规则: R04-并发安全 / R05-错误处理
描述: remountAll()(umountAll() 同构)对每个容器启动一个外层 goroutine,其内部再起一个内层 goroutine 调用 remountToContainer,外层用 select { case <-res: case <-time.After(30s): } 等待。当 30s 超时命中 time.After 分支时,外层 goroutine 调用 wg.Done() 结束,但内层 goroutine 仍持续运行——remountToContainer 内部经 nsexec.exec fork 出 /proc/self/exe 子进程并 setns 进入容器 mnt ns 执行挂载,该 fork 子进程与内层 goroutine 均不会被取消/回收。后果:① goroutine 与子进程泄漏,多次 --all 操作累积;② 超时后挂载操作可能延迟成功,工具已报告 timeout 失败但容器内挂载状态实际被改变,造成观测状态与实际状态不一致;③ 内层 goroutine 向已满的 buffered channel res(cap 1)写入虽不阻塞,但仍属无序收尾。
影响: 长期运行或频繁 remount --all 的节点上 goroutine/进程累积(资源耗尽);挂载状态不一致导致运维误判;理论上的并发挂载竞争。
CVSS: 4.0(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: getContainerIDAndPid 返回的容器列表(来自 isula ps/runc list,受主机容器状态影响)。
风险传播路径: remount --all → remountAll 循环 → 每容器外层 goroutine → 内层 goroutine remountToContainer → nsexec.exec fork 子进程 → 30s 超时 → 外层退出、内层与子进程泄漏 → 状态不一致。

调用栈

remountcmd.go:53  recmountContainer.Action (--all)
  -> remountcmd.go:72  remountAll(initMountns, initUserns)
     -> remountcmd.go:191  for _, value := range out
        -> remountcmd.go:197  go func() { defer wg.Done()           // 外层 goroutine
           -> remountcmd.go:200  go func() {                          // 内层 goroutine(不被取消)
              -> remountcmd.go:201  remountToContainer(...)
                 -> remountcmd.go:304  libmount.NsExecMount(pid, "", ...)
                    -> libmount/nsexec/nsexec.go:105  cmd.Start()   // fork /proc/self/exe
                       -> (setns 容器 mnt ns, doMount)               // 子进程持续运行
              -> remountcmd.go:204  res <- struct{}{}
           }()
           -> remountcmd.go:206  select { case <-res: case <-time.After(30s): }  // 超时后外层退出
        }()

问题代码

// 文件: remountcmd.go:196-211
wg.Add(1)
go func() {
    defer wg.Done()
    res := make(chan struct{}, 1)
    go func() {                                       // 内层 goroutine,无取消机制
        if err := remountToContainer(initMountns, initUserns, containerslice[0], containerslice[1], true); err != nil {
            lxcfs_log.Errorf("remount lxcfs dir to container(%s) failed: %v", containerslice[0], err)
        }
        res <- struct{}{}
    }()
    select {
    case <-res:
    case <-time.After(30 * time.Second):              // 超时:外层退出,内层 + fork 子进程泄漏
        lxcfs_log.Errorf("remount lxcfs dir to container(%s) timeout", containerslice[0])
    }
}()

修复建议

// 修复:使用 context.WithTimeout + cmd.Process.Kill 取消内层与子进程;或合并为单层 goroutine + context。
wg.Add(1)
go func(cid, cpid string) {
    defer wg.Done()
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    // 将 ctx 传入 remountToContainer → NsExecMount → nsexec.exec,
    // 在 ctx.Done() 时 cmd.Process.Kill() 终止 fork 子进程。
    if err := remountToContainerCtx(ctx, initMountns, initUserns, cid, cpid, true); err != nil {
        lxcfs_log.Errorf("remount container(%s) failed: %v", cid, err)
    }
}(containerslice[0], containerslice[1])
// nsexec.exec 内:启动 cmd 后 select { case <-ctx.Done(): cmd.Process.Kill(); ... case <-cmdDone: }

🟡 [重要] remountcmd.go:330 — isContainerExsit 子串匹配容器 ID(混淆代理/误操作容器)

CWE: CWE-863(授权不当)/ CWE-1023(不安全的比较)
规则: R02-注入(逻辑绕过)/ R07-代码质量
描述: isContainerExsit() 以 strings.Contains(containerslice[0], containerid) 判定目标容器——子串匹配而非精确/前缀匹配。containerid 来自 CLI --container-id(管理员输入),containerslice[0] 为 isula ps/runc list 输出的容器 ID。当多个容器 ID 共享子串时(如 abc123 与 abc999 共享 abc,或短 ID 是长 ID 前缀),--container-id abc 会命中列表中第一个含 abc 的容器,操作错误的目标容器。函数在首次匹配后即 return,无歧义检测。
影响: 管理员意图重挂容器 A,实际操作了容器 B(共享 ID 子串时),造成非预期容器的挂载/卸载状态变更(混淆代理)。在多租户或共享主机上可能跨租户误操作。
CVSS: 4.0(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: CLI --container-id(remountcmd.go:79 / umountcmd.go:73)与 isula ps/runc list 输出。
风险传播路径: lxcfs-tools remount -c <id> → remountToContainer → isContainerExsit(<id>) → strings.Contains(实际ID, <id>) 首次子串命中 → 返回错误容器的 PID → 操作错误容器。

调用栈

remountcmd.go:79  containerid := context.String("container-id")
  -> remountcmd.go:85  remountToContainer(initMountns, initUserns, containerid, "", false)
     -> remountcmd.go:273  pid, err = isContainerExsit(containerid)
        -> remountcmd.go:330  if strings.Contains(containerslice[0], containerid)   // 子串匹配
           -> remountcmd.go:331  return containerslice[1], nil   // 返回首个命中容器的 PID
              -> (remountToContainer 以该 PID 进入错误容器 mnt ns 执行挂载)

问题代码

// 文件: remountcmd.go:325-333
for _, value := range out {
    containerslice := strings.Fields(value)
    if len(containerslice) < 2 {
        continue
    }
    if strings.Contains(containerslice[0], containerid) {   // 子串匹配,可能误命中
        return containerslice[1], nil
    }
}

修复建议

// 修复:使用精确匹配;若支持短 ID 前缀匹配,需校验唯一性。
var matched []string
for _, value := range out {
    cs := strings.Fields(value)
    if len(cs) < 2 { continue }
    if cs[0] == containerid {              // 精确匹配优先
        return cs[1], nil
    }
    if strings.HasPrefix(cs[0], containerid) {
        matched = append(matched, cs[1])
    }
}
if len(matched) == 1 {                      // 唯一前缀匹配
    return matched[0], nil
}
if len(matched) > 1 {
    return "", fmt.Errorf("container id %q is ambiguous, matches: %v", containerid, matched)
}
return "", fmt.Errorf("no such container %s", containerid)

🟡 [重要] remountcmd.go:360 — execCommond 读流出错调 os.Exit 孤儿化子进程 + 末行丢失

CWE: CWE-404(资源泄漏)/ CWE-252(未检查返回值)/ CWE-459(清理不完整)
规则: R05-错误处理 / R01-文件边界
描述: execCommond() 在 bufio.Reader.ReadString('\n') 循环中,遇到非 EOF 错误时调用 onfail(err2),而 onfail(main.go:35)执行 os.Exit(1)。此时 cmd.Start() 已启动的子进程(isula/runc/mount)仍在运行,父进程直接退出 → 子进程孤儿化(由 init 回收为僵尸后再回收,但 stdout 管道已关闭,子进程写 stdout 可能 SIGPIPE 退出,行为不可控);管道 fd 未关闭。此外,ReadString('\n') 在 EOF 时 break 而不 append 末尾部分行:当外部命令输出末行无换行符时(如 isula ps 最后一行无 \n,或 runc list 输出 [] 无 \n),该行被静默丢弃,导致 remount --all 漏处理最后一个容器、或 getRuncContainerIDAndPid 得到空切片(与 🟡-1 叠加)。
影响: ① 读流出错时孤儿子进程 + fd 泄漏,极端情况子进程挂起占用资源;② 末行丢失致 --all 操作静默漏处理容器(数据丢失/操作不完整)。
CVSS: 3.5(Low,AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L)
数据来源: isula ps / runc list / mount 的 stdout 流。
风险传播路径: execCommond → cmd.Start → ReadString 出错 → onfail → os.Exit(1) → 子进程孤儿;或 ReadString EOF 末行无 \n → 末行丢弃。

调用栈

remountcmd.go:338  execCommond(command, params)
  -> remountcmd.go:349  cmd.Start()                    // 子进程已启动
     -> remountcmd.go:356  line, err2 := reader.ReadString('\n')
        -> remountcmd.go:357  if err2 == io.EOF { break }   // 末行无 \n 被丢弃
        -> remountcmd.go:359  else if err2 != nil { onfail(err2) }   // os.Exit(1) → 子进程孤儿

问题代码

// 文件: remountcmd.go:353-363
reader := bufio.NewReader(stdout)
for {
    line, err2 := reader.ReadString('\n')
    if err2 == io.EOF {
        break                    // 末尾部分行未 append → 丢失
    } else if err2 != nil {
        onfail(err2)             // os.Exit(1),子进程仍在运行 → 孤儿
    }
    res = append(res, line)
}
if err := cmd.Wait(); err != nil {   // onfail 后不会执行到此处
    return res, err
}

修复建议

reader := bufio.NewReader(stdout)
for {
    line, err2 := reader.ReadString('\n')
    // EOF 时若 line 非空,仍需收集(末行无换行)
    if err2 == io.EOF {
        if line != "" {
            res = append(res, line)
        }
        break
    }
    if err2 != nil {
        // 不再调 os.Exit;关闭管道、Kill 子进程、返回错误
        _ = cmd.Process.Kill()
        _ = stdout.Close()
        return res, err2
    }
    res = append(res, line)
}
if err := cmd.Wait(); err != nil {
    return res, err
}

并在 main.go 的 onfail 中避免 os.Exit,改为返回 error 由顶层统一处理。


🟢 [建议] hooks/lxcfs-hook/utils/utils.go:48-64 — ParseSyslogService 变量遮蔽致解析地址丢失

规则: R07-代码质量 / R05-错误处理
描述: ParseSyslogService 在三个 if/else if 分支内使用 serviceAddr :=(短变量声明)创建新的局部变量,遮蔽外层 serviceAddr(第 46 行声明为 "")。分支内对局部变量赋值后,外层 serviceAddr 仍为 "",最终 serv.Addr = serviceAddr 始终为空字符串。setupLog("unix:///dev/log", ...) 期望解析出 /dev/log,实际 serv.Addr = "",/dev/log 被丢弃。
调用栈: hooks/lxcfs-hook/main.go:46 utils.SetupSyslog("unix:///dev/log", ...) → utils.go:78 ParseSyslogService → 分支内 serviceAddr :=(遮蔽)→ utils.go:71 serv.Addr = "" → logrus_syslog.NewSyslogHook("", "", ...)。
修复建议: 将分支内 serviceAddr := 改为 serviceAddr =(赋值,非短声明)。


🟢 [建议] hooks/lxcfs-hook/utils/utils.go:33-34 — 默认 syslog 端口错误(541 应为 514)

规则: R08-依赖与配置 / R07-代码质量
描述: syslogDefaultUDPService = "localhost:541"、syslogDefaultTCPService = "localhost:541",标准 syslog 端口为 514,541 非标准。回退默认时 syslog 转发失效。(与 🟢-1 变量遮蔽叠加,该默认值当前实际不被使用,但仍为潜在配置错误。)
调用栈: utils.go:52,58 在 serviceAddr == "" 时回退到 syslogDefaultUDPService。
修复建议: 改为 localhost:514。


🟢 [建议] Makefile:57 — install 目标拼写错误"hakc"+ 残留"loacal:"目标 + 版本元数据不一致

规则: R07-代码质量 / R08-依赖与配置
描述: ① Makefile install: 目标 cd hakc && ./install.sh,"hakc" 应为 "hack",make install 必然失败(目录不存在);② 第 57 行 loacal: 为拼写错误的游离目标(无依赖、无配方,且与 install: 粘连在同一行 install:\n\tcd hakc && ./install.shloacal:,实际 install.shloacal: 也是错误拼接);③ GO_LDFLAGS 含 -X main.gitCommit=${COMMIT},但 main.go 仅声明 var version,未声明 gitCommit,链接器对不存在符号的 -X 为空操作(告警);④ main.go:33 var version = "0.1" 与 VERSION 文件 v0.3、spec Version: 0.3 不一致,CLI --version 显示 0.1(除非经 ldflags 覆盖)。
调用栈: Makefile:56-57 install: / Makefile:23 GO_LDFLAGS / main.go:33。
修复建议: 修正 cd hack && ./install.sh;删除游离 loacal:;main.go 增加 var gitCommit string 或移除 ldflags 中 -X main.gitCommit;统一 version 与 VERSION 文件。


🟢 [建议] libmount/nsexec/nsexec.go:147,154,161-169 — pid 未校验为正整数即拼入 nsenter 路径

规则: R02-注入(路径)/ R01-输入校验
描述: Mount(pid, ...)/Umount(pid, ...) 接收字符串 pid,buildNSString 以 fmt.Sprintf("%s:/proc/%s/ns/%s", ns, pid, ns) 拼入传给 nsenter C 解析器的 NsPathsAttr。pid 在 CLI 路径来自 isula ps/runc list 输出字段(字符串),在 hook 路径来自 strconv.Itoa(state.Pid)(int)。未校验 pid 为纯数字正整数,若 isula ps 输出异常字段(含 ,/:/..)可构造畸形 nsPaths,理论上可注入额外 type:path 命名空间条目(见 ⚠️-3)。当前因 isulad 为可信源而受限。
调用栈: remountcmd.go:304 libmount.NsExecMount(pid, ...) → nsexec.go:147 Mount → nsexec.go:149 buildNSString(pid, ...) → nsexec.go:167 fmt.Sprintf("mnt:/proc/%s/ns/mnt", pid)。
修复建议: 在 Mount/Umount 入口校验 n, err := strconv.Atoi(pid); err != nil || n <= 0 { return error },并以 strconv.Itoa(n) 重建规范化 pid。


🟢 [建议] libmount/container_work.go:124-125,131 — 基于错误字符串匹配判断(脆弱)

规则: R07-代码质量 / R05-错误处理
描述: doUmount 以 strings.Contains(err.Error(), "invalid argument") 与 "no such file or directory" 判定 umount 失败是否可忽略。该方式依赖 Go syscall 错误消息文本,若 Go 运行时未来变更错误字符串或返回包装错误,匹配失效,导致非可忽略错误被静默吞没或可忽略错误被错误返回。
调用栈: container_work.go:123 syscall.Unmount → container_work.go:124-125 strings.Contains(err.Error(), ...)。
修复建议: 使用 errno, ok := err.(syscall.Errno); if ok && (errno == syscall.EINVAL || errno == syscall.ENOENT) { continue },或 errors.Is(err, syscall.EINVAL)。


🟢 [建议] hooks/lxcfs-hook/main.go:65-67 — state.Pid<=0 仅记日志未 return,以非法 pid 继续执行

规则: R05-错误处理 / R07-代码质量
描述: if state.Pid <= 0 { logrus.Errorf(...) } 后未 return,继续 nsPID = state.Pid(≤0)、prestartMountHook(nsPID, rootfs)。下游 strconv.Itoa(pid) → /proc/0/ns/mnt 或 /proc/-5/ns/mnt → os.Readlink 失败返回错误(被上层捕获),但控制流不应放行非法 pid。
调用栈: main.go:65 if state.Pid <= 0 → main.go:70 nsPID = state.Pid → main.go:73 prestartMountHook(nsPID, ...)。
修复建议: if state.Pid <= 0 { logrus.Errorf(...); return }。


🟢 [建议] libmount/nsexec/nsexec.go:91,116,129 — parent 端 socketpair fd 未关闭(fd 泄漏)

规则: R04-并发安全(资源管理)/ R05-错误处理
描述: exec() 创建 socketpair 得 parent, child,child 经 cmd.ExtraFiles 传子进程(子进程退出后关闭),但 parent 在 cmd.Wait() 与读取响应后从未 Close()。每次 NsExecMount/NsExecUmount 调用泄漏一个 fd;remount --all 对 N 个容器调用 N 次,泄漏 N 个 fd,长期运行或大列表可耗尽进程 fd 表。
调用栈: nsexec.go:91 newPipe() → nsexec.go:116 io.Copy(parent, ...) → nsexec.go:120 WriteJSON(parent, ...) → nsexec.go:125 cmd.Wait() → nsexec.go:129-143 decoder.Decode(parent) → 函数返回(无 parent.Close())。
修复建议: defer parent.Close() 置于 newPipe 成功之后。


🟢 [建议] remountcmd.go:48-49 / umountcmd.go:42-43 — container-id 默认值" "(单空格)哨兵脆弱 + 仅剥离换行

规则: R07-代码质量 / R06-隐私(日志注入)
描述: ① cli.StringFlag{Value: " "} 以单空格作默认值,配合 len(containerid) == 1 判定"未提供",哨兵脆弱(用户若合法传入单字符 ID 会被误拒,且依赖长度魔法数);应默认 "" 并 == "" 判定。② containerid = strings.Replace(containerid, "\n", "", -1) 仅剥离 \n,未处理 \r、\t 及其他控制字符,作为日志注入缓解不完整(containerid 经 logrus.Infof 记录)。
调用栈: remountcmd.go:79-82 / umountcmd.go:73-76。
修复建议: 默认值改 ""、判定改 == "";用 strings.Map 剥离所有 unicode.IsControl 字符或仅保留 [A-Za-z0-9_.-]。


🟢 [建议] hooks/lxcfs-hook/utils/utils.go:106 — ioutil.ReadAll 无界读取 stdin(潜在 OOM)

规则: R07-代码质量 / R01-资源管理
描述: ParseHookState 用 ioutil.ReadAll(reader) 读取全部 stdin 后 json.Unmarshal。若 stdin 提供超大输入(异常容器运行时或恶意构造),内存无界增长。正常容器运行时仅发送小 JSON,风险低,但缺乏防御。
调用栈: utils.go:106 ioutil.ReadAll(reader) → utils.go:111 json.Unmarshal(stateBuf, &state)。
修复建议: stateBuf, err := ioutil.ReadAll(io.LimitReader(reader, 1<<20))(限制 1MiB),超限则报错。


🟢 [建议] 全项目 — 拼写错误与命名不一致(可维护性)

规则: R07-代码质量
描述: 多处拼写错误影响可读性与可维护性:isContainerExsit(→ Exist,remountcmd.go:315)、execCommond(→ Command,remountcmd.go:338)、recmountContainer(→ remount,remountcmd.go:36)、Unspported(→ Unsupported,utils.go:66)、perstart(→ prestart,remountcmd.go:115 Description)、Prase(→ Parse,execmount.go:30)、runing(→ running,remountcmd.go:184/umountcmd.go:87)。
修复建议: 统一修正拼写;导出符号改名需评估外部兼容性(本项目为独立工具,可安全改)。


🟢 [建议] remountcmd.go:218-219 — runcStateDir 硬编码,docker/isulad 异构环境回退失败

规则: R08-依赖与配置 / R07-代码质量
描述: runcStateDir = "/run/docker/runtime-runc/moby" 硬编码。docker 默认 state 目录随版本/发行版不同(如 /run/docker/runtime-runc/),isulad-only 主机无此目录。isula 未安装时回退到 runc 读该目录,路径不匹配则 runc list 失败,remount --all 无法工作。缺乏可配置性与错误提示。
调用栈: remountcmd.go:228 execCommond("runc", []string{"--root", runcStateDir, ...})。
修复建议: 通过环境变量/配置文件/CLI flag 可配置 runcStateDir;或自动探测 docker info 获取实际根目录。


⚠️ 疑似[重要] libmount/container_work.go:103-108 — Rootfs 可控下自绑定只读重挂的目标重定向

置信度: 50%
描述: doMount 先以 Rootfs+lxcfsPath 对自身做 MS_BIND,再 MS_BIND|MS_REMOUNT|MS_RDONLY 改只读。若 Rootfs(容器创建者可控的 OCI root)含 ..,bind 与只读重挂的目标均被重定向至预期 rootfs 之外(容器 mnt ns 内)。因源==目标(自绑定),不产生跨位置数据移动,只读重挂仅改变目标挂载点属性。但若目标指向容器内已挂载的宿主机卷(如 --volume /host:/data,构造 Rootfs 使 Rootfs+lxcfsPath 解析到 /data/var/lib/lxc/lxcfs/cgroup),可能将容器内该路径误设为只读,影响容器对卷的写入。
建议: 与 🟡-2 同源,按 🟡-2 修复方案净化 Rootfs 即可缓解;另需评估自绑定只读重挂对容器内卷路径的副作用。


⚠️ 疑似[建议] hack/lxcfs-tools.spec:53-70 — %post 脚本以 isula info 输出构造 hookspec 写入路径

置信度: 40%
描述: RPM %post 脚本以 GRAPH=\isula info | grep -Eo "iSulad Root Dir:.+" | grep -Eo "/.*"`解析 isulad 根目录,并cat << EOF > ${HOOK_SPEC}/hookspec.json 写入 hook 配置。GRAPH来自 isulad 运行时输出,若 isulad 配置被篡改以报告恶意根目录(如/etc),则 hookspec.json被写到/etc/hookspec.json(覆盖或创建)。需先 compromise isulad 配置(已需 root),攻击面有限。 **建议**: 校验 GRAPH必须为绝对路径且不含..`、属主 root;写 hookspec 前校验目标目录在预期 isulad 根下。


⚠️ 疑似[建议] libmount/nsexec/nsexec.go:161-169 — nsPaths 字符串注入额外命名空间条目

置信度: 30%
描述: buildNSString(pid, ["mnt"]) 产出 mnt:/proc/<pid>/ns/mnt,作为 NsPathsAttr 经 netlink 传 nsenter C 解析器(runc vendor),解析器按 type:path 以 , 分隔多组。若 pid 含 , 或 :(如 1,ipc:/proc/1/ns/ipc),nsPaths 变为 mnt:/proc/1,ipc:/proc/1/ns/ipc/ns/mnt,解析器行为依赖其实现(vendor runc nsenter.c,按规则排除未审)。pid 在 CLI 路径来自 isulad 输出(可信),hook 路径为 int 转字符串(安全),故实际可利用性低。
建议: 在 buildNSString 前校验 pid 仅含数字(见 🟢-4);审查 vendor nsenter.c 对畸形 nsPaths 的处理(虽 vendor 排除审计,但此处为业务层数据流入点)。

【缺陷所属的os版本】

24.03-LTS-next

【内核版本】

kernel6.6

【缺陷所属软件及版本号】

lxcfs-tools

【环境信息】

【问题复现步骤】

【实际结果】

【期望结果】

【其他相关附件信息】

【缺陷详情及分析指导参考链接】

二、缺陷分析结构反馈
影响性分析说明:

缺陷严重等级:(Critical/High/Moderate/Low)

缺陷根因说明:

受影响版本排查(受影响/不受影响):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4

修复是否涉及abi变化(是/否):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4

likedislike
openeuler-ci-botopeneuler-ci-bot成员
9月14日 将 chantsztung 设为负责人
openeuler-ci-botopeneuler-ci-bot成员
9月14日 修改了issue 的描述
openeuler-ci-botopeneuler-ci-bot成员
9月14日 将 jingxiaolu、Yekelu、haomintsai、yangna1310 设为负责人
openeuler-ci-bot
openeuler-ci-bot成员
9月14日 评论:

以下的要求不是强制性的, 未按模板评论时对issue无任何影响
issue处理注意事项:
1. 当前issue受影响的分支提交pr时, 须在pr描述中填写当前issue编号进行关联, 否则无法关闭当前issue;
2. 模板内容需要填写完整, 无论是受影响或者不受影响都需要填写完整内容,未引入的分支不需要填写, 否则无法关闭当前issue;
3. 以下为模板中需要填写完整的内容, 请复制到评论区回复, 注: 内容的标题名称(影响性分析说明, 缺陷严重等级, 受影响版本排查(受影响/不受影响), 修复是否涉及abi变化(是/否))不能省略,省略后defect-manager将无法正常解析填写内容.
评论区可能使用到的指令说明:

指令 指令说明 使用权限
/check-issue 触发defect-manager校验 不限
/reason xxx /reason +挂起或取消条件 不限

影响性分析说明:

缺陷严重等级:(Critical/High/Moderate/Low)

缺陷根因说明:

受影响版本排查(受影响/不受影响):

  1. openEuler-20.03-LTS-SP4:
  2. openEuler-22.03-LTS-SP4:
  3. openEuler-24.03-LTS-SP1:
  4. openEuler-24.03-LTS-SP3:
  5. openEuler-24.03-LTS-SP4:

abi变化(是/否):

  1. openEuler-20.03-LTS-SP4:
  2. openEuler-22.03-LTS-SP4:
  3. openEuler-24.03-LTS-SP1:
  4. openEuler-24.03-LTS-SP3:
  5. openEuler-24.03-LTS-SP4:

缺陷issue处理具体操作请参考:
https://atomgit.com/openeuler/cve-manager/blob/master/cve-vulner-manager/doc/md/defect-manager-manual.md
pr关联issue具体操作请参考:
https://docs.atomgit.com/docs/help/home/org_project/pullrequests/pr-related-issue

likedislike
openeuler-ci-botopeneuler-ci-bot成员
9月14日 添加了label:DEFECT/UNFIXED
openeuler-ci-botopeneuler-ci-bot成员
9月14日 添加了label:sig/iSulad
openeuler-ci-bot
openeuler-ci-bot成员
9月14日 评论:

Welcome To openEuler Community

Hey @yangna1310 , thanks for your contribution to the community.

Bot Usage Manual

I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands. You can self-configure the PR merge rules for this repository. For more details, please refer to Here.

Contact Guide

If you have any questions, please contact the SIG: iSulad ,
and any of the maintainers: @Yekelu, @chantsztung, @haomintsai, @jingxiaolu ,

likedislike
openeuler-ci-botopeneuler-ci-bot成员
9月14日 修改了issue 的描述
openeuler-ci-botopeneuler-ci-bot成员
9月14日 移除了负责人 Yekelu、haomintsai、chantsztung、yangna1310