模糊测试:我们的应用程序测试中发生的最好的事情
#编程 #database #测试 #java

QUESTDB是一个时间序列数据库,可提供快速摄入速度,ILP和PGWIRE支持以及SQL查询语法。数据库被置于恒定且苛刻的工作量之下,并且建造一个人教授了许多艰难的课程。我们很高兴与您分享。

大约两年前,我们正在玩无休止的游戏游戏,其中包括Segfaults,数据损坏和各种并发错误。我们的用户正在报告它们,对于每份报告,我们必须重现该错误,分析并 - 最后 - 修复它。最终,我们决定退后一步,并提出更深入的解决方案。本文详细介绍了我们的痛苦以及我们从中摆脱困境的旅程。也许我们可以帮助您摆脱类似的绑定。

杀死了许多头部九头蛇

解决了一个错误,另外五个出现。我们发现了错误,我们的用户抓了错误。每个报告都会进行调查,在大多数情况下 - 解决方案。但是有时用户会应用解决方法并在不报告问题的情况下进行前进,并且某些错误将无法解决。这个循环导致我们的用户和QuestDB团队都感到沮丧。

我们将第一个模糊测试引入了QUESTDB项目,以使数据库更加强大,从那时起,我们添加了更多数据库。很难量化模糊性发现的错误,但是所有已知的关键问题都消失了,如今看到社区报告的关键问题是非常罕见的情况。

最重要的是,最近SQLancer团队在其测试工具中添加了QUESTDB支持,并帮助我们在SQL引擎中找到了许多问题。这就是为什么我们认为几乎所有复杂的应用程序都会从​​这种测试中获得很多收益,因此,如果您没有它们,今天我们希望激发您开始编写模糊测试。

什么是模糊?

“什么是绒毛?告诉我发生了什么。” - 著名的rock opera

的修改歌词

在分享我们的故事之前,让我们从基础知识开始并定义模糊测试。

Wikipedia says

...模糊或模糊测试是一种自动化软件测试技术,涉及将无效,意外或随机数据作为计算机程序的输入。然后,监视该程序的例外,例如崩溃,内置代码断言失败或潜在的内存泄漏。

因此,例如,如果您编写编译器,则可以使用模糊来生成源代码变化,包括无效的变化,并测试您的编译器是否能够为输入程序给出有意义的结果。如果您有Web服务,则可以编写一个模糊测试,该测试将尝试将无效或半valid请求发送到您的服务,然后分析是否有任何请求破坏您的服务安全性。甚至崩溃。

此外,如果您具有命令行实用程序,例如curl,则可以使用模糊测试来查找令人讨厌的东西like memory corruption bugs。但不仅如此。在数据库世界中,通常将模糊应用于接受查询语言的API,例如SQLInfluxDB Line Protocol(ILP)等专业协议。最后,更不用说模糊可用于在程序中找到一般漏洞。

那么我们如何写它们?

变得模糊

编写模糊测试有多种不同的方法:

  1. 您的fuzzer可能是基于生成的,并从头开始生成输入。否则它可能是基于突变的,并且具有生成最终输入的种子输入语料库。

  2. 测试可能是愚蠢的,如果它产生了非结构化的随机输入,例如随机字符串而不是适当的SQL语句。如果它知道预期的输入结构,它可能很聪明。

  3. 您可以选择使用白色,灰色或黑色盒子测试技术,具体取决于对计划以下的测试结构的认识。

如果要模糊测试数据库,则该测试可能与以下测试一样简单

public void testSqlEngine() {
    try (Connection conn = openConnection()) {
        int len = randomInt();
        String stmt = randomString(len);
        executeSql(stmt);
        assertDatabaseDidNotCrash();
    }
}

在这里,我们生成一个填充随机字符的字符串,通过数据库连接发送它,最后检查数据库进程是否仍在运行。这是(1)基于(2)愚蠢的(3)黑框测试,对SQL语法没有任何假设,仅遵循数据库网络协议。

有趣的是,它非常接近“ fuzz”一词在1988年测试Unix实用程序时所做的事情。当然,将这种方法应用于数据库软件并不是很有效。在QuestDB,我们希望模糊器是基于世代和智能的,但这是我们的偏爱。其他组合可能在其他软件项目中很有用。

涵盖了基础知识,让我们看一下模糊如何帮助我们使QuestDB变得更好...

我们的模糊故事

正如我们已经提到的,我们的第一个模糊测试是在两年前写的。它通过发送半随机,可能无效的消息来测试ILP协议。第一个测试立即揭示了许多关键问题,包括一些segfaults。

给您一个fuzzer的印象,让我们潜入我们的ILP协议implementation。使用ILP创建单行表就像将以下内容发送到<questdb_host>:9009
一样简单

weather,city=Sofia temperature=27.5 1692010877000000000\n

发送后,此消息告诉QuestDB创建一个具有以下结构的表:

CREATE TABLE 'weather' (
  city SYMBOL,
  temperature DOUBLE,
  timestamp TIMESTAMP
) timestamp (timestamp) PARTITION BY DAY WAL;

和以下行:

城市 温度 时间戳
索非亚 27.5 2023-08-14T11:01:17.000000Z

在这里,我们有一个名为weatherpartitioned表。表结构,列名和行值在我们的ILP消息中完全定义。现在,让我们通过TCP发送另一个消息:

weather,city=Berlin temperature=28,humidity=0.42 1692011659000000000\n

结果,我们在表中有一个新的humidity列。

现在的表内容是:

城市 温度 湿度 时间戳
索非亚 27.5 0 2023-08-14T11:01:17.000000Z
柏林 28 0.42 2023-08-14T11:14:19.000000Z

我们可以发送另一排吗?让我们这样做:

weather,country=France,CiTy=Paris HuMiDiTy=0.58,TeMpErAtUrE=26 1692012370000000000\n

在这里,我们更改了humiditytemperature列的订单,在较旧的列名称中添加了一个名为country的新列,并稍微改进了字母资本化。同样,数据库应添加新列,还应忽略列名中的资本化差异。话虽如此,该消息应产生以下行:

城市 温度 湿度 时间戳 乡村
索非亚 27.5 0 2023-08-14T11:01:17.000000Z
柏林 28 0.42 2023-08-14T11:14:19.000000Z
巴黎 26 0.58 2023-08-14T11:26:10.000000Z 法国

添加新列,重新排序列和更改的列名并不是我们在ILP协议上有可能做的唯一事情。

为您了解我们可以做什么,我们可以:

  • 在表和列名中包括非ASCII字符,以及列值
  • 跳过消息中的现有列和时间戳值
  • 重复某些列,以便在同一消息中多次重复列名及其值
  • 继续进行突变列表,导致有效和无效的ILP消息

接下来,我们可以决定是否使用基于RNG(随机数生成器)应用这些突变,并在多个连接上运行该方案。一旦发送和接收到所有消息,我们就可以将表的内容与预期的行进行比较。

上面的描述基本上是我们的第一个模糊测试所做的。虽然很简单,但正如我们所说,它围绕ILP代码揭示了一个“蠕虫罐”,即许多问题。自第一个模糊器以来,我们编写了62项其他测试,其中大多数旨在强调我们的存储,协议实施和并发代码。不用说,这个数字一直在增长。

虽然62个模糊声听起来不像是一个大数字,但每个测试都会产生大量不同的测试方案,这要归功于随机化。为了增加我们发现错误的机会,我们的CI定期运行模糊器。如果您想看看测试的外观,涵盖新的重复数据删除功能的take a look at the test

根据我们的经验,我们认为任何处理消息和事件的复杂软件都可以从模糊中受益。使用随机性产生消息和事件的组合以及最终结果的验证逻辑是一种非常强大的方法。而且,这不仅与数据库,编译器和CLI工具有关 - 您可以成功地将模糊器添加到几乎任何类型的应用程序中。

至于QuestDB团队,我们希望通过添加专用于我们的SQL引擎的测试来改善模糊器。幸运的是,在此期间,SQLancer团队已经进行了营救。

SQL模糊

SQLancer(合成的查询灯笼)是一种自动测试SQL数据库管理系统(DBMS)的工具,以在其实现中找到逻辑错误。

它分为两个阶段:

  1. 数据库生成。在此阶段,SQLancer创建了一个填充的数据库,并强调DBMS以增加导致不一致状态的概率。首先,它创建随机表。然后,选择随机SQL语句以生成,修改和删除数据。

  2. 测试。在这里,SQLancer根据生成的数据库检测逻辑错误。

取决于受支持的数据库,以上两个阶段都支持多种测试方法。例如,测试阶段支持所谓的非优化参考引擎构建方法(NOREC)方法。 Norec的目的是找到优化错误。它将数据库优化的查询转换为另一个查询,该查询的优化可能性要小得多。然后它运行两个查询并比较结果。

有趣的是,我们在测试我们的Simd JIT compiler时也做类似的事情。我们还拥有一个non-fuzz test,该non-fuzz test使用启用和禁用的JIT编译器运行查询,然后检查结果集是否相等。这种方法通常称为test oracle(或Just Oracle)。

最初的QuestDB支持是Suri Zhang向SQLancer贡献的,此后,我们从SQLancer团队那里获得了a number of GH issues。我们非常感谢SQLancer团队所报告的所有问题,并继续通过新版本进行修复。不用说:我们将继续使用SQLancer在SQL引擎中查找错误。我们也是working on a patch,以改善QuestDB支持。

我需要模糊吗?

我们相信答案是“是的,你愿意”。模糊对于任何复杂的软件都是有价值的。模糊测试不仅涉及数据库,编译器和CLI工具 - 您可以成功地将它们添加到几乎任何类型的应用程序中。这并不意味着您应该全力以赴进行这种测试,但别无其他,只要编写魔力,一旦您编写了“传统”测试,就可以帮助我们构建一个更强大的数据库,并且肯定会帮助您。

像往常一样,我们鼓励您尝试最新的QuestDB版本,并与我们的Slack Community共享您的反馈。您也可以使用我们的live demo播放,以查看其执行查询的速度。当然,对我们的open-source database on GitHub的贡献非常受欢迎。


Andrei Pechkurov撰写的文章。在Twitter上关注他。