Files
tee_tee_dev_kit/docs/opentrustee-guidelines/opentrustee-security-driver-guidelines.md
suwanghw bf5fd53b38 add guide for tee
Signed-off-by: suwanghw <wangsu14@huawei.com>
2025-01-13 14:26:05 +08:00

90 KiB
Raw Permalink Blame History

TEE 安全驱动开发指导

概述

功能简介

本文对TEE安全驱动的开发流程、接口、函数库等进行说明,指导驱动开发者进行安全驱动程序的开发与调试工作。

约束与限制

  • 开发的驱动程序仅支持在TEE可信执行环境子系统上加载和运行。

场景介绍

目前TEEOS支持内置驱动的开发,驱动和TEEOS镜像共同编译打包。具体开发指导请参考开发示例章节。

驱动开发框架

驱动开发整体可以分为两部分,第一个是驱动业务开发框架,第二个是访问该驱动的驱动访问者框架。

驱动业务框架

为方便各个驱动开发统一,TEE可信执行环境子系统为各个驱动设计了一套基础框架,如下所示,驱动开发者结合驱动实际逻辑分别定义这些基础函数即可。

#define DRV_NAME_MAX_LEN 32U
#define DRV_RESERVED_NUM 8U

struct drv_data {
    int32_t fd; /* fd信息,对应驱动的唯一标识 */
    uint32_t taskid; /* 访问者的taskid */
    void *private_data; /* 私有数据 */
    struct tee_uuid uuid; /* 访问者的uuid */
};

/* 驱动加载时初始化函数 */
typedef int32_t (*init_func)(void);

/* 驱动在系统休眠时被调用的函数 */
typedef int32_t (*suspned_func)(void);

/* 驱动在系统唤醒时被调用的函数 */
typedef int32_t (*resume_func)(void);

/* 驱动被访问时命令分发函数 */
typedef int64_t (*ioctl_func)(struct drv_data *drv, uint32_t cmd, unsigned long args, uint32_t args_len);

/* 驱动被访问时初始化函数 */
typedef int64_t (*open_func)(struct drv_data *drv, unsigned long args, uint32_t args_len);

/* 驱动被访问结束时资源释放函数 */
typedef int64_t (*close_func)(struct drv_data *drv);

struct tee_driver_module {
    init_func init;
    ioctl_func ioctl;
    open_func open;
    close_func close;
    suspned_func suspend;
    resume_func resume;
    suspned_func suspend_s4;
    resume_func resume_s4;
    uint64_t reserved[DRV_RESERVED_NUM]; /* has not used, just reserved */
};

#define tee_driver_declare(name, init, open, ioctl, close, suspend, resume, suspend_s4, resume_s4) \
__attribute__((visibility("default"))) const struct tee_driver_module g_driver_##name = { \
    init, ioctl, open, close, suspend, resume, suspend_s4, resume_s4, {0} }

结构体struct tee_driver_module便是每个驱动业务开发时需要注册的信息,各个变量说明如下:

表 1 驱动业务开发注册信息表

变量名

类型

说明

name

常量字符串

驱动名字,每个驱动必须唯一,有效长度小于32个字节,仅支持数字、字母和'_'。

init

init_func函数指针

驱动加载时初始化函数。

open

函数指针

驱动被访问时初始化函数。

ioctl

函数指针

驱动被访问时命令分发函数。

close

函数指针

驱动被访问结束时资源释放函数。

suspend

函数指针

驱动在系统休眠时被调用的函数。

resume

函数指针

驱动在系统唤醒时被调用的函数。

suspend_s4

函数指针

驱动在系统休眠时被调用的函数,对应linux kernel里freeze_noirq操作流程。

resume_s4

函数指针

驱动在系统唤醒时被调用的函数,对应linux kernel里restore_noirq操作流程。

reserved

预留

现有框架暂未使用。

表 2 驱动业务框架接口说明

接口名

描述

int32_t (*init_func)(void)

该函数是在驱动加载完后便被驱动框架调用的初始化函数,其主要作用是在该驱动被访问之前进行初始化操作。该函数在驱动加载后的整个生命周期内只会被调用一次。

返回值:

0:初始化成功

非0:初始化失败

int64_t (*open_func)(struct drv_data *drv, unsigned long args, uint32_t args_len)

该函数是驱动访问者访问驱动时调用的初始化函数,其主要作用是在驱动里申请一个fd,并进行初始化。该函数在每次新增驱动访问时都会调用。

参数:

drv:入参,表示此次open该驱动的fd所有信息

args:入参,表示给驱动传入参数对应的buffer地址,由驱动访问者设置

args_len:入参,表示给驱动传入参数对应的buffer长度,由驱动访问者设置

返回值:

非正数:异常值

其他:返回的fd信息

int64_t (*ioctl_func)(struct drv_data *drv, uint32_t cmd, unsigned long args, uint32_t args_len)

该函数是驱动在open初始化之后,进行的一系列针对业务逻辑的操作。其主要作用是根据传入的drv获取fd资源信息,再执行cmd命令对应的执行流,其中[args.args_len]组成的buffer信息是给该cmd执行流传入的参数。

参数:

drv:入参,表示此次ioctl该驱动的fd所有信息,fd由驱动访问者传入,驱动框架根据fd传入对应drv_data结构体

cmd:入参,表示此次ioctl该驱动传入的命令号,驱动可根据不同命令号执行不同的业务逻辑,由驱动访问者设置

args:入参,表示给驱动传递参数的buffer基地址

args_len:入参,表示给驱动传递参数的buffer长度

返回值:

0:操作成功

-1:操作失败,框架层面返回的异常值

其他值:操作失败,驱动自行定义的异常值

int64_t (*close_func)(struct drv_data *drv)

该函数主要作用是在驱动业务逻辑访问结束后进行对该fd对应的资源清理操作。

参数:

drv:入参,表示此次ioctl该驱动的fd所有信息

返回值:

0:操作成功

非0:操作失败

int32_t (*suspned_func)(void)

该函数主要作用是此驱动休眠状态下的一些列操作,会在系统休眠时有驱动框架自行调用。

返回值:

0:操作成功

非0:操作失败

int32_t (*resume_func)(void)

该函数主要作用是此驱动唤醒态下的一系列操作,会在系统唤醒流程中由驱动框架自行调用,与suspend函数对应。

返回值:

0:操作成功

非0:操作失败

驱动访问者框架

驱动访问时,驱动访问者先调用tee_drv_open函数获取驱动唯一标记fd;再调用tee_drv_ioctl函数,传入cmd信息,访问该驱动对应cmd执行流,如果针对某个fd有多个ioctl执行流,多次调用tee_drv_ioctl即可;如果访问结束,驱动访问者还需要调用tee_drv_close函数关闭该fd信息。

表 3 驱动访问者框架接口说明

接口名

描述

int64_t tee_drv_open(const char *drv_name, const void *param, uint32_t param_len)

主要作用是驱动访问者通过调用该函数,访问drv_name指定的驱动,调用驱动的open函数,返回与该驱动对应的唯一标记fd信息。其中param buffer对应的内容组装结构由drv_name对应驱动定义,实际与驱动open函数里[args.args_len]表示的buffer内容一致。

参数:

drv_name:入参,表示要访问的驱动名称;

param:入参,表示给驱动传递的参数地址;

param_len:入参,表示给驱动传递的参数长度,与param组成的buffer内容便是给驱动传递的参数信息。

返回值:

非正数:非法值,操作失败

大于0fd信息,对应驱动的唯一标识

int64_t tee_drv_ioctl(int64_t fd, uint32_t cmd_id, const void *param, uint32_t param_len)

主要作用是驱动访问者通过调用该函数,访问fd对应的驱动模块,执行命令ID号为cmd_id对应的业务逻辑,传入参数为param与param_len对应buffer存储的内容,其中param与param_len对应buffer内容组装结构由fd对应的驱动定义,实际与驱动ioctl函数[args.args_len]组成的buffer内容一致。

参数:

fd:入参,表示open该驱动成功返回时的返回值;

cmd_id:入参,表示ioctl该驱动时对应的命令id号;

param:入参,表示ioctl该驱动cmd_id流程时传入的参数基地址;

param_len:入参,表示ioctl该驱动cmd_id流程时传入的参数buffer长度。与param组成的buffer内容便是给驱动命令号cmd_id对应的参数信息。

返回值:

0:操作成功

-1:框架层面访问失败

其他值:各驱动自定义失败返回值

int64_t tee_drv_close(int64_t fd)

主要作用是关闭fd对应驱动信息,一般常见操作是释放该fd维护的驱动资源。

返回值:

0:操作成功

-1:操作失败

接口说明

地址转换接口说明

驱动进行地址转换操作需要使用的接口列表。

表 4 地址转换接口列表

接口名

描述

uint64_t drv_virt_to_phys(uintptr_t addr);

虚拟地址转换为物理地址。

map接口说明

驱动进行内存映射操作所需要的接口列表。

表 5 map接口列表

接口名

描述

int32_t tee_map_secure(paddr_t paddr, uint64_t size, uintptr_t *vaddr, cache_mode_type cache_mode);

给驱动访问者映射一段安全属性的物理内存。

int32_t tee_map_nonsecure(paddr_t paddr, uint64_t size, uintptr_t *vaddr, cache_mode_type cache_mode);

给驱动访问者映射一段非安全属性的物理内存,其中映射属性是只读不能写。

IO操作接口说明

驱动进行IO操作所需要的接口列表。

表 6 IO操作接口列表

接口名

描述

void *ioremap(uintptr_t phys_addr, unsigned long size, int32_t prot);

将IO地址映射至虚拟地址。

int32_t iounmap(uintptr_t pddr, void *addr);

解除物理地址映射。

void read_from_io (void *to, const volatile void *from, unsigned long count);

将IO输入的值读取至驱动指定的地址,读取长度由count指定。

void write_to_io(volatile void *to, const void *from, unsigned long count);

将驱动指定地址的值输出至IO,读取长度由count指定。

内存拷贝接口说明

驱动进行内存拷贝操作所需要的接口列表。

表 7 内存拷贝接口列表

接口名

描述

int32_t copy_from_client(uint64_t src, uint32_t src_size, uintptr_t dst, uint32_t dst_size);

将驱动内存拷入client端。

int32_t copy_to_client(uintptr_t src, uint32_t src_size, uint64_t dst, uint32_t dst_size);

将client端数据拷出至驱动。

共享内存接口说明

驱动进行共享内存操作所需要的接口列表。

表 8 共享内存接口列表

接口名

描述

void *tee_alloc_sharemem_aux(const struct tee_uuid *uuid, uint32_t size);

申请进程间通信共享内存。

uint32_t tee_free_sharemem(void *addr, uint32_t size);

释放进程间通信共享内存。

开发示例

编译文件配置

  • 进入base/tee/tee_os_framework/目录
  • 首先在drivers/目录新建一个目录,例如demo_driver
  • 在demo_driver目录新建src目录用于存放源代码文件,新建Makefile编译文件

Makefile示例如下

DRIVER := demo_driver.elf

include $(BUILD_CONFIG)/var.mk

demo_driver_c_files += $(wildcard src/*.c) 

#include
inc-flags += -I./src

# Libraries

SVC_PARTITIAL_LINK = y
include $(BUILD_SERVICE)/svc-common.mk
  • 修改build/mk/common/operation/project.mk,增加drivers += demo_driver
  • 修改build/mk/pack/plat_config.mk, 增加以下内容
ifeq ($(CONFIG_ARCH_AARCH64),y)
product_apps += $(OUTPUTDIR)/aarch64/drivers/demo_driver.elf
check-a64-syms-y += $(OUTPUTDIR)/aarch64/drivers/demo_driver.elf
else
product_apps += $(OUTPUTDIR)/arm/drivers/demo_driver.elf
check-syms-y += $(OUTPUTDIR)/arm/drivers/demo_driver.elf
endif

驱动开发示例

驱动框架注册实例如下:

#define TEST_NUM_INIT_VAL 0x11
#define TEST_NUM_ADD_TAG 0x10
static int32_t g_test_num = TEST_NUM_INIT_VAL;

/* 驱动初始化函数 */
int32_t init_test(void)
{
    if (g_test_num != TEST_NUM_INIT_VAL) {
        tloge("driver init test failed, g_test_num:0x%x not equal 0x%x\n", g_test_num, TEST_NUM_INIT_VAL);
        return -1;
    }

    g_test_num += TEST_NUM_ADD_TAG;
    tloge("driver init test end\n");
    return 0;
}

/* buffer校验的相关逻辑 */
static int32_t buf_check(uint32_t *buf, uint32_t size, uint32_t args)
{
    if (buf == NULL) {
        tloge("buf is invalid, check failed\n");
        return -1;
    }

    uint32_t i;
    for (i = 0; i < size; i++) {
        if (buf[i] != args) {
            tloge("buf[%u]=%u which not equal %u\n", i, buf[i], args);
            return -1;
        }
    }

    return 0;
}

/* 驱动具体业务处理逻辑函数 */
static int64_t args_test(struct drv_data *drv, unsigned long args, uint32_t args_len)
{
    /* cmd处理函数在使用参数之前需要判断参数合法性 */
    if (args_len != sizeof(uint32_t) || args == 0) {
        tloge("invalid args args_len:%u\n", args_len);
        return -1;
    }

    uint32_t *input_args = (uint32_t *)(uintptr_t)args;

    tloge("driver args test begin: drv.fd=%d args=0x%x g_test_num:0x%x\n", drv->fd, *input_args, g_test_num);
    int64_t ret = buf_check(drv->private_data, TOKEN_BUF_SIZE, *input_args);
    if (ret != 0)
        tloge("args test FAIL\n");
    else
        tloge("args test SUCC\n");

    return ret;
}

/* 驱动命令分发函数,根据不同cmdid调用不同函数逻辑 */
int64_t ioctl_test(struct drv_data *drv, uint32_t cmd, unsigned long args, uint32_t args_len)
{
    if (drv == NULL) { /* drv可以在调用业务逻辑之前判断,args和args_len需要根据具体cmdid逻辑去判断 */
        tloge("ioctl invalid drv\n");
        return -1;
    }

    int64_t ret = -1;

    switch (cmd) {
    case ARGS_TEST_ID:
        ret = args_test(drv, args, args_len);
        break;
    default:
        tloge("cmd:0x%x not support\n", cmd);
    }

    return ret;
}

/* buffer 初始化 */
static uint32_t *buf_init(uint32_t args)
{
    uint32_t *buf = (uint32_t *)malloc(TOKEN_BUF_SIZE * sizeof(uint32_t));
    if (buf == NULL) {
        tloge("alloc buf failed\n");
        return NULL;
    }

    int32_t i;
    for (i = 0; i < TOKEN_BUF_SIZE; i++)
        buf[i] = args;

    return buf;
}

/* 驱动被访问时初始化函数 */
int64_t open_test(struct drv_data *drv, unsigned long args, uint32_t args_len)
{
    if (drv == NULL) {
        tloge("open invalid drv\n");
        return -1;
    }

    if (args_len < sizeof(uint32_t) || args == 0) {
        tloge("open invalid drv\n");
        return -1;
    }

    uint32_t *input = (uint32_t *)args;
    if (*input == UINT32_MAX) {
        tloge("open test input args is UINT32_MAX, just retrun -1\n");
        return -1;
    }

    uint32_t *buf = buf_init(*input);
    if (buf == NULL)
        return -1;

    /* 将buf赋值给fd结构的private_data,后续都可以通过此fd获取到buf内容 */
    drv->private_data = buf;
    tloge("driver open test begin: fd=%d args=0x%x g_test_num:0x%x\n", drv->fd, *input, g_test_num);

    return 0;
}

/* 驱动业务逻辑访问结束后的资源清理操作 */
int64_t close_test(struct drv_data *drv)
{
    if (drv == NULL) {
        tloge("close invalid drv\n");
        return -1;
    }

    tloge("driver close test begin: fd:%d g_test_num:0x%x\n", drv->fd, g_test_num);
    /* close流程需要释放fd对应的所有资源 */
    if (drv->private_data != NULL) {
        tloge("free private data in close\n");
        free(drv->private_data);
    }

    return 0;
}

/* 在系统休眠时由驱动框架自行调用 */
int32_t suspend_test(void)
{
    tloge("suspend test begin\n");
    return 0;
}

/* 在系统唤醒时有驱动框架自行调用 */
int32_t resume_test(void)
{
    tloge("resume test begin\n");
    return 0;
}

/* 在系统s4休眠时被调用的函数 */
int32_t suspend_s4_test(void)
{
    tloge("suspend_s4 test begin\n");
    return 0;
}

/* 在系统s4唤醒时被调用的函数 */
int32_t resume_s4_test(void)
{
    tloge("resume_s4 test begin\n");
    return 0;
}

/* 驱动框架注册 */
tee_driver_declare(drv_test_module, init_test, open_test, ioctl_test, close_test, \
                   suspend_test, resume_test, suspend_s4_test, resume_s4_test);

驱动访问者开发示例


#include <securec.h>
#include <stdlib.h>
#include <string.h>
#include <tee_drv_client.h>
#include <tee_ext_api.h>
#include <tee_log.h>
#include <test_drv_cmdid.h>
#include <mem_ops.h>


#define DRV_UUID1                                          \
    {                                                      \
        0x11112222, 0x0000, 0x0000,                        \
        {                                                  \
            0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 \
        }                                                  \
    }

#define BUFFER_SIZE 1024
struct share_buffer_arg {
    uint64_t addr;
    uint32_t len;
    uint32_t share_token;
};

/* 访问驱动的各个流程,在TA的TA_InvokeCommandEntryPoint阶段被执行 */
static TEE_Result TeeTestDrive(uint32_t cmd)
{
    int ret;
    const char *drvName = "drv_test_module";
    uint32_t args = (uint32_t)(&drvName);
    const char drvcallerInput[] = "the param is drvcaller_input";
    char drvOutput[] = "DRVMEM_OUTPUT";

    uint32_t drvcallerInputLen = (uint32_t)strlen(drvcallerInput) + 1;
    uint32_t drvOutputLen = (uint32_t)strlen(drvOutput) + 1;
    TEE_UUID uuid = DRV_UUID1;

    /* 调用驱动的open函数,返回与该驱动对应的唯一标记fd信息 */
    int64_t fd = tee_drv_open(drvName, &args, sizeof(args));
    if (fd <= 0) {
        tloge("open %s for get fd failed\n", drvName);
        return TEE_ERROR_GENERIC;
    }

    char *tempBuffer = tee_alloc_sharemem_aux(&uuid, BUFFER_SIZE);
    if (tempBuffer == NULL) {
        tloge("alloc share mem failed\n");
        return TEE_ERROR_GENERIC;
    }
    (void)memset_s(tempBuffer, BUFFER_SIZE, 0x0, BUFFER_SIZE);
    ret = strcpy_s(tempBuffer, drvcallerInputLen, drvcallerInput);
    if (ret != 0) {
        tloge("strcpy_s failed,ret = 0x%x\n", ret);
        return TEE_ERROR_GENERIC;
    }

    struct share_buffer_arg inputArg = { 0 };
#ifndef __aarch64__
    inputArg.addr = (uint64_t)(uint32_t)tempBuffer;
#else
    inputArg.addr = (uint64_t)tempBuffer;
#endif

    inputArg.len = BUFFER_SIZE;

    tlogi("%s drv test ioctl begin args:0x%x fd:%d\n", drvName, inputArg, (int32_t)fd);

    /* 访问fd对应的驱动模块,执行命令ID号为cmd_id对应的业务逻辑 */
    ret = (int)tee_drv_ioctl(fd, cmd, (const void *)(&inputArg), sizeof(inputArg));
    if (ret != 0) {
        tloge("%s drv test ioctl failed, fd:%d \n", drvName, (int32_t)fd);
    }
    if (cmd == DRVTEST_COMMAND_COPYTOCLIENT) {
        if (strncmp(drvOutput, (char *)tempBuffer, drvOutputLen) != 0) {
            tloge("%s drv copy_to_client test failed, fd:%d, heap_buffer is:%s \n", drvName, (int32_t)fd, tempBuffer);
            free_sharemem(tempBuffer, BUFFER_SIZE);
            return TEE_ERROR_GENERIC;
        }
    }

    /* 关闭fd对应驱动信息 */
    ret |= (int)tee_drv_close(fd);
    if (ret != 0) {
        tloge("drv test fail!\n");
    }

    if (free_sharemem(tempBuffer, BUFFER_SIZE) != 0) {
        tloge("free sharemem failed\n");
        ret = -1;
    }
    return (TEE_Result)ret;
}

/* TA实例的构造函数,每个TA实例的生命周期中只被调用一次 */
TEE_Result TA_CreateEntryPoint(void)
{
    tlogi("---- TA_CreateEntryPoint ----------- \n");

    return TEE_SUCCESS;
}

/* 在CA请求创建一个与TA的会话时,TEE系统会调用此函数 */
TEE_Result TA_OpenSessionEntryPoint(uint32_t parmType, TEE_Param params[4], void **sessionContext)
{
    (void)parmType;
    (void)sessionContext;
    tlogi("---- TA_OpenSessionEntryPoint -------- \n");
    if (params[0].value.b == 0xFFFFFFFE)
        return TEE_ERROR_GENERIC;
    else
        return TEE_SUCCESS;
}

/* 在CA发送指令给TA时,需要指定之前创建成功的会话,TEE系统收到请求后会调用此函数 */
TEE_Result TA_InvokeCommandEntryPoint(void *sessionContext, uint32_t cmd, uint32_t parmType, TEE_Param params[4])
{
    TEE_Result ret = TEE_SUCCESS;
    (void)sessionContext;
    (void)parmType;
    (void)params;
    tlogi("---- TA invoke command ----------- command id: %u\n", cmd);

    switch (cmd) {
        case DRVTEST_COMMAND_DRVVIRTTOPHYS:
        case DRVTEST_COMMAND_COPYFROMCLIENT:
        case DRVTEST_COMMAND_COPYTOCLIENT:
            ret = TeeTestDrive(cmd);
            if (ret != TEE_SUCCESS)
                tloge("invoke command for driver test failed! cmdId: %u, ret: 0x%x\n", cmd, ret);
            break;
        default:
            tloge("not support this invoke command! cmdId: %u\n", cmd);
            ret = TEE_ERROR_GENERIC;
            break;
    }

    return ret;
}

/* 在CA发起关闭与TA的会话连接时,TEE系统会调用此函数 */
void TA_CloseSessionEntryPoint(void *sessionContext)
{
    (void)sessionContext;
    tlogi("---- TA_CloseSessionEntryPoint ----- \n");
}

/* TA实例的析构函数,TEE系统在销毁TA实例时调用此函数 */
void TA_DestroyEntryPoint(void)
{
    tlogi("---- TA_DestroyEntryPoint ---- \n");
}

标准C库支持

支持绝大多数的libc接口。支持大多数的POSIX接口,具体支持情况请看附件。标准文档请参照如下网址:

POSIX:https://mirror.math.princeton.edu/pub/oldlinux/download/c953.pdf

目前使用的openharmony的musl-1.2.5/libc库。

表 9 标准C支持列表

模块

函数接口名

说明

malloc

aligned_alloc

-

calloc

-

malloc

-

realloc

-

free

-

posix_memalign

-

mman

mmap

-

munmap

-

time

gettimeofday

-

strftime

-

time

-

stdio

printf

目前不支持文件系统,文件操作只支持标准输入输出。

scanf

-

snprintf

-

sprintf

-

vsnprintf

-

vsprintf

-

errno

errno

-

strerror

-

exit

abort

-

unistd

getpid

-

locale

setlocale

-

strcoll

-

strxfrm

-

strtod

-

multibyte

mbrtowc

-

wcrtomb

-

wctob

-

prng

srandom

-

initstate

-

setstate

-

random

-

string

memchr

-

memcmp

-

memcpy

-

memmove

-

memset

-

strchr

-

strcmp

-

strcpy

-

strlen

-

strncmp

-

strncpy

-

strnlen

-

strrchr

-

strstr

-

wcschr

-

wcslen

-

wmemchr

-

ctype

isalpha

-

isascii

-

isdigit

-

islower

-

isprint

-

isspace

-

iswctype

-

iswdigit

-

iswlower

-

iswspace

-

iswupper

-

towupper

-

towlower

-

math

atan

-

ceil

-

ceilf

-

copysignl

-

exp

-

fabs

-

floor

-

frexp

-

frexpl

-

log

-

log2

-

pow

-

roundf

-

scalbn

-

scalbnl

-

sqrt

-

stdlib

abs

-

atof

-

atoi

-

atol

-

atoll

-

bsearch

-

div

-

ecvt

-

imaxabs

-

llabs

-

qsort

-

strtoul

-

strtol

-

wcstod

-

注意:

  1. 不支持文件系统、控制台。
  2. 不支持fstatfsyncwritev接口。

安全函数库

概述

危险函数依赖于程序员对参数进行检查或保证空间能足够容纳所产生的结果,函数本身不对这些情况进行判断,即使有问题也不会给出错误的指示。C11标准中对于过时的不安全的危险函数定义了对应的安全函数(_s版本的函数),相比危险函数,安全函数对照C11标准进行了相应的安全增强,会对入参以及不同的错误情况进行判断,降低操作不当所引入的安全风险。下表列举了危险函数以及对应的安全函数,TA代码中涉及到相关危险函数的必须使用安全函数。

表 10 危险函数以及对应的安全函数

危险函数

安全函数

memcpy

memcpy_s

memmove

memmove_s

memset

memset_s

strcpy

strcpy_s

strncpy

strncpy_s

strcat

strcat_s

strncat

strncat_s

strtok

strtok_s

snprintf

snprintf_s

vsnprintf

vsnprintf_s

详细描述请参考头文件<securec.h>。

安全函数包含部分如下特性:

  • 强化边界检查:在接口参数中增加一个buffer长度的参数,在长度参数正确情况下不会出现溢出。
  • 保证结果字符串以’\0’结尾,避免访问buffer边界之外的信息。
  • 发现缓冲区溢出发生,将目的缓冲区的首字节置零。
  • 增加错误返回值,便于程序员快速进行错误定位。
  • 增强入参检查。
  • 增加入参的内存重叠检查(memcpy_sp宏中对于常量拷贝不进行内存重叠检查)。
  • 定义SECUREC_STRING_MAX_LEN和SECUREC_MEM_MAX_LEN宏,可以通过其限定字符串和内存操作时的最大长度。
  • 定义SECUREC_ERROR_INVALID_PARAMTER、SECUREC_ERROR_INVALID_RANGE和SECUREC_ERROR_BUFFER_OVERLAP宏,其会在参数出错时进行统一处理,用户可自定义发生错误时的函数处理行为。

内存复制

memcpy_s

errno_t memcpy_s(void* dest, size_t destMax, const void* src, size_t count);

复制源缓冲区的数据到目的缓冲区。

注意:

  • 与系统函数相比:1)多了一个参数:目的缓冲区总大小; 2)系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区清0,具体参考如上表1
  • 进行字符串拷贝时,不会自动在末尾添加结束符。(对字符串操作,建议使用字符串操作函数)
  • 示例
 #include "securec.h" 
 #include <string.h> 
  
 #define BUFFER_SIZE 11 
  
 int main() 
 { 
     char str1[BUFFER_SIZE] = "0123456789"; 
     char str2[BUFFER_SIZE] = {0x00}; 
     errno_t rc =  EOK; 
  
     rc = memcpy_s(str2,  BUFFER_SIZE, str1, BUFFER_SIZE - 1); 
     printf("rc = %d,  %s\n", rc, str2); 
  
     /* count is bigger than destMax, return ERANGE_AND_RESET  and dest is reset. */ 
     rc = memcpy_s(str2,  BUFFER_SIZE, str1, BUFFER_SIZE + 1); 
     printf("rc = %d, %s\n", rc,  str2); 
  
     /* initialize */ 
     rc = memcpy_s(str2,  BUFFER_SIZE, str1, BUFFER_SIZE - 1); 
     printf("rc = %d, %s\n", rc,  str2); 
  
     /* overlap, return EOVERLAP_AND_RESET and dest is reset.  */ 
     rc = memcpy_s(str2,  BUFFER_SIZE, str2 + 2, BUFFER_SIZE - 1); 
     printf("rc = %d, %s\n", rc,  str2); 
  
     return 0; 
 }
运行结果:
rc =  0, 0123456789
rc =  162
rc =  0, 0123456789
rc =  182

memmove_s

errno_t memmove_s(void* dest,size_t destMax, const void* src, size_t count);

移动源缓冲区的数据到目的缓冲区

注意:

  • 与系统函数相比:
  • 多了一个参数:目的缓冲区总大小;
  • 系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区清0,具体参考如上表2
  • 示例
 #include "securec.h" 
 #include <string.h> 
  
 #define BUFFER_SIZE 11 
  
 int main() 
 { 
     char str[BUFFER_SIZE] = "0123456789"; 
     errno_t rc =  EOK; 
  
     printf("Before:  %s\n", str); 
  
     /* Move six bytes from the start of the  string 
     * to a new position shifted  by one byte. To protect against 
     * buffer overrun, the  secure version of memmove requires the 
     * the length of the  destination string to be specified. 
     */ 
  
     rc = memmove_s(str,  BUFFER_SIZE, str + 1, 6); 
     printf("After: rc = %d, %s\n", rc,  str); 
  
     /* count is bigger than destMax, return ERANGE_AND_RESET  and dest is reset. */ 
     rc = memmove_s(str,  BUFFER_SIZE, str + 1, BUFFER_SIZE + 100); 
     printf("Later: rc = %d, %s\n",  rc, str); 
  
     return 0; 
 }
运行结果:
Before: 0123456789
After: rc = 0,  123456789
Later: rc =  162

内存初始化

memset_s

errno_t memset_s(void* dest, size_t destMax, int c, size_t count);

复制源缓冲区的数据到目的缓冲区

注意:

  • 与系统函数相比:1)多了一个参数:目的缓冲区总大小; 2)系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 示例
 #include "securec.h" 
 #include <string.h> 
  
 #define BUFFER_SIZE 40 
  
 int main() 
 { 
     char buffer[BUFFER_SIZE] =  "This is a test of the memset  function"; 
     errno_t rc =  EOK; 
  
     printf( "Before: %s\n", buffer ); 
  
     rc = memset_s(buffer,  BUFFER_SIZE, '*', 20 ); 
     printf( "After: rc = %d, %s\n", rc, buffer  ); 
  
     /* count is bigger than destMax, return ERANGE_AND_RESET  and destMax size is set. */ 
     rc = memset_s(buffer,  BUFFER_SIZE, '*', BUFFER_SIZE + 1 ); 
     printf( "Later: rc = %d, %s\n", rc, buffer  ); 
  
     return  0; 
 }
 
 运行结果:
 Before: This is a test of the memset function 
 After: rc = 0, ********************e memset  function 
 Later: rc = 162,  ****************************************

字符串复制

strcpy_s

errno_t strcpy_s(char* strDest, size_t destMax, const char* strSrc);

复制源字符串到目的缓冲区

注意:

  • 与系统函数相比:1)、多了一个参数:目的缓冲区总大小; 2)、系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区首字符置0,具体参考如上表1。 源字符串必须含有结束符。
  • 示例
 #include "securec.h" 
 #include <stdio.h> 
  
 #define SMALL_BUF_SIZE 10 
 #define BIG_BUF_SIZE 100 
  
 int main() 
 { 
     char str1[BIG_BUF_SIZE] = {0x00}; 
     char str2[SMALL_BUF_SIZE] = {0x00}; 
     char *str3 = str1 +  4; 
     errno_t rc =  EOK; 
  
     rc = strcpy_s(str1,  BIG_BUF_SIZE, "Security Design  Department"); 
     printf("rc = %d,  %s\n", rc, str1); 
  
     /* strSrc length + 1 is bigger than destMax, return  ERANGE_AND_RESET and strDest is reset. */ 
     rc = strcpy_s(str2,  SMALL_BUF_SIZE, "Security Design"); 
     printf("rc = %d,  %s\n", rc, str2); 
  
     memset_s(str1,  BIG_BUF_SIZE, 0x41, BIG_BUF_SIZE); 
     /* overlap, return EOVERLAP_AND_RESET and strDest is reset.  */ 
     rc = strcpy_s(str1,  BIG_BUF_SIZE, str3); 
     printf("rc = %d,  %s\n", rc, str1); 
  
     return 0; 
 }

运行结果:
rc  = 0, Security Design Department
rc  = 162,
rc  = 182,

strncpy_s

errno_t strncpy_s(char* strDest, size_t destMax, const char* strSrc, size_t count);

复制指定长度的源字符串到目的缓冲区。

注意:

  • 与系统函数相比:1)多了一个参数:目的缓冲区总大小; 2)系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区首字符置0,具体参考如上表2
  • 源字符串必须含有结束符。
  • 调用该函数时建议传入的参数destMax 大于 count以保证有截断功能。
  • 安全函数strncpy_s执行完后保证strDest有’\0’结尾,而危险函数strncpy不保证。
  • 当 count 大于 strlen(strSrc) 时, strncpy函数会复制完字符串后在strDest中填充count-strlen(strSrc)个’\0’字符,而strncpy_s函数不做填充。
  • 示例
 #define SMALL_BUF_SIZE 10
 #define BIG_BUF_SIZE 100 
  
 int main() 
 { 
     char str1[BIG_BUF_SIZE] =  {0x00}; 
     char str2[BIG_BUF_SIZE] =  "security design  department"; 
     char *str3 = str1 +  4; 
     char str4[SMALL_BUF_SIZE] =  {0x00}; 
     errno_t rc =  EOK; 
  
     rc = strncpy_s(str1,  BIG_BUF_SIZE, str2, 15); 
     printf("rc = %d, %s\n", rc, str1); 
  
     /* count is bigger than destMax, return ERANGE_AND_RESET  and dest is reset. */ 
     rc = strncpy_s(str4,  SMALL_BUF_SIZE, "security Design Department", 15); 
     printf("rc = %d, %s\n", rc, str4); 
  
     memset_s(str1,  BIG_BUF_SIZE, 0x41, BIG_BUF_SIZE); 
     /* overlap, return EOVERLAP_AND_RESET and dest is reset.  */ 
     rc = strncpy_s(str1,  BIG_BUF_SIZE, str3, 15); 
     printf("rc = %d, %s\n", rc, str1); 
  
     return 0; 
 }

运行结果:
rc =  0, security design
rc =  162,
rc =  182,

字符串连接

strcat_s

errno_t strcat_s(char* strDest, size_t destMax, const char* strSrc);

将源字符串连接到目的字符串后面。

注意:

  • 与系统函数相比:
  • 多了一个参数:目的缓冲区总大小;
  • 系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区的首字符置0,具体参考如上表1
  • 示例
 #include "securec.h" 
 #include <stdio.h> 
  
 #define BUFFER_SIZE  64 
  
 int main() 
 { 
     char   str1[BUFFER_SIZE] = {’’}; 
     char   str2[BUFFER_SIZE] = {b}; 
     errno_t rc = EOK; 
  
     rc = strcat_s(str1,  3, str2); 
     printf("rc = %d, %s\n", rc, str1); 
  
  
     /*strdest in destMax dont have \0*/ 
     memset_s(str1, BUFFER_SIZE,'a',4); 
     memset_s(str2, BUFFER_SIZE,'b',4); 
     rc = strcat_s(str1, 4,  str2); 
     printf("rc = %d, %s\n", rc, str1); 
  
     /*destmax isnt enough for strSrc*/ 
     memset_s(str1, BUFFER_SIZE,'a',4); 
     memset_s(str2, BUFFER_SIZE,'b',4); 
     rc = strcat_s(str1,8, str2); 
     printf("rc = %d, %s\n", rc, str1); 
  
     /*dest , src overlap*/ 
     rc = strncat_s(str2,8, str2+2,1); 
     printf("rc = %d, %s\n", rc, str2); 
  
     return 0; 
 }

运行结果:
rc = 0, ab
rc = 150,
rc = 162,
rc = 182,

strncat_s

errno_t strncat_s(char* strDest, size_t destMax, const char* strSrc, size_t count);

将指定长度的源字符串连接到目的字符串后面。

注意:

  • 与系统函数相比:
  • 多了一个参数:目的缓冲区总大小;
  • 系统函数返回值为指针,安全函数返回值为整型。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区的首字符置0,具体参考如上表2
  • 示例
 #include "securec.h" 
 #include <stdio.h> 
  
 #define BUFFER_SIZE  64 
  
 int main() 
 { 
     char   str1[BUFFER_SIZE] = {’’}; 
     char   str2[BUFFER_SIZE] = {b}; 
     errno_t rc = EOK; 
  
     rc = strncat_s(str1, 3, str2,  1); 
     printf("rc = %d, %s\n", rc , str1); 
  
     /*strdest in destMax dont have  \0*/ 
     memset_s(str1, BUFFER_SIZE,a,4); 
     memset_s(str2, BUFFER_SIZE,b,4); 
     rc = strncat_s(str1, 4, str2,  2); 
     printf("rc  = %d, %s\n", rc ,  str1); 
  
     /*destmax isnt enough for  strSrc*/ 
     memset_s(str1, BUFFER_SIZE,a,4); 
     memset_s(str2, BUFFER_SIZE,b,4); 
     rc = strncat_s(str1,8, str2,5); 
     printf("rc = %d, %s\n", rc , str1); 
  
     /*dest , src overlap*/ 
     rc = strncat_s(str2,8, str2+2,1); 
     printf("rc = %d, %s\n", rc , str2); 
  
     return 0; 
 }

运行结果:
rc  = 0, ab
rc  = 150,
rc  = 162,
rc  = 182,

字符串分割

strtok_s

errno_t strtok_s(char* strToken, const char* strDelimit, char** context);

将字符串按照指定的分隔符分割成子字符串。

注意:

  • 与系统函数相比:多了一个参数context:保存调用strtok_s后的下个子字符串的首地址,当不再有子字符串时,context指向被分割字符串结尾。
  • 当在被分割字符串中没有找到分隔符时,如果被分割字符串长度大于0,会返回被分割字符串首地址,否则返回NULL。
  • 以逗号分隔符为例,顺序调用strtok_s(str,&p), strtok_s(NULL,&p), strtok_s(NULL,&p)函数的返回值与不同分割字符串的关系如下:

表 11 返回值与字符串关系

被分割字符串

返回值

strtok_s(str,&p)

strtok_s(NULL,&p)

strtok_s(NULL,&p)

str = "aaa"

"aaa"

NULL

_

str = ""

NULL

_

_

str = ","

NULL

_

_

str = ",,"

NULL

_

_

str = ",aaa"

"aaa"

NULL

_

str = ",,aaa"

"aaa"

NULL

_

str = "aaa,"

"aaa"

NULL

_

str = "aaa,,"

"aaa"

NULL

_

str = "aaa,bbb"

"aaa"

"bbb"

NULL

str = "aaa,,bbb"

"aaa"

"bbb"

NULL

str = "aaa,bbb,"

"aaa"

"bbb"

NULL

str = "aaa,bbb,,"

"aaa"

"bbb"

NULL

  • 示例
 #include "securec.h" 
 #include <stdio.h> 
  
 int main() 
 { 
     char string1[64]  = "A string\tof ,,tokens\nand some  more tokens"; 
     char string2[64]  = "Another string\n\tparsed at the same  time."; 
     char seps[] =  " ,\t\n"; 
     char *token1 =  NULL; 
     char *token2 =  NULL; 
     char *next_token1  = NULL; 
     char *next_token2= NULL; 
  
     printf( "Tokens:\n" ); 
  
     /* Establish string and get the first token:  */ 
     token1 = strtok_s(string1, seps,  &next_token1); 
     token2 = strtok_s(string2, seps,  &next_token2); 
  
     /* While there are tokens in "string1" or "string2"  */ 
     while ((token1 !=  NULL) || (token2 != NULL)) 
     { 
         /* Get next token: */ 
         if (token1 !=  NULL) 
         { 
             printf("  %s\n", token1); 
             token1 = strtok_s(NULL,  seps, &next_token1); 
         } 
         if (token2 !=  NULL) 
         { 
             printf("        %s\n", token2); 
             token2 = strtok_s (NULL, seps,  &next_token2); 
         } 
     } 
  
     return 0; 
 }

运行结果:
Tokens:
more
same
some
the
and
at
tokens
parsed
of
string
string
Another
time.
tokens

格式化输出

snprintf_s

int snprintf_s(char* strDest, size_t destMax, size_t count, const char* format, ...);

将数据按照指定长度格式化输出到目的缓冲区。

注意:

  • 与系统函数相比:
  • 多了一个参数:目的缓冲区总大小;
  • count参数与系统函数的用法存在差异,参见参数表。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区首字符清0,具体参考如上表1
  • 调用该函数时建议传入的参数destMax 大于 count以保证有截断功能。
  • 确保输入的源数据与目标缓冲区不存在重叠。
  • 输入源数据中若是字符串则必须含有结束符。 输入源数据的类型、个数必须与格式化控制字符串(format)中的类型、个数保持一致。
  • 示例
 #include "securec.h" 
  
 void  snprintf_sample() 
 { 
     char buffer[200]={0}; 
     char     *string = "computer"; 
     wchar_t *wstring =  L"Unicode"; 
  
     /* format strings. */ 
     int iRet = snprintf_s( buffer, 200, 20,  "%s,%ls", string, wstring); 
     printf("iRet = %d, buffer = %s.\n", iRet,  buffer); 
  
     /* test edge. */ 
     printf("------------------\n"); 
     iRet = snprintf_s( buffer, 3, 3, "%s",  "123"); 
     printf("iRet =  %d, buffer = %s.\n", iRet, buffer); 
  
     /* test edge. */ 
     printf("------------------\n"); 
     iRet = snprintf_s( buffer, 3, 2, "%s",  "123"); 
     printf("iRet = %d, buffer = %s.\n", iRet,  buffer); 
  
  
     return; 
 } 
  
 int  main() 
 { 
     snprintf_sample (); 
     return 0; 
 }

运行结果:
iRet = 16, buffer  = computer,Unicode.
iRet = -1, buffer  = .

iRet = -1, buffer  = 12.

vsnprintf_s

int vsnprintf_s(char* strDest, size_t destMax, size_t count, const char* format, va_list arglis);

将参数列表(va_list)数据按照指定长度格式化输出到目的缓冲区。

注意:

  • 与系统函数相比:
  • 多了一个参数:目的缓冲区总大小;
  • count参数与系统函数的用法存在差异,参见参数表。
  • 调用函数时,注意判断返回值是否成功,否则有可能操作结果和预期不一致。
  • 某些出错情况下,会对目的缓冲区首字符清0,具体参考如上表2。
  • 调用该函数时建议传入的参数destMax 大于 count以保证有截断功能。
  • 确保输入的源数据与目标缓冲区不存在重叠。
  • 输入源数据中若是字符串则必须含有结束符。
  • 输入源数据的类型、个数必须与格式化控制字符串(format)中的类型、个数保持一致。
  • 示例1
 #include "securec.h" 
  
 void FormatOutput(char* formatstring, ...) 
 { 
     va_list args; 
     int nSize =  0; 
     char buff[10]; 
     memset_s(buff,sizeof(buff), 0, sizeof(buff)); 
     va_start(args,  formatstring); 
     nSize = vsnprintf_s( buff, sizeof(buff), 8,  formatstring, args); 
     printf("nSize: %d,  buff: %s\n", nSize, buff); 
 } 
  
 int main() 
 { 
     FormatOutput("%s  %s", "Hi", "there"); 
     FormatOutput("%s  %s", "Hi", "there!"); 
  
     return 0; 
 }

运行结果:
nSize: 8, buff: Hi there
nSize: -1, buff: Hi there
  • 示例2
 void vsnprintf_base(char * format, ...)
 { 
     va_list  args; 
     char  buffer[100]; 
     int iRet =  -1; 
  
     va_start(args,  format); 
     iRet = vsnprintf_s(buffer,  100, 20, format, args); 
     printf("iRet = %d, buffer  = %s.\n", iRet, buffer); 
  
     return; 
 } 
 void vsnprintf_sample(void) 
 { 
     char    *string = "computer"; 
     wchar_t *wstring =  L"Unicode"; 
  
     /* format strings.  */ 
     vsnprintf_base("%s,%ls",  string, wstring); 
  
     /* test  edge.*/ 
     printf("------------------\n"); 
     vsnprintf_base("%s",  "12345678901234567890a"); 
  
     return; 
 } 
  
 int main() 
 { 
     vsnprintf_sample(); 
     return  0; 
 }

运行结果:
iRet = 16, buffer = computer,Unicode.
iRet = -1, buffer = 12345678901234567890.