PVS-Studio团队在2018-2019年会议上提供的Bug发现挑战解决方案

图片2


嗨! 尽管2019年会议季节尚未结束,但我们想谈一谈过去会议期间我们在展位上为访客提供的Bug查找挑战。 从2019年秋季开始,我们一直面临着一系列新的挑战,因此我们现在可以揭示针对2018年以前的任务和2019年上半年的解决方案-毕竟,其中许多来自以前发布的文章,并且我们有一个链接或QR码,其中包含有关打印在我们的挑战传单上的各个文章的信息。

如果您参加了我们通过展位参加的活动,您可能会看到甚至试图解决我们的一些挑战。 这些是使用C,C ++,C#或Java编写的真实开源项目的代码片段。 每个摘录都包含一个错误,并且要求来宾尝试找到它。 成功的解决方案(或只是参与错误的讨论)将获得奖励:螺旋绑定的桌面状态,钥匙串等:

图片4

也要吗 然后欢迎在即将举行的活动中到我们的展位前来。

顺便说一下,在文章“ 会议时间!总结2018 ”和“ 会议。2019年上半年小计 ”中,我们分享了我们参与今年初和2018年举办的活动的经验。

好吧,让我们玩我们的“发现错误”游戏。 首先,我们将按语言分组分析2018年早期的挑战。

2018年


C ++


铬虫

static const int kDaysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; bool ValidateDateTime(const DateTime& time) { if (time.year < 1 || time.year > 9999 || time.month < 1 || time.month > 12 || time.day < 1 || time.day > 31 || time.hour < 0 || time.hour > 23 || time.minute < 0 || time.minute > 59 || time.second < 0 || time.second > 59) { return false; } if (time.month == 2 && IsLeapYear(time.year)) { return time.month <= kDaysInMonth[time.month] + 1; } else { return time.month <= kDaysInMonth[time.month]; } } 

解决方案
在Chromium中发现的这个错误可能是最“长期运行”的挑战。 我们一直将其提供到2018年,并且还将其包含在多个演示中。

 if (time.month == 2 && IsLeapYear(time.year)) { return time.month <= kDaysInMonth[time.month] + 1; // <= day } else { return time.month <= kDaysInMonth[time.month]; // <= day } 

最后一个If-else块的主体在return语句中包含拼写错误: time.month是第二次而不是time.day被意外写入。 此错误使函数始终返回true 。 该错误将在文章“ 2月31日 ”中详细讨论,并且是一个很酷的错误示例,该错误很难被代码审查发现。 这个案例也很好地说明了我们如何使用数据流分析。

虚幻引擎错误

 bool VertInfluencedByActiveBone( FParticleEmitterInstance* Owner, USkeletalMeshComponent* InSkelMeshComponent, int32 InVertexIndex, int32* OutBoneIndex = NULL); void UParticleModuleLocationSkelVertSurface::Spawn(....) { .... int32 BoneIndex1, BoneIndex2, BoneIndex3; BoneIndex1 = BoneIndex2 = BoneIndex3 = INDEX_NONE; if(!VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[0], &BoneIndex1) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[1], &BoneIndex2) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[2]) &BoneIndex3) { .... } 

解决方案
这里要注意的第一件事是VertInfluencedByActiveBone()函数的最后一个参数具有默认值,不需要指定。 现在以简化形式查看if块:

 if (!foo(....) && !foo(....) && !foo(....) & arg) 

该错误现在清晰可见。 由于存在拼写错误,对VertInfluencedByActiveBone()函数的第三次调用使用三个参数而不是四个参数执行,返回值然后参与操作(按位AND:左操作数是VertInfluencedByActiveBone返回的bool类型的值( ) ,右操作数是整数变量BoneIndex3 )。 该代码仍可编译。 这是固定版本(添加逗号,右括号移到表达式的末尾):

 if(!VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[0], &BoneIndex1) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[1], &BoneIndex2) && !VertInfluencedByActiveBone( Owner, SourceComponent, VertIndex[2], &BoneIndex3)) 

该错误最初是在文章“ 虚幻引擎4的期待已久的检查 ”中提到的,其标题为“最好的错误”,我完全同意。

Android错误

 void TagMonitor::parseTagsToMonitor(String8 tagNames) { std::lock_guard<std::mutex> lock(mMonitorMutex); // Expand shorthands if (ssize_t idx = tagNames.find("3a") != -1) { ssize_t end = tagNames.find(",", idx); char* start = tagNames.lockBuffer(tagNames.size()); start[idx] = '\0'; .... } .... } 

解决方案
程序员对if块条件下的操作优先级有错误的假设。 此代码无法正常工作:

 if (ssize_t idx = (tagNames.find("3a") != -1)) 

将为idx变量分配值0或1,条件是真还是假将取决于此值,这是一个错误。 这是固定版本:

 ssize_t idx = tagNames.find("3a"); if (idx != -1) 

在文章“ 我们通过PVS-Studio检查了Android源代码,或者没有完美的方法 ”中提到了此错误。

这是另一个带有Android错误的重要挑战:

 typedef int32_t GGLfixed; GGLfixed gglFastDivx(GGLfixed n, GGLfixed d) { if ((d>>24) && ((d>>24)+1)) { n >>= 8; d >>= 8; } return gglMulx(n, gglRecip(d)); } 

解决方案
问题出在(d >> 24)+ 1表达式中。

程序员希望检查d变量的8个最高有效位是否设置为1,但不是一次都设置为全部。 换句话说,他们想检查最高有效字节是否存储除0x00和0xFF之外的任何值。 首先,程序员使用(d >> 24)表达式检查最高有效位是否为null。 然后,他们将八个最高有效位移至最低有效字节,并期望最高有效符号位在所有其他位中重复。 也就是说,如果d变量的值为0b11111111'00000000'00000000'00000000,则在移位后它将变为0b11111111'11111111'11111111'11111111。 通过在int值0xFFFFFFFF上加1,程序员期望得到0(-1 + 1 = 0)。 因此, ((d >> 24)+1)表达式用于检查不是所有八个最高有效位都设置为1。

但是,最高有效符号位在移位时不一定会“扩展”。 这就是标准所说的:“ E1 >> E2的值是E1右移E2位的位置。 如果E1具有无符号类型,或者E1具有带符号类型且非负值,则结果的值是E1 / 2 / E2的商的整数部分。 如果E1具有带符号的类型和负值,则结果值是实现定义的

因此,这是实现定义的行为的示例。 此代码的确切工作方式取决于CPU架构和编译器实现。 移位后最高有效位很可能最终为零,并且((d >> 24)+1)表达式将始终返回除0以外的值,即始终为真值。

确实,这是一个不平凡的挑战。 像以前的错误一样,该错误最初在文章“ 我们通过PVS-Studio检查了Android源代码,或者没有什么是完美的 ”中讨论过。

2019年


C ++


“都是海湾合作委员会的错”

 int foo(const unsigned char *s) { int r = 0; while(*s) { r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1); s++; } return r & 0x7fffffff; } 

程序员将此错误归咎于GCC 8编译器。 真的是海湾合作委员会的错吗?

解决方案
该函数返回负值,因为编译器不会为按位与(&)生成代码。 该错误与未定义的行为有关。 编译器注意到, r变量用于计算和存储总和,仅涉及正值。 r变量不应溢出,因为那将是未定义的行为,编译器完全不必考虑。 因此得出的结论是,由于r在循环结束时不能有负值,因此不需要操作r&0x7fffffff来清除符号位,因此只需告诉函数返回r的值即可。

在文章“ PVS-Studio 6.26已发布 ”中描述了此错误。

QT错误

 static inline const QMetaObjectPrivate *priv(const uint* data) { return reinterpret_cast<const QMetaObjectPrivate*>(data); } bool QMetaEnum::isFlag() const { const int offset = priv(mobj->d.data)->revision >= 8 ? 2 : 1; return mobj && mobj->d.data[handle + offset] & EnumIsFlag; } 

解决方案
mobj指针的处理方式不安全:先取消引用,然后再检查。 经典。

在“ 使用PVS-Studio对Qt 5进行第三次检查文中提到了该错误。

C#


Infer.NET错误

 public static void WriteAttribute(TextWriter writer, string name, object defaultValue, object value, Func<object, string> converter = null) { if ( defaultValue == null && value == null || value.Equals(defaultValue)) { return; } string stringValue = converter == null ? value.ToString() : converter(value); writer.Write($"{name}=\"{stringValue}\" "); } 

解决方案
评估value.Equals(defaultValue)表达式时,可能会取消对value变量的空引用。 当变量的值等于 defaultValue!= Nullvalue == null时,就会发生这种情况。

此错误来自文章“ Infer.NET代码中存在什么错误?

FastReport错误

 public class FastString { private const int initCapacity = 32; private void Init(int iniCapacity) { sb = new StringBuilder(iniCapacity); .... } public FastString() { Init(initCapacity); } public FastString(int iniCapacity) { Init(initCapacity); } public StringBuilder StringBuilder => sb; } .... Console.WriteLine(new FastString(256).StringBuilder.Capacity); 

程序将在控制台中输出什么? FastString类怎么了?

解决方案
程序将输出值32。原因是传递给构造函数中Init方法的变量的拼写错误的名称:

 public FastString(int iniCapacity){ Init(initCapacity); } 

不会使用构造函数参数iniCapacity ; 相反,传递的是常量initCapacity

在“狂野的西部最快的报告-以及少数的错误...文中讨论了该错误。

罗斯林错误

 private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....)) { .... var nodeOrToken = current.ChildThatContainsPosition(....); .... current = nodeOrToken.AsNode(); } .... } public SyntaxNode AsNode() { if (_token != null) { return null; } return _nodeOrParent; } 

解决方案
current.FullSpan.Contains(....)表达式中的current的潜在空引用。 作为调用nodeOrToken.AsNode()方法的结果,可以为当前变量分配一个空值。

此错误来自文章“ 检查Roslyn源代码 ”。

Unity错误

 .... staticFields = packedSnapshot.typeDescriptions .Where(t => t.staticFieldBytes != null & t.staticFieldBytes.Length > 0) .Select(t => UnpackStaticFields(t)) .ToArray() .... 

解决方案
错字:使用运算符代替&& 。 即使t.staticFieldBytes变量为null ,这也会导致始终执行t.staticFieldBytes.Length> 0检查,这反过来又导致空取消引用。

该错误最初显示在“ 讨论Unity3D的开源组件中的错误 ”一文中。

爪哇


IntelliJ IDEA错误

 private static boolean checkSentenceCapitalization(@NotNull String value) { List<String> words = StringUtil.split(value, " "); .... int capitalized = 1; .... return capitalized / words.size() < 0.2; // allow reasonable amount of // capitalized words } 

为什么程序会错误地计算大写单词的数量?

解决方案
如果大写单词的数量少于20%,则该函数应返回true 。 但是由于整数除法,该检查不起作用,整数除法仅求值为0或1。仅当所有单词都大写时,该函数才会返回false 。 否则,除法将得出0,并且该函数将返回true

该错误来自文章“ PVS-Studio for Java ”。

Spotbugs错误

 public static String getXMLType(@WillNotClose InputStream in) throws IOException { .... String s; int count = 0; while (count < 4) { s = r.readLine(); if (s == null) { break; } Matcher m = tag.matcher(s); if (m.find()) { return m.group(1); } } throw new IOException("Didn't find xml tag"); .... } 

搜索xml标记有什么问题?

解决方案
计数<4条件始终为true,因为变量计数在循环内不会增加。 xml标记本应在文件的前四行中搜索,但是由于缺少增量,因此程序将读取整个文件。

像以前的错误一样,该错误在文章“ PVS-Studio for Java ”中进行了描述。

今天就这些了。 快来看我们即将举行的活动-寻找独角兽。 我们将提供新的有趣挑战,当然还有奖品。 再见!

Source: https://habr.com/ru/post/zh-CN476268/


All Articles