【NPU】Ascend Docker Runtime v26.0.1 之六 mindxcheckutils/ — 超深度逐行分析
·
mindxcheckutils/ — 超深度逐行分析
文件行数:345行
核心职责:路径安全校验、文件信息检查、字符串白名单过滤、日志权限管理
一、模块定位
1.1 业务职责
mindxcheckutils 是整个项目的安全基石。所有外部输入(文件路径、命令行参数、环境变量值)都经过此模块校验。它提供:
- 路径安全校验 — 防目录穿越(
../)、防软链接攻击 - 文件安全校验 — 大小限制、权限检查、UID检查、防setuid/setgid
- 字符串白名单过滤 — 防注入攻击
- 递归文件检查 — 带深度限制的目录递归校验
- 日志权限管理 — 运行后收紧日志文件权限
- 日志前缀生成 — UID+TTY审计信息
1.2 在系统中的位置
1.3 安全威胁模型
二、常量定义
const (
maxLogFieldLength = 64
notValidPath = "not-valid-file-path"
maxAllowFileSize = 102400 // in megabytes
oneMegabytes = 1048576
DefaultSize = 100 // in megabytes
DefaultWhiteList = "-_./~"
DefaultStringSize = 256
DefaultPathSize = 4096
runLogDir = api.RunTimeLogDir
backupLogFileMode os.FileMode = 0400
runLogFileMode os.FileMode = 0750
maxFileNum = 32
getStatErr = "failed to get file stat, error: %v"
groupWriteIndex = 5
otherWriteIndex = 8
permLength = 10
)
var logPrefix = ""
| 常量 | 值 | 用途 |
|---|---|---|
maxAllowFileSize |
102400 (MB) | 文件大小绝对上限~100GB |
DefaultSize |
100 (MB) | 默认文件大小限制100MB |
DefaultWhiteList |
-_./~ |
默认白名单字符 |
DefaultStringSize |
256 | 字符串最大长度 |
DefaultPathSize |
4096 | 路径最大长度(与Linux PATH_MAX一致) |
backupLogFileMode |
0400 | 日志备份权限(仅所有者可读) |
runLogFileMode |
0750 | 日志运行时权限 |
maxFileNum |
32 | 日志目录最大文件数 |
groupWriteIndex |
5 | 权限字符串中组写位位置(`-rwx |
otherWriteIndex |
8 | 权限字符串中其他写位位置 |
权限字符串解析:
索引: 0 1 2 3 4 5 6 7 8 9
权限: - r w x r w x r w x
类型 所有者 组 其他
groupWriteIndex=5— 检查组是否有写权限otherWriteIndex=8— 检查其他用户是否有写权限
三、核心函数逐行解析
3.1 StringChecker — 字符串白名单校验
func StringChecker(text string, minLength, maxLength int, whiteList string) bool {
if len(text) <= minLength || len(text) >= maxLength {
return false
}
for _, char := range text {
if isLetter(char) || isNumber(char) || isInWhiteList(char, whiteList) {
continue
}
return false
}
return true
}
func isLetter(c rune) bool {
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
}
func isNumber(c rune) bool {
return '0' <= c && c <= '9'
}
func isInWhiteList(c rune, whiteList string) bool {
return strings.Contains(whiteList, string(c))
}
逐行解析:
- 长度检查:
len(text) <= minLength或len(text) >= maxLength→ 拒绝- 注意:使用
<=和>=,所以minLength=0意味着空字符串也被拒绝
- 注意:使用
- 逐字符遍历:
- 字母(a-z, A-Z) → 允许
- 数字(0-9) → 允许
- 在白名单中的字符 → 允许
- 其他字符 → 拒绝
- 所有字符都通过 → 返回true
白名单字符说明:
| 字符 | 用途 | 在路径中的含义 |
|---|---|---|
- |
连字符 | 文件名中常见 |
_ |
下划线 | 文件名中常见 |
. |
点 | 当前目录/文件扩展名 |
/ |
斜杠 | 路径分隔符 |
~ |
波浪号 | Home目录 |
|
空格 | 调用方追加(参数分隔) |
安全分析:
- ✅ 防止shell注入:
;,|,&,$,`,()都不在白名单中 - ✅ 防止路径穿越:
..中的.在白名单中,但CheckPath会额外检查 - ⚠️ 注意:StringChecker仅检查字符级别,不检查语义级别(如
../的字符都合法)
3.2 CheckPath — 路径安全校验
func CheckPath(path string, allowLink bool) error {
if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
return fmt.Errorf("invalid path")
}
if strings.Contains(path, "..") || strings.Contains(path, "./") {
return fmt.Errorf("err path %s", path)
}
filePath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("get abs path failed %v", err)
}
if len(filepath.Base(filePath)) > DefaultStringSize {
return fmt.Errorf("path too long: %s", path)
}
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil || (realPath != filePath && !allowLink) {
return fmt.Errorf("symlinks or not existed, failed %v, %v, %v, %v", filePath, err, realPath, allowLink)
}
return nil
}
逐行解析:
- 字符级校验:StringChecker检查字符白名单
- 目录穿越防御:拒绝包含
..或./的路径 - 绝对路径转换:
filepath.Abs将相对路径转为绝对路径 - 文件名长度限制:Base名不超过256字符
- 软链接检查:
EvalSymlinks解析所有符号链接获取真实路径- 如果真实路径与原始路径不同且
!allowLink→ 拒绝 - 如果解析失败(文件不存在) → 拒绝
安全层次:
| 层次 | 检查 | 防御目标 |
|---|---|---|
| 字符级 | StringChecker | 非法字符注入 |
| 语义级 | .. 和 ./ 检查 |
目录穿越 |
| 长度级 | Base名≤256 | 缓冲区溢出 |
| 链接级 | EvalSymlinks | 软链接攻击 |
3.3 CheckFileInfo — 文件信息安全校验
func CheckFileInfo(fileInfo *os.File, size int) error {
if fileInfo == nil {
return fmt.Errorf("file info is nil")
}
fileStat, err := fileInfo.Stat()
if err != nil {
return fmt.Errorf("invalid file")
}
if !fileStat.Mode().IsRegular() {
return fmt.Errorf("invalid regular file")
}
if size > maxAllowFileSize || size < 0 {
return fmt.Errorf("invalid size")
}
if fileStat.Size() > int64(size)*int64(oneMegabytes) {
return fmt.Errorf("size too large")
}
perm := fileStat.Mode().Perm().String()
if len(perm) != permLength {
return fmt.Errorf("permission not right %v %v", fileInfo.Name(), perm)
}
for index, char := range perm {
switch index {
case groupWriteIndex, otherWriteIndex:
if char == 'w' {
return fmt.Errorf("write permission not right %v %v", fileInfo.Name(), perm)
}
default:
}
}
stat, ok := fileStat.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("can not get stat %v", fileInfo.Name())
}
uid := int(stat.Uid)
if !(uid == 0 || uid == os.Getuid()) {
return fmt.Errorf("owner not right %v %v", fileInfo.Name(), uid)
}
if fileStat.Mode()&os.ModeSetuid != 0 {
return fmt.Errorf("setuid not allowed %v", fileInfo.Name())
}
if fileStat.Mode()&os.ModeSetgid != 0 {
return fmt.Errorf("setgid not allowed %v", fileInfo.Name())
}
return nil
}
逐行解析:
- nil检查:防止空指针
- 获取文件信息:
fileInfo.Stat() - 常规文件检查:
!fileStat.Mode().IsRegular()— 拒绝设备文件、管道、套接字等 - 大小参数校验:size参数本身有效性检查
- 文件大小检查:
fileStat.Size() > size * 1MB— 防止超大文件DoS - 权限字符串检查:
Perm().String()返回如"-rwxr-xr-x"(10字符)- 长度必须为10
- 索引5(组写位)不能为’w’
- 索引8(其他写位)不能为’w’
- UID检查:
stat.Uid— 文件所有者必须是root(0)或当前用户 - setuid检查:
ModeSetuid— 拒绝setuid文件(防止权限提升) - setgid检查:
ModeSetgid— 拒绝setgid文件
权限检查详解:
-rwx|rwx|rwx
↑ ↑
索引5 索引8
组写 其他写
不能w 不能w
设计意图:确保配置文件、可执行文件等关键文件不被组用户或其他用户篡改。只有文件所有者(root或当前用户)可以修改。
3.4 RealFileChecker — 完整文件安全校验
func RealFileChecker(path string, checkParent, allowLink bool, size int) (string, error) {
if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
return notValidPath, fmt.Errorf("invalid path")
}
_, err := FileChecker(path, false, checkParent, allowLink, 0)
if err != nil {
hwlog.RunLog.Errorf("failed to check files, error: %v", err)
return notValidPath, err
}
realPath, err := filepath.Abs(path)
if err != nil {
return notValidPath, err
}
realPath, err = filepath.EvalSymlinks(realPath)
if err != nil {
return notValidPath, err
}
fileInfo, err := os.Stat(realPath)
if err != nil {
return notValidPath, err
}
if !fileInfo.Mode().IsRegular() {
return notValidPath, fmt.Errorf("invalid regular file")
}
if size > maxAllowFileSize || size < 0 {
return notValidPath, fmt.Errorf("invalid size")
}
if fileInfo.Size() > int64(size)*int64(oneMegabytes) {
return notValidPath, fmt.Errorf("size too large")
}
return realPath, nil
}
与CheckFileInfo的区别:
| 维度 | RealFileChecker | CheckFileInfo |
|---|---|---|
| 输入 | 路径字符串 | *os.File句柄 |
| 返回 | 真实路径字符串 | 无返回值 |
| 路径校验 | StringChecker + FileChecker | 无 |
| 软链接 | EvalSymlinks解析 | 无 |
| 权限检查 | FileChecker中完成 | 直接检查 |
| 使用场景 | 从路径校验文件 | 从已打开文件校验 |
执行流程:
- StringChecker字符校验
- FileChecker递归校验(包括权限、UID、setuid/setgid检查)
- 获取绝对路径
- 解析符号链接
- Stat获取文件信息
- 常规文件检查
- 大小检查
- 返回真实路径
3.5 RealDirChecker — 目录安全校验
func RealDirChecker(path string, checkParent, allowLink bool) (string, error) {
if !StringChecker(path, 0, DefaultPathSize, DefaultWhiteList) {
return notValidPath, fmt.Errorf("invalid path")
}
_, err := FileChecker(path, true, checkParent, allowLink, 0)
if err != nil {
return notValidPath, err
}
realPath, err := filepath.Abs(path)
if err != nil {
return notValidPath, err
}
realPath, err = filepath.EvalSymlinks(realPath)
if err != nil {
return notValidPath, err
}
fileInfo, err := os.Stat(realPath)
if err != nil {
return notValidPath, err
}
if !fileInfo.IsDir() {
return notValidPath, fmt.Errorf("not a dir")
}
return realPath, nil
}
与RealFileChecker的区别:
FileChecker(path, true, ...)— 第二参数allowDir=true- 最后检查
fileInfo.IsDir()而非IsRegular() - 无大小检查(目录没有大小限制)
3.6 FileChecker — 递归文件/目录校验
func FileChecker(path string, allowDir, checkParent, allowLink bool, deep int) (bool, error) {
const maxDepth, groupWriteIndex, otherWriteIndex, permLength int = 99, 5, 8, 10
if deep > maxDepth {
return false, fmt.Errorf("over maxDepth %v", maxDepth)
}
if strings.Contains(path, "..") || strings.Contains(path, "./") {
return false, fmt.Errorf("err path %s", path)
}
filePath, err := filepath.Abs(path)
if err != nil {
return false, fmt.Errorf("get abs path failed %v", err)
}
if len(filepath.Base(filePath)) > DefaultStringSize {
return false, fmt.Errorf("path too long")
}
fileInfo, ok, err := normalFileCheck(filePath, allowDir, allowLink)
if err != nil {
return ok, err
}
perm := fileInfo.Mode().Perm().String()
if len(perm) != permLength {
return false, fmt.Errorf("permission not right %v %v", filePath, perm)
}
for index, char := range perm {
switch index {
case groupWriteIndex, otherWriteIndex:
if char == 'w' {
return false, fmt.Errorf("write permission not right %v %v", filePath, perm)
}
default:
}
}
stat, ok := fileInfo.Sys().(*syscall.Stat_t)
if !ok {
return false, fmt.Errorf("can not get stat %v", filePath)
}
uid := int(stat.Uid)
if !(uid == 0 || uid == os.Getuid()) {
return false, fmt.Errorf("owner not right %v %v", filePath, uid)
}
if filePath != "/" && checkParent {
return FileChecker(filepath.Dir(filePath), true, true, allowLink, deep+1)
}
return true, nil
}
逐行解析:
- 递归深度检查:
deep > 99→ 拒绝(防无限递归) - 目录穿越检查:
..和./ - 绝对路径转换
- 文件名长度限制:Base名≤256
- normalFileCheck:检查文件类型、软链接、setuid/setgid
- 权限检查:组写位和其他写位不能为’w’
- UID检查:文件所有者必须是root或当前用户
- 递归检查父目录:如果
checkParent=true且不是根目录,递归向上检查父目录
递归检查设计意图:即使目标文件本身安全,如果其父目录可被其他用户写入,攻击者可以替换文件。因此需要递归向上检查整个路径的所有组件。
3.7 normalFileCheck — 基础文件检查
func normalFileCheck(filePath string, allowDir bool, allowLink bool) (os.FileInfo, bool, error) {
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil || (realPath != filePath && !allowLink) {
return nil, false, fmt.Errorf("symlinks or not existed, failed %v, %v", filePath, err)
}
fileInfo, err := os.Stat(filePath)
if err != nil {
return nil, false, fmt.Errorf("get file stat failed %v", err)
}
if allowDir {
if !fileInfo.Mode().IsRegular() && !fileInfo.IsDir() {
return nil, false, fmt.Errorf("not regular file/dir %v", filePath)
}
} else {
if !fileInfo.Mode().IsRegular() {
return nil, false, fmt.Errorf("not regular file %v", filePath)
}
}
if fileInfo.Mode()&os.ModeSetuid != 0 {
return nil, false, fmt.Errorf("setuid not allowed %v", filePath)
}
if fileInfo.Mode()&os.ModeSetgid != 0 {
return nil, false, fmt.Errorf("setgid not allowed %v", filePath)
}
return fileInfo, false, nil
}
逐行解析:
- 软链接解析:
EvalSymlinks获取真实路径- 如果解析失败 → 拒绝(文件不存在或循环链接)
- 如果真实路径≠原始路径且
!allowLink→ 拒绝
- Stat获取文件信息
- 文件类型检查:
allowDir=true:允许文件或目录allowDir=false:仅允许常规文件
- setuid检查:
ModeSetuid→ 拒绝 - setgid检查:
ModeSetgid→ 拒绝
3.8 GetLogPrefix — 审计前缀生成
func GetLogPrefix() (string, error) {
if logPrefix != "" {
return logPrefix, nil
}
uid := os.Geteuid()
if uid < 0 {
return "", fmt.Errorf("err: uid not right")
}
tty, err := filepath.EvalSymlinks("/proc/self/fd/0")
if err != nil || !StringChecker(tty, 0, DefaultStringSize, "/:") {
tty = "unknown"
}
logPrefix = fmt.Sprintf("uid: %d tty: %v ", uid, tty)
return logPrefix, nil
}
逐行解析:
- 缓存机制:
logPrefix全局变量,首次调用后缓存结果 - 获取有效用户ID:
os.Geteuid()— 获取当前进程的有效UID - 获取终端路径:
/proc/self/fd/0是stdin的符号链接,指向实际终端EvalSymlinks解析链接- 如果失败或包含非法字符 → 设为"unknown"
- 注意:白名单中包含
/和:``,允许/dev/pts/0`这样的路径
- 格式化前缀:
"uid: 0 tty: /dev/pts/0 "
设计意图:审计日志需要知道是谁在什么终端执行了操作。UID和TTY是基本的审计信息。
3.9 ChangeRuntimeLogMode — 日志权限管理
func ChangeRuntimeLogMode(runLog string) error {
runLogDirLen := len(runLogDir)
counter := 0
err := filepath.Walk(runLogDir, func(fileOrPath string, fileInfo os.FileInfo, err error) error {
counter += 1
if counter > maxFileNum {
return fmt.Errorf("the counter file is over maxFileNum")
}
if err != nil {
hwlog.RunLog.Errorf("prevent panic by handling failure accessing a path %q: %v", fileOrPath, err)
return err
}
hasLogPrefix := strings.HasPrefix(fileOrPath[runLogDirLen:], runLog)
if !hasLogPrefix {
return nil
}
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
return fmt.Errorf("the file or path is symlink")
}
if errChmod := os.Chmod(fileOrPath, backupLogFileMode); errChmod != nil {
hwlog.RunLog.Errorf("set file mode %s failed, error: %v", fileOrPath, err)
return fmt.Errorf("set file mode %s failed", fileOrPath)
}
return nil
})
if err != nil {
hwlog.RunLog.Warnf("change runtime log mode failed, error: %v", err)
return nil
}
return nil
}
逐行解析:
runLogDirLen— 日志目录路径长度,用于后续前缀匹配counter— 文件计数器,超过32个文件则终止(安全限制)filepath.Walk— 递归遍历日志目录- 文件计数:
counter > maxFileNum(32)→ 返回错误(防DoS) - 前缀匹配:
fileOrPath[runLogDirLen:]获取相对路径,检查是否以runLog参数开头- 例如:
runLog="runtime-run-",匹配runtime-run-20260726.log
- 例如:
- 软链接检查:如果是符号链接 → 拒绝(防符号链接攻击)
- 修改权限:
os.Chmod(fileOrPath, 0400)— 仅所有者可读 - 错误容忍:Walk返回错误时仅记录Warn日志,不返回错误(确保不影响主流程)
四、安全检查矩阵
| 检查项 | 防御目标 | 实现函数 |
|---|---|---|
| 字符白名单 | 命令注入 | StringChecker |
| 长度限制 | 缓冲区溢出/DoS | StringChecker |
| 目录穿越 | 访问越权文件 | CheckPath/FileChecker |
| 软链接 | 链接攻击 | EvalSymlinks |
| 常规文件 | 设备文件/管道攻击 | IsRegular() |
| 文件大小 | DoS | CheckFileInfo |
| 组/其他不可写 | 篡改攻击 | Perm()检查 |
| UID检查 | 非授权文件 | Stat_t.Uid |
| setuid/setgid | 权限提升 | ModeSetuid/ModeSetgid |
| 递归深度 | 无限递归DoS | maxDepth=99 |
| 递归父目录 | 父目录替换攻击 | FileChecker递归 |
五、函数调用关系图
六、关键设计决策
| 决策 | 原因 | 实现 |
|---|---|---|
| 字符级白名单 | 防注入比黑名单更安全 | StringChecker仅允许字母+数字+白名单字符 |
| 组/其他不可写 | 防止非授权用户修改关键文件 | Perm检查索引5和8不能为’w’ |
| 仅root或当前用户所有 | 防止攻击者植入文件 | Stat_t.Uid检查 |
| 拒绝setuid/setgid | 防止权限提升 | ModeSetuid/ModeSetgid检查 |
| 递归检查父目录 | 防止父目录可写导致文件被替换 | FileChecker递归向上 |
| 递归深度限制99 | 防无限递归DoS | deep > maxDepth |
| 文件计数限制32 | 防遍历DoS | ChangeRuntimeLogMode中counter |
| 日志权限收紧0400 | 日志含敏感信息 | ChangeRuntimeLogMode Chmod |
| 缓存日志前缀 | 避免重复读取/proc | logPrefix全局变量 |
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)