探索Zephyr操作系统的代码质量

PVS-Studio和Zephyr

我们最近说过,PVS-Studio代码分析器开始与PlatformIO集成。当然,PVS-Studio开发团队与PlatformIO团队进行了交流,出于兴趣的考虑,他们建议检查Zephyr实时操作系统的代码。我们想,为什么不呢?这是一篇有关这种研究的文章。

PlatformIO


在开始本文的主要部分之前,我想向嵌入式系统开发人员推荐PlatformIO项目,这可以使他们的生活更轻松。它是一个跨平台的微控制器编程工具。PlatformIO的核心是命令行工具,但建议将其用作Visual Studio Code的插件。支持大量现代芯片和基于它们的主板。它能够自动下载合适的装配系统,并且该站点具有大量用于管理插入式电子组件的库。

PVS工作室


PVS-Studio在嵌入式系统领域仍然鲜为人知,因此,以防万一,我将为尚未熟悉此工具的新读者做一个介绍。我们的普通读者可以直接跳到下一部分。

PVS-Studio是静态代码分析器,它可以检测用C,C ++,C#和Java编写的程序的代码中的错误和潜在漏洞。如果仅谈论C和C ++,则支持以下编译器:

  • 视窗 Visual Studio 2010-2019年C,C ++,C ++ / CLI,C ++ / CX(WinRT)
  • 视窗 IAR嵌入式工作台,用于ARM C,C ++的C / C ++编译器
  • 视窗 QNX Momentics,QCC C,C ++
  • Windows / Linux Keil µVision,DS-MDK,ARM编译器5/6 C,C ++
  • Windows / Linux 德州仪器Code Composer Studio,ARM代码生成工具C,C ++
  • Windows/Linux/macOS. GNU Arm Embedded Toolchain, Arm Embedded GCC compiler, C, C++
  • Windows/Linux/macOS. Clang C, C++
  • Linux/macOS. GCC C, C++
  • Windows. MinGW C, C++

该分析仪具有自己的警告分类系统,但是如果需要,您可以根据编码标准CWESEI CERTMISRA启用警报显示

您甚至可以在大型旧项目中快速开始定期使用PVS-Studio。为此提供了一种特殊的大规模抑制警告机制。当前所有的警告都被视为技术债,并且被隐藏了,这使您可以专注于仅适用于新代码或修改代码的警告。这使团队可以立即在工作中每天开始使用分析仪,并且您可以不时回到技术职责上来改进代码。

使用PVS-Studio还有许多其他方案。例如,您可以将其用作SonarQube的插件。可以与Travis CI,CircleCI,GitLab CI / CD等系统集成。对PVS-Studio的更详细描述超出了本文的范围。因此,我建议您熟悉这篇文章,该文章包含许多有用的链接,并且回答了许多问题:“ 将PVS-Studio静态代码分析器引入开发过程的原因。”

和风


将PVS-Studio集成到PlatformIO中的过程中,我们的团队进行了交谈,并被要求从嵌入式世界(即Zephyr)中检出一个项目。我们喜欢这个主意,这就是撰写本文的原因。

Zephyr是一种轻量级的实时操作系统,旨在在各种架构的资源有限的设备上运行。该代码根据开源Apache 2.0许可证分发。在以下平台上工作:ARM(Cortex-M0,Cortex-M3,Cortex-M4,Cortex-M23,Cortex-M33,Cortex-R4,Cortex-R5,Cortex-A53),x86,x86-64,ARC,RISC- V,Nios II,Xtensa。

一些功能:

  • 统一地址空间。特定的应用程序代码与自定义内核结合在一起,会创建在设备上执行的整体映像。
  • . , .
  • . .
  • . . .
  • : , , , , .

在我们感兴趣的时刻,Synopsys参与了操作系统的开发2014年,Synopsys收购了Coverity,该公司生产了同名的静态代码分析器。

很自然,Zephyr开发人员从一开始就使用Coverity分析器分析器是市场的领导者,它不仅会更好地影响操作系统代码的质量。

Zephyr代码质量


我认为Zephyr操作系统代码就是质量。这就是给我理由的原因:

  • PVS-Studio 122 High 367 Medium. , , 560 C/C++ . . 7810 C/C++ 10075 . , . , , .
  • . «» , .
  • 分析了源代码SourceMonitor实用程序统计出该代码中有48%是注释。这很多,以我的经验,这表明对代码的质量及其对其他开发人员的可理解性表示高度关注。
  • 开发项目时,使用Coverity静态代码分析器。由于这个事实,最有可能的是,PVS-Studio分析仪尽管发现了项目中的错误,却无法生动地展示自己,因为在分析其他项目时有时会发生这种情况。

基于此,我相信该项目的作者关心代码的质量和可靠性。现在让我们看一下PVS-Studio分析仪(版本7.06)发出的一些警告。

半错误警告


由于项目代码的级别较低,因此它是专门编写的,并带有许多条件编译(#ifdef)。这会产生大量警告,这些警告并不表示真正的错误,但不能仅仅称为错误。用几个例子来澄清这一点将是最容易的。

“半错误”响应N1的示例

static struct char_framebuffer char_fb;

int cfb_framebuffer_invert(struct device *dev)
{
  struct char_framebuffer *fb = &char_fb;

  if (!fb || !fb->buf) {
    return -1;
  }

  fb->inverted = !fb->inverted;

  return 0;
}

PVS-Studio警告:V560条件表达式的一部分始终为false :!Fb。 cfb.c 188

当获取静态变量的地址时,总是获得非零指针。因此,指针fb总是非零,并且其验证没有意义。

但是,很明显,这根本不是错误,而只是过度检查,不会造成任何危害。此外,在构建发行版时,编译器会将其丢弃,因此甚至不会导致速度下降。

以我的理解,类似的情况属于“半错误”分析仪操作的概念。从形式上讲,分析仪绝对正确。并且最好从代码中删除多余的毫无意义的检查。但是,所有这些都是小问题,在本文的框架中考虑这些警告甚至都没有意思。

N2“半切割”致动的示例

int hex2char(u8_t x, char *c)
{
  if (x <= 9) {
    *c = x + '0';
  } else if (x >= 10 && x <= 15) {
    *c = x - 10 + 'a';
  } else {
    return -EINVAL;
  }
  return 0;
}

警告PVS-Studio:V560条件表达式的一部分始终为true:x> =10。hex.c 31

分析仪在断言部分条件始终为true时再次形式正确。如果变量x不小于/等于9,那么事实证明它始终大于/等于10。代码可以简化:

} else if (x <= 15) {

再一次,很明显,这里没有真正有害的错误,并且进行额外的比较只是为了使代码美观。

现在,让我们来看一个更复杂的N3示例:

首先,看看如何实现CHECKIF

#if defined(CONFIG_ASSERT_ON_ERRORS)
#define CHECKIF(expr) \
  __ASSERT_NO_MSG(!(expr));   \
  if (0)
#elif defined(CONFIG_NO_RUNTIME_CHECKS)
#define CHECKIF(...) \
  if (0)
#else
#define CHECKIF(expr) \
  if (expr)
#endif

根据项目的编译模式,可以执行或跳过检查。在我们的例子中,当使用PVS-Studio检查代码时,选择了以下宏实现:

#define CHECKIF(expr) \
  if (expr)

现在,让我们看看这将导致什么。

int k_queue_append_list(struct k_queue *queue, void *head, void *tail)
{
  CHECKIF(head == NULL || tail == NULL) {
    return -EINVAL;
  }

  k_spinlock_key_t key = k_spin_lock(&queue->lock);
  struct k_thread *thread = NULL;
  if (head != NULL) {
    thread = z_unpend_first_thread(&queue->wait_q);
  }
  ....
}

PVS-Studio 警告V547 [CWE-571]表达式'head!= NULL'始终为true。queue.c 244

分析器认为检查(head!= NULL)始终为真。确实是这样。如果指针为NULL,则该函数将由于在函数开头的检查而停止工作:

CHECKIF(head == NULL || tail == NULL) {
  return -EINVAL;
}

回想一下,此处宏扩展如下:

if (head == NULL || tail == NULL) {
  return -EINVAL;
}

因此,PVS-Studio分析仪从其角度来看是正确的,并发出正确的警告。但是,此检查无法删除。她是需要的。在另一种情况下,宏将像这样打开:

if (0) {
  return -EINVAL;
}

然后需要重新检查指针。当然,在此版本的代码编译中,分析器不会生成警告。但是,它会警告编译的调试版本。

我希望读者现在可以清楚地知道“半错误”警告的出处。但是,它们没有错。PVS-Studio分析仪提供了多种抑制误报的机制,可以在文档中找到这些机制。

案例警告


但是,您是否发现了一些有趣的东西?这是成功的,现在我们将研究各种错误。同时,我要立即注意两点:

  1. . : , , , Coverity. , PVS-Studio - , .
  2. . , «» . , . GitHub, PVS-Studio.

N1,

static void gen_prov_ack(struct prov_rx *rx, struct net_buf_simple *buf)
{
  ....
  if (link.tx.cb && link.tx.cb) {
    link.tx.cb(0, link.tx.cb_data);
  }
  ....
}

PVS-Studio警告:V501 [CWE-571]'&&'运算符的左侧和右侧有相同的子表达式:link.tx.cb && link.tx.cb pb_adv.c 377

选中了一项相同的变量link.tx.cb显然,这是一个错字,要检查的第二个变量应该是link.tx.cb_data

片段N2,缓冲区溢出

考虑功能net_hostname_get,稍后将使用它。

#if defined(CONFIG_NET_HOSTNAME_ENABLE)
const char *net_hostname_get(void);
#else
static inline const char *net_hostname_get(void)
{
  return "zephyr";
}
#endif

就我而言,在预处理时,选择了与#else分支相关的选项也就是说,在预处理文件中,该函数的实现方式如下:
static inline const char *net_hostname_get(void)
{
  return "zephyr";
}

该函数返回一个指向7个字节的数组的指针(我们考虑到行尾的终端零)。

现在考虑导致数组边界退出的代码。

static int do_net_init(void)
{
  ....
  (void)memcpy(hostname, net_hostname_get(), MAX_HOSTNAME_LEN);
  ....
}

PVS-Studio警告:V512 [CWE-119]调用'memcpy'函数将导致'net_hostname_get()'缓冲区超出范围。log_backend_net.c 114

预处理后,MAX_HOSTNAME_LEN扩展如下:

(void)memcpy(hostname, net_hostname_get(),
    sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"));

因此,在复制数据时,会出现越界字符串文字。很难预测这将如何影响程序的执行,因为这会导致不确定的行为。

片段N3,潜在的缓冲区溢出

int do_write_op_json(struct lwm2m_message *msg)
{
  u8_t value[TOKEN_BUF_LEN];
  u8_t base_name[MAX_RESOURCE_LEN];
  u8_t full_name[MAX_RESOURCE_LEN];
  ....
  /* combine base_name + name */
  snprintf(full_name, TOKEN_BUF_LEN, "%s%s", base_name, value);
  ....
}

PVS-Studio警告:V512 [CWE-119]调用'snprintf'函数将导致缓冲区'full_name'溢出。 lwm2m_rw_json.c 826

如果我们替换宏值,则正在发生的情况如下图所示:

u8_t value[64];
u8_t base_name[20];
u8_t full_name[20];
....
snprintf(full_name, 64, "%s%s", base_name, value);

在形成字符串full_name缓冲区下仅分配20个字节。在这种情况下,构成字符串的部分将存储在大小为20和64字节的缓冲区中。而且,传递给snprintf函数并设计为防止数组出国的常数64 显然太大了!

此代码不一定会导致缓冲区溢出。也许总是幸运的,子字符串总是很小。但是,通常来说,此代码没有任何形式的溢出保护,并且包含经典的安全漏洞CWE-119

片段N4,表达式始终为真

static int keys_set(const char *name, size_t len_rd, settings_read_cb read_cb,
                    void *cb_arg)
{
  ....
  size_t len;
  ....
  len = read_cb(cb_arg, val, sizeof(val));
  if (len < 0) {
    BT_ERR("Failed to read value (err %zu)", len);
    return -EINVAL;
  }
  ....
}

PVS-Studio 警告V547 [CWE-570]表达式'len <0'始终为false。无符号类型的值永远不会小于0 keys.c 312 len

变量具有无符号类型,因此不能小于0。因此,不会以任何方式处理错误状态。在其他地方,类型intssize_t用于存储read_cb函数的结果 例:


static inline int mesh_x_set(....)
{
 ssize_t len;
 len = read_cb(cb_arg, out, read_len);
 if (len < 0) {
 ....
}

注意。read_cb函数似乎一切都不好事实是它是这样声明的:

static u8_t read_cb(const struct bt_gatt_attr *attr, void *user_data)

类型u8_t无符号字符。

该函数始终仅返回unsigned char类型的正数如果将此值放在类型为intssize_t的带符号变量中,则所有这些相同,则该值将始终为正。因此,在其他地方,检查错误状态也不起作用。但是我没有深入研究这个问题。

N5片段,很奇怪

static char *mntpt_prepare(char *mntpt)
{
  char *cpy_mntpt;

  cpy_mntpt = k_malloc(strlen(mntpt) + 1);
  if (cpy_mntpt) {
    ((u8_t *)mntpt)[strlen(mntpt)] = '\0';
    memcpy(cpy_mntpt, mntpt, strlen(mntpt));
  }
  return cpy_mntpt;
}

PVS-Studio警告:V575 [CWE-628]'memcpy'函数不会复制整个字符串。使用'strcpy / strcpy_s'函数保留终端null。shell.c 427

奇怪的代码

有人试图模拟strdup函数,但是他没有成功。

让我们从分析仪警告开始。它报告memcpy函数复制该行,但不复制终端零,这非常可疑。

似乎该终端0复制到这里:

((u8_t *)mntpt)[strlen(mntpt)] = '\0';

但不是!这是一个错字,因此终端零将被复制到自身!请注意,写入mntpt数组,而不是cpy_mntpt如此一来,mntpt_prepare函数将返回一个不包含零的字符串。

实际上,程序员想这样写:

((u8_t *)cpy_mntpt)[strlen(mntpt)] = '\0';

但是,仍然不清楚为什么它是如此复杂!该代码可以简化为以下选项:

static char *mntpt_prepare(char *mntpt)
{
  char *cpy_mntpt;

  cpy_mntpt = k_malloc(strlen(mntpt) + 1);
  if (cpy_mntpt) {
    strcpy(cpy_mntpt, mntpt);
  }
  return cpy_mntpt;
}

片段N6,在验证之前取消对指针的引用

int bt_mesh_model_publish(struct bt_mesh_model *model)
{
  ....
  struct bt_mesh_model_pub *pub = model->pub;
  ....
  struct bt_mesh_msg_ctx ctx = {
    .send_rel = pub->send_rel,
  };
  ....
  if (!pub) {
    return -ENOTSUP;
  }
  ....
}

PVS-Studio警告:V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'pub'指针。检查行:708,719。access.c 708

一个非常常见的错误模式。首先,将指针取消引用以初始化结构的成员:

.send_rel = pub->send_rel,

然后只有检查该指针可以为空。

片段N7-N9,在验证之前取消对指针的引用

int net_tcp_accept(struct net_context *context, net_tcp_accept_cb_t cb,
                   void *user_data)
{
  ....
  struct tcp *conn = context->tcp;
  ....
  conn->accept_cb = cb;

  if (!conn || conn->state != TCP_LISTEN) {
    return -EINVAL;
  }
  ....
}

PVS-Studio警告:V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'conn'指针。检查行:1071,1073。tcp2.c 1071

与前面的情况相同。这里不需要解释。

在这里可以看到另外两个这样的错误:

  • V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'context-> tcp'指针。检查行:1512、1518。tcp.c 1512
  • V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'fsm'指针。检查行:365、382。fsm.c 365

N10片段,错误检查

static int x509_get_subject_alt_name( unsigned char **p,
                                      const unsigned char *end,
                                      mbedtls_x509_sequence *subject_alt_name)
{
  ....
    while( *p < end )
    {
        if( ( end - *p ) < 1 )
            return( MBEDTLS_ERR_X509_INVALID_EXTENSIONS +
                    MBEDTLS_ERR_ASN1_OUT_OF_DATA );
    ....
  }
  ....
}

PVS-Studio 警告V547 [CWE-570]表达式'(end- * p)<1'始终为假。x509_crt.c 635

近距离查看条件:

  • * p <结束
  • (结束-* p)<1

他们彼此矛盾。

如果(* p <end),则(end-* p)的值将始终为1或更大。通常,这里有些问题,但是我不知道如何正确拼写。

片段N11,无法访问的代码

uint32_t lv_disp_get_inactive_time(const lv_disp_t * disp)
{
    if(!disp) disp = lv_disp_get_default();
    if(!disp) {
        LV_LOG_WARN("lv_disp_get_inactive_time: no display registered");
        return 0;
    }

    if(disp) return lv_tick_elaps(disp->last_activity_time);

    lv_disp_t * d;
    uint32_t t = UINT32_MAX;
    d          = lv_disp_get_next(NULL);
    while(d) {
        t = LV_MATH_MIN(t, lv_tick_elaps(d->last_activity_time));
        d = lv_disp_get_next(d);
    }

    return t;
}

PVS-Studio警告:V547 [CWE-571]表达式“ disp”始终为true。 lv_disp.c 148

如果disp是空指针,则函数终止。然后,相反,将检查disp指针是否不为null(并且总是如此),然后该函数再次完成其工作。

结果,函数中的部分代码将永远无法获得控制权。

片段N12,返回值奇怪

static size_t put_end_tlv(struct lwm2m_output_context *out, u16_t mark_pos,
        u8_t *writer_flags, u8_t writer_flag,
        int tlv_type, int tlv_id)
{
  struct tlv_out_formatter_data *fd;
  struct oma_tlv tlv;
  u32_t len = 0U;

  fd = engine_get_out_user_data(out);
  if (!fd) {
    return 0;
  }

  *writer_flags &= ~writer_flag;

  len = out->out_cpkt->offset - mark_pos;

  /* use stored location */
  fd->mark_pos = mark_pos;

  /* set instance length */
  tlv_setup(&tlv, tlv_type, tlv_id, len);
  len = oma_tlv_put(&tlv, out, NULL, true) - tlv.length;
  return 0;
}

PVS-Studio警告:V1001已分配'len'变量,但在功能结束时未使用。lwm2m_rw_oma_tlv.c 338

该函数包含两个都返回0的return语句。奇怪的是该函数始终返回0。还很奇怪的是,赋值后不再使用len变量我非常怀疑实际上应该这样写:

  len = oma_tlv_put(&tlv, out, NULL, true) - tlv.length;
  return len;
}

片段N13-N16,同步错误

static int nvs_startup(struct nvs_fs *fs)
{
  ....
  k_mutex_lock(&fs->nvs_lock, K_FOREVER);
  ....
  if (fs->ate_wra == fs->data_wra && last_ate.len) {
    return -ESPIPE;
  }
  ....
end:
  k_mutex_unlock(&fs->nvs_lock);
  return rc;
}

PVS-Studio警告:V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:620,549。nvs.c 620

某些情况下,某个功能在未解锁互斥锁的情况下完成了工作。据我了解,这样写是正确的:

static int nvs_startup(struct nvs_fs *fs)
{
  ....
  k_mutex_lock(&fs->nvs_lock, K_FOREVER);
  ....
  if (fs->ate_wra == fs->data_wra && last_ate.len) {
    rc = -ESPIPE;
    goto end;
  }
  ....
end:
  k_mutex_unlock(&fs->nvs_lock);
  return rc;
}

另外三个这样的错误:

  • V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:574,549。nvs.c 574
  • V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:908、890。net_context.c 908
  • V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:1194、1189。shell.c 1194

结论


我希望你喜欢它。请访问我们的博客,以了解有关其他项目和其他有趣出版物的检查。

在您的工作中使用静态分析器,即使在编写代码的阶段,也可以减少错误的数量和潜在的漏洞。特别是早期的错误检测与嵌入式系统有关,更新程序通常是一个耗时且昂贵的过程。

我还建议不要推迟并尝试使用PVS-Studio分析仪检查项目。请参阅文章:如何快速查看PVS-Studio分析器针对C和C ++代码生成的有趣警告?



如果您想与讲英语的读者分享这篇文章,请使用翻译链接:Andrey Karpov。检查Zephyr操作系统代码

All Articles