C语言stat函数如何防止栈溢出?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

stat 函数简介

stat 是 C 标准库中的一个函数,用于获取文件或文件系统的状态信息,它的原型通常在 <sys/stat.h> 中定义。

c语言 stat eoverflow
(图片来源网络,侵删)
#include <sys/stat.h>
#include <unistd.h> // 用于 S_IS macros
int stat(const char *pathname, struct stat *buf);
  • pathname: 你想查询信息的文件或目录的路径。
  • buf: 一个指向 struct stat 结构体的指针,函数会将文件信息填充到这个结构体中。

struct stat 结构体中包含了文件的元数据,

  • st_dev: 设备 ID。
  • st_ino: inode 号。
  • st_mode: 文件类型和权限。
  • st_nlink: 硬链接数。
  • st_uid: 用户 ID。
  • st_gid: 组 ID。
  • st_size: 文件大小(以字节为单位)。
  • st_atime: 最后访问时间。
  • st_mtime: 最后修改时间。
  • st_ctime: 最后状态改变时间。
  • ...等等。

什么是 stat 溢出?

这里的“溢出”通常指的是 整数溢出,而不是缓冲区溢出,它发生在对 stat 结构体中的某个数值(尤其是 st_size)进行算术运算时,结果超出了该数据类型的表示范围,导致意外的、错误的行为。

核心问题:st_size 的类型

st_size 的类型在不同平台和不同编译器下可能有所不同,但最常见的是:

  • off_t: 这是一个有符号或无符号的整数类型,其大小取决于系统架构(32位或64位)。
    • 32位系统 上,off_t 通常是 32位 的,它能表示的最大值是 2,147,483,647 字节(约 2GB)。
    • 64位系统 上,off_t 通常是 64位 的,它能表示的最大值是 9,223,372,036,854,775,807 字节(约 8 EB)。

溢出就发生在你对 st_size 进行计算时,结果超出了其类型的最大值。

c语言 stat eoverflow
(图片来源网络,侵删)

溢出的场景和示例

整数溢出本身不会直接导致程序崩溃,但它会破坏后续计算的正确性,从而引发更严重的问题,如 缓冲区溢出

场景1:分配内存时溢出

这是最经典和最危险的场景,程序获取文件大小,然后根据这个大小来分配内存。

错误的代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
void read_file_and_process(const char *filename) {
    struct stat st;
    if (stat(filename, &st) == -1) {
        perror("stat");
        return;
    }
    // 假设我们要读取文件内容
    // 错误:直接使用 st_size 作为 malloc 的大小
    char *buffer = (char *)malloc(st.st_size);
    if (buffer == NULL) {
        perror("malloc");
        return;
    }
    printf("Allocated %zu bytes for buffer.\n", st.st_size);
    // ... 读取文件到 buffer ...
    free(buffer);
}
int main() {
    // 假设存在一个恰好比 2GB 小 1 字节的文件
    // st_size = 2147483646 (在32位系统上是有效的)
    read_file_and_process("large_file_1");
    // 假设存在一个比 2GB 大 1 字节的文件
    // st_size = 2147483648 (在32位系统上,off_t 是 32位 int)
    // 这个值会被 "截断" 成一个负数,-2147483648
    read_file_and_process("large_file_2");
    return 0;
}

问题分析:

c语言 stat eoverflow
(图片来源网络,侵删)
  1. 对于 large_file_1

    • st.st_size2147483646
    • malloc(2147483646) 在32位系统上可能会成功(如果内存足够),分配一个非常大的缓冲区。
  2. 对于 large_file_2

    • 文件实际大小是 2147483648 字节(2GB)。
    • 在一个 32位系统 上,off_tint2147483648 这个数字超出了 int 的最大正值(2147483647),发生了整数上溢
    • 结果会变成一个负数,通常是 -2147483648 (0x80000000)。
    • malloc(-2147483648) 的行为是未定义的,在某些实现中,malloc 会将负数转换为无符号整数,导致它尝试分配一个巨大的内存块(2GB),这几乎肯定会失败,返回 NULL
    • 如果代码没有检查 malloc 的返回值,后续向 buffer 写入数据就会导致空指针解引用,程序崩溃。

更隐蔽的错误:

考虑下面的代码,问题更严重:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
void dangerous_copy(const char *src_file, const char *dest_file) {
    struct stat st;
    if (stat(src_file, &st) == -1) {
        perror("stat");
        return;
    }
    // 假设我们要读取一个块,1KB
    size_t chunk_size = 1024;
    // 错误:计算需要分配的块数
    // st.st_size 很大,这个乘法会溢出
    size_t num_chunks = st.st_size / chunk_size; 
    size_t buffer_size = num_chunks * chunk_size; // 这行也可能溢出
    char *buffer = (char *)malloc(buffer_size);
    if (buffer == NULL) {
        perror("malloc");
        return;
    }
    // ... 使用 buffer 读取和写入文件 ...
    free(buffer);
}

st.st_size2^32 - 1 (一个很大的数),num_chunks 也会很大。num_chunks * chunk_size 这个乘法很容易就溢出,如果 num_chunks2^31chunk_size22^31 * 2 = 2^32size_t 是32位的,结果会溢出回 0

malloc(0) 可能会返回一个小的、非空的指针(虽然不推荐这样做),然后程序会向这个很小的缓冲区中写入海量数据,直接导致堆缓冲区溢出,这是极其危险的漏洞。

如何防止 stat 溢出?

预防的核心思想是:永远不要盲目信任来自外部的数值(如文件大小),并在进行任何算术运算前进行充分的检查。

使用更大的数据类型

在进行计算时,使用比原始数据类型更大的数据类型,可以防止中间结果溢出。

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <stdint.h> // for uint64_t
void safe_read_file(const char *filename) {
    struct stat st;
    if (stat(filename, &st) == -1) {
        perror("stat");
        return;
    }
    // 使用 uint64_t 来安全地存储和处理大小
    uint64_t file_size = (uint64_t)st.st_size;
    size_t buffer_size = (size_t)file_size; // 假设我们确实需要分配整个文件
    // 检查转换是否安全
    if (file_size != (uint64_t)buffer_size) {
        fprintf(stderr, "Error: File size is too large to be allocated.\n");
        return;
    }
    // 检查文件大小是否合理
    if (buffer_size > 1024 * 1024 * 1024) { // 限制为 1GB
        fprintf(stderr, "Error: File size exceeds the limit of 1GB.\n");
        return;
    }
    char *buffer = (char *)malloc(buffer_size);
    if (buffer == NULL) {
        perror("malloc");
        return;
    }
    printf("Successfully allocated %zu bytes.\n", buffer_size);
    free(buffer);
}

在运算前进行边界检查

在执行乘法或加法之前,先检查操作数是否会导致溢出。

#include <limits.h> // for SIZE_MAX
// 检查 a * b 是否会溢出 size_t
int is_multiplication_safe(size_t a, size_t b) {
    if (a == 0 || b == 0) return 1;
    return (a <= SIZE_MAX / b);
}
void safe_copy_with_check(const char *src_file) {
    struct stat st;
    if (stat(src_file, &st) == -1) {
        perror("stat");
        return;
    }
    size_t chunk_size = 1024;
    // 先进行除法,再检查乘法
    size_t num_chunks = st.st_size / chunk_size;
    if (!is_multiplication_safe(num_chunks, chunk_size)) {
        fprintf(stderr, "Error: Calculation overflowed.\n");
        return;
    }
    size_t buffer_size = num_chunks * chunk_size;
    // ... 后续操作 ...
}

检查 malloc 的返回值

这是最基本但最重要的一步,永远不要假设 malloc 会成功。

char *buffer = (char *)malloc(buffer_size);
if (buffer == NULL) {
    // 处理错误,而不是继续执行
    perror("malloc failed");
    return;
}

使用 64 位 API

如果你处理的文件可能非常大(超过 2GB),确保你的编译器和库使用的是 64 位的 off_t

  • 在 GCC/Clang 中,你可以使用 -D_FILE_OFFSET_BITS=64 编译选项来强制使用 64 位的 off_tfseeko 等函数。
  • 这使得 st_size 在32位程序中也能表示 64 位的文件大小,从根源上减少了溢出的可能性,但不能完全消除(因为后续的 size_t 可能还是32位的)。
问题 描述 后果 防护措施
stat 整数溢出 stat 结构体中的 st_size(类型为 off_t)进行算术运算时,结果超出其类型表示范围。 分配的内存远小于预期(malloc(0)malloc(负数))。
导致后续的缓冲区溢出(堆溢出)。
程序崩溃或被恶意利用。
检查返回值:始终检查 statmalloc 的返回值。
类型转换:使用更大的数据类型(如 uint64_t)进行中间计算。
边界检查:在乘法/加法前检查是否会溢出。
设置上限:为文件大小或分配的内存设置合理的上限。
使用 64 位 API:编译时使用 -D_FILE_OFFSET_BITS=64

理解 stat 溢出的原理对于编写健壮、安全的 C 语言程序至关重要,它提醒我们,即使是像获取文件大小这样看似无害的操作,也可能成为安全漏洞的根源。

-- 展开阅读全文 --
头像
malloc与realloc有何区别?
« 上一篇 12-01
韩顺平dedecms二次开发视频学什么?
下一篇 » 12-01

相关文章

取消
微信二维码
支付宝二维码

目录[+]