学习
实践
活动
专区
工具
TVP
写文章
专栏首页梵高先生记一次在Mac系统下因为栈上变量溢出导致的内存泄露问题
原创

记一次在Mac系统下因为栈上变量溢出导致的内存泄露问题

栈上变量溢出导致的内存泄漏问题

背景

在Mac上测试TSM SDK C语言版本的SM2Encrypt接口时,遇到一个内存无法释放的问题:

这个截图里面的意思就是说,我的程序尝试去动态释放一块堆上的内存时报错了,因为这块内存没有被动态分配出来。

Mac机器信息为:

os name: macOS,
os release: 12.4,
os version: 21F79,
os platform: x86_64,
processor name: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz

Apple clang version 12.0.0 (clang-1200.0.32.29)
Target: x86_64-apple-darwin21.5.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

场景还原

SM2Encrypt接口的定义是这样的:

int SM2Encrypt(struct__anonymous *ctx, 
               const unsigned char *in, size_t inlen, 
               const char *strPubKey, size_t pubkeyLen, 
               unsigned char *out, size_t *outlen)

ctx
函数入参 - 上下文

in
函数入参 - 待加密消息

inlen
函数入参 - 消息长度(字节单位)

strPubKey
函数入参 - 公钥

pubkeyLen
函数入参 - 公钥长度

out
函数出参 - 密文 - 应当为out分配的内存长度遵循以下规则:密文长度 = 明文长度 + 96 + ASN1编码增量,其中ASN1编码增量长度不定,为简单起见,可直接分配 密文长度 = 明文长度 + 200,此长度可保证安全

outlen
函数入参和出参 - 这是一个UNIX C风格的函数参数用法,入参请将*outlen置为out指针所指向内存的分配大小,函数返回后*outlen将被置为输出密文的实际长度

我的核心测试代码大概是长这样的(为了方便分析做了一些小修改,主要是打印输出上的修改,不影响逻辑):

void sm2_test() {

    // 分配内存
    size_t test_plain_len = 16;
    unsigned char *test_plain = malloc(test_plain_len * sizeof(unsigned char)); // 动态分配内存
    printf("malloc test_plain success, size:%lu bytes, test_plain pointer value:%x\n",
           test_plain_len * sizeof(unsigned char), test_plain);

    /* *****
     * SM2密文主要由C1、C2、C3三部分构成,
     * 其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,
     * C1固定为32字节,C2的长度与明文相同,C3的长度固定为64字节,
     * 如果涉及到 ASN.1 编码,则整个密文长度将会膨胀,
     * 由于 ASN.1 编码带来的膨胀长度不固定,但是长度值绝对小于 104 字节(200 - C1长度 - C3长度)。
     * 因此密文长度的计算,可以按照 明文长度 + 200 个字节来估算。
     * *****/
    int cipher_len = test_plain_len + 32 + 64 + 104; // 注意!!!这里使用int类型,而非size_t类型
    unsigned char *cipher = malloc(cipher_len * sizeof(unsigned char));
    printf("malloc cipher success, size:%lu bytes, cipher pointer value:%x\n",
           cipher_len * sizeof(unsigned char), cipher);

    // 必须初始化sm2_ctx_t,否则会报错-10012
    sm2_ctx_t global_sm2_ctx;
    int sm2_ctx_ret = SM2InitCtx(&global_sm2_ctx);
    printf("init sm2 ctx without pub key, ret=%d\n", sm2_ctx_ret);

    printf("sizeof(int)=%lu, sizeof(size_t)=%lu, sizeof(int*)=%lu, sizeof(size_t*)=%lu\n",
           sizeof(int), sizeof(size_t), sizeof(int *), sizeof(size_t *));

    /* *****
     * SM2Encrypt 默认使用 C1C3C2_ASN1 模式进行加密
     * *****/
    int encrypt_ret = SM2Encrypt(&global_sm2_ctx,
                                 (unsigned char *) (test_plain), (size_t) (test_plain_len * sizeof(unsigned char)),
                                 SM2_TEST_DEMO_PUB_KEY, SM2_PUBKEY_LEN,
                                 (unsigned char *) (cipher), (int *) (&cipher_len));
    printf("sm2 encrypt, ret code:%d, cipher len:%d\n", encrypt_ret, cipher_len);

    sm2_ctx_ret = SM2FreeCtx(&global_sm2_ctx);
    printf("free sm2 ctx, ret=%d\n", sm2_ctx_ret);

    printf("cipher pointer value:%x\n", cipher);
    free(cipher);
    printf("free cipher success, size:%lu bytes\n", cipher_len * sizeof(unsigned char));

    printf("test_plain pointer value:%x\n", test_plain);
    free(test_plain);
    printf("free test_plain success, size:%lu bytes\n", test_plain_len * sizeof(unsigned char));

}

如背景中所描述的那样,在Mac Intel x86_64环境上运行时,得到了报错:

sm2_test_Darwin_x86_64(49620,0x115fec600) malloc: *** error for object 0x600000000000: pointer being freed was not allocated
sm2_test_Darwin_x86_64(49620,0x115fec600) malloc: *** set a breakpoint in malloc_error_break to debug

而这段代码在Linux上运行时,却可以正确运行???

这里强调下,在Linux系统上,也是intel x86_64的cpu:

os name: Linux,
os release: 3.10.107-1-tlinux2_kvm_guest-0055,
os version: #1 SMP Sat Oct 9 14:12:34 CST 2021,
os platform: x86_64,
processor name: Unknown P6 family

gcc (GCC) 4.8.5
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

对Mac上运行的结果进行分析

毫无疑问,直观的来看,test_plain是通过malloc进行分配的,在没有重复free的情况下,free(test_plain)应该是没有问题的;

但是现在free(test_plain)时,却报错pointer being freed was not allocated,在充分检查了代码中没有主动对test_plain做修改后,那么只剩下一种可能,那就是test_plain在运行过程中被修改了

再次检查了代码,发现,test_plain除了被打印之外,只在调用SM2Encrypt时,作为入参被传进去:

难道是在调用了SM2Encrypt之后,test_plain就被改了???

我们先加两句打印语句,看看是否值真的变化了:

运行结果:

test_plain指针果然变化了!!!

基于Mac环境下的深层次原因分析

通过对现象的分析,大致可以确定,就是在执行了SM2Encrypt之后,test_plain指针被改变了,那么为什么test_plain会被改变呢?

通过仔细对比SM2Encrypt的接口定义与实际传参的类型,可以发现,在传入cipher_len这个参数时,类型上有点点区别:

传入的参数cipher_len定义的是int型,而接口定义的类型是size_t*,通过打印这两种类型,可以发现,在mac上,int与size_t所占用的字节数是不同的:

    printf("sizeof(int)=%lu, sizeof(size_t)=%lu, sizeof(int*)=%lu, sizeof(size_t*)=%lu\n",
           sizeof(int), sizeof(size_t), sizeof(int *), sizeof(size_t *));

		sizeof(int)=4, sizeof(size_t)=8, sizeof(int*)=8, sizeof(size_t*)=8

int型变量占用的字节是4个字节,而size_t型变量占用的字节是8个字节。

而在SM2Encrypt接口中,对于cipher_len是按照size_t的变量进行赋值的,也就是说默认会有8个字节长度的值给到cipher_len,而cipher_len本身定义为int型,只有4个字节,因此必然会多出4个字节。

由于我们是在Mac Intel x86_64的硬件架构上进行编译和运型,x86_64是小端系统,也就是说,变量值0x01020304的排列顺序是:

04 03 02 01

假设SM2Encrpt中,对cipher_len赋值的值为1024(十六进制表示为0x000000000400),

那么其在小端系统的内存中排列为:

00 40 00 00 00 00 00 00

cipher_len只有4个字节,因此只能接收前4个字节:00 00 40 00, 那么多出来的4个字节将会溢出,写入到别的内存中。

再回到这段代码的内存分布,我们会发现,核心测试代码中,以cipher_len被定义为分界点,按照顺序定义了以下变量:

size_t test_plain_len; -- 8字节
unsigned char *test_plain; -- 指针,8字节
int cipher_len; -- 4字节
unsigned char *cipher; -- 指针,8字节

C程序的内存空间分布如图所示:

由于test_plain在栈上与cipher_len相邻,而test_plain在某次运行时,其指向的地址刚好为:0x00000000013e0070,内存排列为:

70 00 3e 01 00 00 00 00 

最终导致其在栈上的分布应该如图所示:

因此,当最后去free(test_plain)时,相当于free的是一个只想地址为0的内存块,进而就会导致文中一开始描述的报错信息。

为了验证这个猜想,我们可以尝试把cipher_len类型改为size_t试试看:

通过运行结果可以看到,test_plain addr为0xb48d3768,而cipher_len addr为0xb48d3760,刚好相差8个字节,因此对cipher_len进行填充时,不会覆盖test_plain。

问题的再进一步抽象与简化

上述基于tsm库的分析,其实可以再次对逻辑进行简化,不依赖外部第三方库进行这种现象的复现。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void overflow_int(size_t *y) {
    *y = 0x1122334455667788;
}

int main() {
    // 定义一个指针,指针本身占用8字节,只向一个8字节的内存空间
    unsigned char *test_plain = malloc(8 * sizeof(unsigned char));
    printf("test_plain addr:%x, test_plain value:%x\n", &test_plain, test_plain);

    // 定义一个int型变量,向这个变量的地址空间拷贝2 * sizeof(x) 个字节
    // 然后观察test_plain的值是否被覆盖
    int x = 0; // 本身4个字节
    printf("x addr:%x\n", &x);
    overflow_int(&x);

    printf("test_plain addr:%x, test_plain value:%x\n", &test_plain, test_plain);

    free(test_plain);

}

在Mac上验证:

在Linux上验证:

如之前分析的那样,test_plain 的值,在经过overflow_int赋值之后,变成了0x11223344。

最后一个疑问:为什么一开始在Linux上运行不会报错?

在文章的最开始,我们提到过,同样的代码,在Mac上运行会报错,但是在Linux上不会报错:

借由前面分析的经验,我们同样适用打印地址的方式,来进行排查,只不过,这次打印地址,我们需要打印完整地址,也就是说,在代码中,将%x替换为%p,代码类似于:

之所以这里需要以%p的形式来打印指针的值,主要是希望获取到完整地址值,避免%x只取低地址位造成的地址截断,话不多说,跑代码看效果:

Mac下的效果:<img src="栈上变量溢出导致的内存泄漏问题.assets/image-20220601222737843.png" alt="image-20220601222737843" style="zoom:50%;" />

Linux下的效果:

通过对指针值的完整打印,我们可以发现:

在Mac下,test_plain指向的地址的值,其高位始终都是0x6000开头,虽然由于cipher_len溢出,造成了4个字节被覆写为0x00,但是高地址位仍然是0x6000,这样最终去free时实际访问的是0x600000000000,而这个地址可能指向其他有效的内存空间,因此会触发OS的异常终止。

而在Linux下,我们会发现,test_plain指向的地址的值,其高位始终都是0x0000,只有低位是有效位,同样由于cipher_len溢出,造成了4个字节被覆写为0x00,最终导致free时,其实是对指向0地址的内存空间做free,而这个动作对于free函数来数,是被允许的,虽然这样最终还是会造成内存泄漏,但是并不会触发OS的异常终止。

至于为什么Linux下指针值只有低位地址,而Mac下却有高位地址呢?这个应该与OS内存管理的设计有关,也与OS是否开启地址随机化有关系,这块后面有时间再慢慢研究吧!

关于TSM

腾讯国密算法(TencentSM)基于《中华人民共和国密码行业标准》研发,算法性能十分优异,在业界处于领先水平。单个方法的执行效率较目前主流国密算法均有提升,特别在加解密方面的性能提升明显。

咨询TSM欢迎联系腾讯安全云鼎实验室。

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

登录 后参与评论
0 条评论

相关文章

  • 一次恐怖的 Java 内存泄漏排查实战

    精讲java
  • 何为内存溢出,何为内存泄露

    内存泄漏定义(memory leak):一个不再被程序使用的对象或变量还在内存中占有存储空间。

    Java架构
  • 内存溢出和内存泄漏的区别

    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了...

    全栈程序员站长
  • JVM 发生 OOM 的 8 种原因、及解决办法

    1、代码中可能存在大对象分配 2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

    乔戈里
  • 1篇文章搞清楚8种JVM内存溢出(OOM)的原因和解决方法

    撸Java的同学,多多少少会碰到内存溢出(OOM)的场景,但造成OOM的原因却是多种多样。

    程序员追风
  • JVM第一篇:一个Java内存泄漏的排查案例

    黄小怪
  • 一次恐怖的 Java 内存泄漏排查实战

    最近在看《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版)这本书,理论+实践结合,深入浅出,强烈推荐给大家。

    Java技术栈
  • 内存溢出和内存泄露

    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了...

    Demo_Yang
  • Android常见内存泄露,学会这六招大大优化APP性能

    很多开发者都知道,在面试的时候会经常被问到内存泄露和内存溢出的问题。 内存溢出(Out Of Memory,简称 OOM),通俗理解就是内存不够...

    分享达人秀
  • Java JVM 内存泄露 基本概念 解析及排查处理办法

    JAVA是垃圾回收语言的一种,开发者无需特意管理内存分配。但是JAVA中还是存在着许多内存泄露的可能性,如果不好好处理内存泄露,会导致APP内存单元无法释放被浪...

    大鹅
  • jvm内存结构

    jvm主要分,堆、方法区、java栈、本地方法栈、程序计数器五个区域,其中方法区和堆区是线程共享的

    leobhao
  • iOS 内存概述

    一般情况下我们是不需要考虑堆栈的大小问题,但是堆栈不是无上限的,过多的递归会导致栈溢出,过多的alloc会导致堆溢出

    花落花相惜
  • iOS内存详解

    一般情况下我们是不需要考虑堆栈的大小问题,但是堆栈不是无上限的,过多的递归会导致栈溢出,过多的alloc会导致堆溢出

    花落花相惜
  • Android内存泄漏分析

    强引用:类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    用户1205080
  • JVM(五)

    固定512M,当Metaspace满了之后,就会触发FULL GC,回收的条件也比较苛刻,如这个类加载器被回收,这个类的所有对象实例都被回收等等,所以一旦Met...

    小土豆Yuki
  • 【编程基础】C语言内存使用的常见问题

    所讨论的“内存”主要指(静态)数据区、堆区和栈区空间。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态...

    程序员互动联盟
  • 【JVM进阶之路】四:直面内存溢出和内存泄漏

    在JVM的几个内存区域中,除了程序计数器外,其他几个运行时区域都有发生内存溢出(OOM)异常的可能。

    三分恶
  • 纳尼,Java 存在内存泄泄泄泄泄泄漏吗?

    Java 最牛逼的一个特性就是垃圾回收机制,不用像 C++ 需要手动管理内存,所以作为 Java 程序员很幸福,只管 New New New 即可,反正 Jav...

    Java帮帮

扫码关注腾讯云开发者

领取腾讯云代金券