un

guest
1 / ?
back to lessons

名称不是发现

你现在知道了七种 MOAD 模式。知道名称很重要:它让你在看到时能够识别出模式。但是在受控的课程中识别与在你从未打开过的代码库中发现是有区别的。

代码库不会为其缺陷贴标签。沉积式 MOAD 没有带有注释说 // O(N²) — 修复这个 的提示。暴发群众不会宣布自己是一次缓存缺失的冲击波。你通过带有特定问题的头脑阅读代码来找到它们: 这个值的数据结构是什么,在循环内对它执行什么操作?

检测是一项独立于识别的技能。 识别说:是的,那条模式是 MOAD-0001。检测说:让我在这个代码库中找到所有可能存在这种模式的地方,无论我是否能看到完整的代码或只看到符号名称。

![/static/diagrams/unhamming_moad_overview.svg]

首次扫描

第一次过滤使用 grep。每个 MOAD 都有一个基质:一个数据结构或 API,在某些操作附近的存在值得调查。

MOAD-0001 (沉积): 在循环中使用 List.contains

# 信号:在循环中对列表变量执行成员资格测试
grep -rn '.contains(' src/ | grep -v HashSet | grep -v TreeSet
grep -rn 'visited =' src/ | grep -v set | grep -v Set

MOAD-0002 (交织): 在不同阶段共享可变标志

# 信号:静态可变字段由一个子系统写入,另一个子系统读取
grep -rn 'static ' src/ | grep -v final | grep -v class | grep -v void

MOAD-0003 (泄露上下文): 在池化执行器中使用 ThreadLocal

# 信号:没有确保 ThreadLocal.remove() 的 ThreadLocal.set()
grep -rn 'ThreadLocal' src/
grep -rn 'ThreadLocal.set' src/ -l

MOAD-0004 (记录秘密): 在日志输出中使用 HTTP 头

# 信号:带有 headers 变量的日志调用接近认证端点
grep -rn 'log.*header' src/
grep -rn 'Authorization' src/ --include='*.log'

MOAD-0005 (Thundering Herd):在没有同步的情况下缓存失效

# 信号:cache.get() + null 检查 + cache.put() 无锁
grep -rn 'cache.get' src/ -A4 | grep 'cache.put'

这些模式产生候选者,而不是确定的缺陷。每个候选者都需要分级:阅读周围的代码,验证数据结构类型,确认操作在大规模上运行。

从 MOAD-0001 到 MOAD-0005 中选择一个 MOAD。描述在一个你从未阅读过的代码库中进行第一次检测步骤:你会搜索什么,正向匹配是什么样子的,以及确认缺陷与误报之间的区别是什么。

为了复杂性阅读代码

grep 找到候选者。阅读确认它们。当你打开一个候选文件时,你会阅读一个问题:这个操作的成本随输入大小增长吗?

对于MOAD-0001,确认协议:

1. 找到外层循环。循环迭代次数是由什么限制的?
2. 找到内部操作(.contains, .indexOf, 'in')。它运行的是哪个数据结构?
3. 这个数据结构是否随相同的输入驱动外层循环而增长?
4. 如果是的话:成本是O(N²),其中N=input size。确认缺陷。
5. 如果不是:内部结构受限(配置,枚举,小常数)。错误正例。

一个遍历图遍历N个节点,检查每一步的visited列表:循环和内部数据结构都随N增长。确认。

一个请求处理程序检查5个管理员IP的允许列表:允许列表随请求量增长时不会增长。错误正例。

对于每个MOAD,采用相同的协议:识别外部驱动器,识别内部结构,询问它们是否同时扩展。

Surge Score:优先级排序您的发现

所有确认的缺陷并不都需要立即修复。一个在库中拥有 10,000 个下游依赖项的 MOAD 的冲击分数高于在一个私人内部工具中的同一个 MOAD。

冲击分数 = 加速系数 × 入度。 加速系数:修复如何在典型生产规模下运行得更快? 入度:自动在上游合并时继承修复的下游包或服务有多少?

在 Apache Maven 的依赖解析器中确认的 MOAD-0001,运行在 50,000 个节点的图表,拥有 1,000+ 个自动继承更改的下游 Maven 插件:冲击分数非常高。这次修复应排在您队列的前面。

在一个无依赖用户界面命令行工具中确认的 MOAD-0001:冲击分数接近零。值得修复,但并不紧急。

工作狂 vs. 吸收节点。 一个在高之间性和高加速系数的节点是工作狂:它处理关键流程并在解除阻塞时将下游队列冲洗干净。只有在确认下游容量后才修复它。一个在高出度和低加速系数的节点是吸收者:它消费掉所有喂给它的东西,并不感到痛苦。没有预先确认下游容量地修复工作狂可能会在基础设施规模上产生 MOAD-0005(暴风骤雨的群众)。

您已在两个地方确认了 MOAD-0001:(A) 一个在构建工具中,拥有 20,000 个活跃项目依赖于它,运行在 10,000 个依赖关系树的图表;(B) 一个公司内部的图表工具,运行在 50 个节点的图表。比较它们的冲击分数。您应该首先修复哪个,何时在披露之前采取哪些步骤?

扫描合并:一个MOAD管道

一个已确认的缺陷具有高波动分数通过一个管道。每个阶段产生一个artifact。没有阶段是可选的。

扫描  → 候选列表(grep输出,静态分析结果)
工单  → 缺陷描述(MOAD编号,位置,复杂性分析)
补丁  → 代码更改(数据结构交换,基本元素采用)
测试  → 单元测试(O(1)证明:在N=100和N=10,000时测量修复时间)
UNDF  → 公开披露帖子(undefect.com,公共领域)
披露  → 如果安全相关则引用CVE或CWE
PR  → 上游拉取请求(包含补丁+测试+UNDF链接)
合并  → 维护者接受;修复通过版本号递增传播

每个artifact都喂饱下一个阶段。一个没有测试的补丁无法得到验证。一个没有披露的测试无法传播到相同模式的其他实例。一个没有上游PR的披露会让修复陷入分支。

一个MOAD帖子(UNDF)是工程师最常省略的阶段。 他们修复缺陷,提交PR,并认为自己完成了。然而,没有给出明确帖子的修复意味着每个未来遇到相同模式的工程师必须独立重新发现问题和修复。一个MOAD帖子关闭了知识循环:它给出了模式名称,展示了检测方法,并链接到了补丁。未来的研究人员通过搜索模式名称找到修复。

星球级修复。 一个MOAD-0001修复在广泛使用的库中进行,传播到导入它的所有项目。一个MOAD帖子确保了那些永远不会升级该库的项目的工程师仍然能学到修复。两个路径并行运行。

编写缺陷工单

一个好的缺陷工单能回答五个问题:

1. 在哪里:精确的文件,类,函数和行范围
2. 什么:数据结构类型和对其的操作
3. 为什么:复杂性分析(N²或更糟,N用定义)
4. 影响:什么输入触发最坏的行为,以及在什么规模上
5. 修复:替换的数据结构或基本元素

回答了所有五个问题的工单是自包含的:一个从未阅读过你的分析的维护者可以重复你的发现并验证你的修复。跳过(3)或(4)的工单需要维护者在合并之前重新进行复杂性分析。这种摩擦减少了合并的可能性。

可信度叠加。 第一个 PR 包含明确的 ticket、一份针对性的补丁和一个基准测试,它被合并了。同一作者的第二个 PR 在维护者已经审查了前两者之后会遇到更少的阻力。第三个 PR 被首先合并的维护者审查。开源中的声誉是一个 artifact 的账本:每个接受的补丁都会为下一个赢得信任。

为一个你期望在图库中发现的MOAD-0001编写一个最小的缺陷工单。包括:(1)一个可信的文件/函数名称,(2)数据结构和操作,(3)复杂性声明,(4)典型的影响场景,(5)修复方法。

阅读一个真实的候选者

以下是一个真实的 MOAD-0001 候选者,使用 Python 语言。阅读它并完成筛选流程。

class DependencyResolver:
    def resolve(self, package, resolved=None, seen=None):
        if resolved is None:
            resolved = []
        if seen is None:
            seen = []
        if package in seen:
            return
        seen.append(package)
        for dep in self.registry.get_dependencies(package):
            self.resolve(dep, resolved, seen)
        resolved.append(package)
        return resolved

筛选问题:

1. `seen` 的数据结构是什么?
2. 第 6 行对它执行的操作是什么?
3. `seen` 在输入大小随着增长吗?
4. 驱动递归调用的大循环是否随输入大小增长?
5. 这是一个确认的 MOAD-0001 还是一个误报?
按照五个筛选问题对这个代码进行筛选。然后写出一行修复并解释为什么它不会改变函数的输出。

你的补丁

一个已确认的缺陷具有高的峰值分数需要一个完整的补丁:代码修复、一个证明改进的测试以及一个 MOAD 后出的概要。

测试必须是性能测试,而不是正确性测试。 正确性测试在修复前后通过——这就是问题所在;输出不会发生变化。性能测试在两个输入大小上证明了改进:

import time

def build_graph(n):
    # n packages, each depending on the previous one
    return {f'pkg{i}': [f'pkg{i-1}'] if i > 0 else [] for i in range(n)}

for n in [100, 1000, 5000]:
    registry = build_graph(n)
    resolver = DependencyResolver(registry)
    start = time.perf_counter()
    resolver.resolve(f'pkg{n-1}')
    elapsed = time.perf_counter() - start
    print(f'n={n}: {elapsed:.4f}s')

在修复之前,耗时与 n 成比例增长。在修复之后,它以线性增长。打印两者,并在 PR 描述中包含数字。

一个 MOAD 投稿概述包括:模式名称、子系统(Python依赖解析器)、检测方法(grep in seen,其中 seen[] 开始)、修复方法以及链接到您的 PR。该帖子将作为公共领域发布到 undefect.com。未来的工程师在搜索 'Python 列表成员资格循环慢' 时会找到它。

你已经确认并修复了一个受欢迎的 Python 打包工具中的 MOAD-0001。在你打开 PR 之前,在 PR 描述中,你需要包括三个什么东西,为什么每个对维护者来说都很重要?