# FluidSynth 源码审计报告

**初次审计：** 2026-10-25（新加坡时间，截至 10:00）  
**✦ 复审更新：** 2026-10-25（新加坡时间；本轮验证结果见下）  
**声明基线：** upstream/master `c7ba7b499a29cd230b844007c06f9158f1db6a03`  
**本轮修复工作区：** HEAD `a1e767044ebddd829192764aba1fb39452c0a29a` 加当前未提交补丁；与声明基线分别核对  
**在线 HTML：** [fluidsynth-audit-20261025.pages.dev](https://fluidsynth-audit-20261025.pages.dev/)

## 概览

原报告列出 51 个编号；本轮逐项复审后，**50 项缺陷成立，F26 相对声明基线的归因撤回**，保留该编号以便追踪更正。已更新的 20 个条目以 **✦** 标记，包括诊断、根因补修、输入分类或评级调整。严重度仍为 1–10 的排序分，不是 CVSS；同时考虑实际后果、可达入口、触发条件及常见程度，不因文件不合规就一律降低内存安全问题的优先级。

- 原审计常规测试：**74/74 通过**（历史构建结果）。
- 原审计覆盖率构建：**71/71 通过**；源码行覆盖率 **61.5%**，分支覆盖率 **45.4%**，函数覆盖率 **67.6%**。
- 原审计 ASan / UBSan / LSan：排除两个已有未合并分支修复覆盖的泄漏用例后，**72/72 通过**。未过滤的运行只在 `test_sfont_unloading` 和 `test_shell_server_auto_port` 失败；它们分别对应未合并的 timer-thread handle 和 shell mutex 修复。
- 对比 upstream 的 **24 个**未合并 `origin/fix/*` 分支，排除已有修复的问题。

## ✦ 本轮复审与验证

使用 3 个 sub agent 分别核对 DLS、MIDI/路由/时序，以及 SF2/voice/插值；主审核对 settings、生命周期、效果器、SDL 与调音导出。51 个编号均核对了源码、原始差异与对应测试，重要争议另做独立变形输入和规范核实。没有把全部 50 项基线 sanitizer 复现重新跑一遍。

- **根因补修：** F01、F08、F17/F34、F35、F41、F42、F47；F12/F13 同时移除不必要的 API 兼容性收紧。
- **归因与表述纠正：** F26 在声明基线已经修复；F16 不是普通合法 DLS 调制；F19 固定力度 0 不保证静音；F06 协议名、F21 分配量及 F29 超长消息合法性已更正。
- **评级调整：** F02 5→3、F10 3→1、F12 2→5、F16 6→3、F19 5→3、F30 3→5。其余沿用原排序分，F26 撤回不计分。
- **最终验证：** 独立新建普通构建 **71/71 通过**；clang ASan / UBSan / float-cast-overflow / LSan 未过滤运行 **69/71 通过**，失败仅为原已披露的 `test_sfont_unloading`（timer-thread handle 泄漏）和 `test_shell_server_auto_port`（shell mutex 泄漏）。本轮新增与修改的根因回归均通过。启用 SDL3、DLS、多核线程；关闭 libsndfile 及部分宿主音频后端，因此测试数量与原 74 项配置不同。
- 原覆盖率、fuzz 和 Mull 数字是历史结果，**未对本轮补丁重新测量**，不能作为新增边界已经覆盖的证明。

## 原审计方法与测试

先用 Lizard 复杂度和 gcovr 覆盖率定位高复杂、低覆盖函数，再通过独立输入复现问题。对确认的问题补充回归测试，并用 ASan、UBSan、LSan 检查内存安全和未定义行为。

- libFuzzer：MIDI、DLS、SF2 解析器累计分别运行 428,395、471,995、195,433 次；最近一轮分别运行 91,203、131,129、65,361 次，未发现崩溃。
- SysEx fuzz：以有效 MTS、GS、XG 种子运行 181 秒，共 17,525,710 个字节输入，未发现崩溃。
- 合成器公开 API 状态 fuzz：F44 修复版额外运行 26,963,447 个输入，未发现新崩溃。此前另有 11,175 和 21,483 个输入分别重现并修复数值边界问题。
- Mull 源码变异分数按文件范围统计，反映所选回归测试杀死变异的比例，不代表单项缺陷分数：MIDI parser 57%、MIDI router 87%、DLS parser 78%、synth 31%、voice 55%、sequencer 50%、单音连奏 50%。
- F51 的单独定向 Mull 运行在 DLS 源文件范围内杀死 30/92 个变异（32%）；这不是 F51 单项分数。DLS parser 78% 来自覆盖更广的测试集。幸存变异仍代表可以继续补充的测试空间。

## 发现清单

### F14 — 删掉合成器后，设置变化仍会呼叫已释放的合成器

- **区域 / 类型：** 对象生命周期 / 内存安全
- **严重度：** 7 / 10
- **输入场景：** 常规合法 API 生命周期
- **影响：** 合成器注册了设置回调。删掉合成器后，回调还留在设置对象里；随后应用改一次移调时间设置，就可能访问已经释放的合成器。
- **触发条件：** 用同一个 settings 创建并销毁 synth，之后继续修改 synth.portamento-time。
- **发现方法：** 用 ASan 在删除 synth 后修改仍存活的 settings，重现回调里的 use-after-free。
- **发现时间：** 2026-10-25 约 03:14–03:16（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+2 / −0 行`
- **回归测试：** `test_settings_unregister_callback`

### ✦ F01 — 路由器把不同声道的同音高音符当成一个

- **区域 / 类型：** MIDI router / 音频正确性
- **严重度：** 6 / 10
- **输入场景：** 常规合法 MIDI 输入
- **影响：** 两个输出声道同时弹同一个音高时，路由器只记了一份按键状态。清理路由规则后，第一个 NOTE OFF 会清掉第二个音符的记录，第二个关闭消息可能被丢掉，声音就挂住。
- **触发条件：** 在两个输出声道同时弹同一音高，然后清空路由规则并分别发送 NOTE OFF。
- **发现方法：** 构造双声道同音高的路由用例，基线只转发第一个 NOTE OFF。复审另发现等待清理的规则仍为未转发的新 NOTE ON / Sustain 按下登记 pending，随后可能转发幽灵关闭；现仅记录实际转发的按下事件。回归同时验证跨声道独立清理，以及被抑制的新按下不会产生关闭消息。
- **发现时间：** 2026-10-25 约 02:10（新加坡时间）
- **生产代码改动：** `fluid_midi_router.c（含按输出声道保存状态、等待规则仅追踪已转发事件）`
- **回归测试：** `test_midi_router`
- **✦ 复审更新：** 原跨声道诊断成立；补全“pending 只对应实际转发事件”的状态不变量，并纠正原测试对幽灵 NOTE OFF 的错误预期。

### ✦ F13 — 替换 SoundFont 默认调制器后，新音符会读旧地址

- **区域 / 类型：** SoundFont 生命周期 / 内存安全
- **严重度：** 6 / 10
- **输入场景：** 合法但罕见的 API 更新
- **影响：** 每个采样原先保存的是调制器链表的旧地址。应用替换 SoundFont 默认调制器后，旧链表被释放，后续新音符仍从旧地址读调制器，可能触发 use-after-free。
- **触发条件：** 加载 SF2，弹出音符；在不并发渲染的情况下替换该 SoundFont 的默认调制器，再触发新音符。
- **发现方法：** ASan 基线重现旧链表访问；回归测试覆盖有效列表替换、清空和之后的新音符。
- **发现时间：** 2026-10-25 约 03:12–03:14（新加坡时间）
- **生产代码改动：** `fluid_sfont.h / fluid_defsfont.c / fluid_dls.cpp / fluid_synth.c / fluid_sfont.c（列表间接引用及保持 NULL 清除契约）`
- **回归测试：** `test_sfont_mod_dest_bounds`
- **✦ 复审更新：** 旧列表地址的生命周期诊断和间接引用修复成立。复审撤掉附带新增的 mods!=NULL || nmods==0 限制：按原文档，mods 为 NULL 时任意非负计数都应清除列表；回归保留此兼容行为。

### F18 — 合法的 unpitched SF2 样本在自定义音律下越界

- **区域 / 类型：** SF2 播放 / 内存安全
- **严重度：** 6 / 10
- **输入场景：** 合法格式组合，但较少见
- **影响：** SF2 用 255 标记没有固定音高的样本。启用自定义音律时，FluidSynth 把 255 当成音高表下标，越过数组边界，还会算错播放音高。
- **触发条件：** 载入一个 byOriginalPitch=255 的 SF2 样本，开启自定义音律，再播放该音符。
- **发现方法：** 按 SF2 规格创建 unpitched 样本并使用自定义音律；ASan 重现越界访问。修复按规格使用根音 60 回退。
- **发现时间：** 2026-10-25 约 03:35（新加坡时间）
- **生产代码改动：** `fluid_defsfont.c：+3 / −1 行`
- **回归测试：** `test_sfont_unpitched_tuning`

### ✦ F12 — 坏调制器目的地会变成 voice 数组下标

- **区域 / 类型：** SF2 载入 / 调制器 API / 内存安全
- **严重度：** 5 / 10
- **输入场景：** 构造的不合规 SF2 / API 值
- **影响：** 恶意 SoundFont 或调用者把调制器目标设到 generator 范围之外。弹音或更新控制器时，这个值会被用作 voice generator 数组下标，造成越界访问。
- **触发条件：** 向 SF2 DMOD 写入无效目的地、调用默认调制器 API，或让自定义 preset 通过 fluid_voice_add_mod 加入越界目标，再发送对应 MIDI 控制器。
- **发现方法：** 基线 ASan / UBSan 在 note-on 和控制器更新路径重现越界；回归覆盖文件导入、两个默认调制器 API 和公开 voice API。
- **发现时间：** 2026-10-25 约 03:06–03:08；voice API 路径约 07:18（新加坡时间）
- **生产代码改动：** `defsfont.c / mod.c / synth.c / sfont.c：+19 / −13 行；fluid_voice.c：+4 / −0 行`
- **回归测试：** `test_sfont_mod_dest_bounds`
- **✦ 复审更新：** 文件与 API 路径的无效目标检查修到了数组下标根因。恶意 SF2 可在播放/控制器更新时导致越界读写，与调用方误用 API 的暴露面不同，排序分由 2 调为 5；同时撤掉不必要的 NULL 清除限制，保留原 API 契约。

### ✦ F30 — SF2 样本结束位置能超出 PCM 数据边界

- **区域 / 类型：** SF2 载入 / 播放 / 内存安全
- **严重度：** 5 / 10
- **输入场景：** 构造的不合规 SF2
- **影响：** 样本头把 end 指到采样数据之后时，加载器仍接受这个样本；播放插值会越过 PCM 数组末尾读取。
- **触发条件：** 把 SF2 的 Fm3_a1 样本 end 设为总采样字数加 1，载入后播放对应音符。
- **发现方法：** ASan 基线在四阶插值器读取结束点时报告堆越界；新用例确认加载器丢弃这个样本，避免产生可播放的越界样本。
- **发现时间：** 2026-10-25 约 05:20（新加坡时间）
- **生产代码改动：** `fluid_sfont.c：+1 / −1 行`
- **回归测试：** `test_sfont_sample_end_bounds`
- **✦ 复审更新：** 样本结束索引与 PCM 长度混淆的诊断成立，改为 end>=字数即拒绝修到了根因。恶意 SF2 可直接在常规播放中引起堆越界读，无需错误 API 或自定义音律，排序分由 3 调为 5。

### F09 — 调音名称缓冲区长度为 0 时发生栈越界

- **区域 / 类型：** Tuning API / 内存安全
- **严重度：** 4 / 10
- **输入场景：** 罕见 API 边界调用
- **影响：** 传入非空名称缓冲区和长度 0，代码把负数长度交给 snprintf；ASan 报告栈缓冲区越界。
- **触发条件：** 调用 fluid_synth_tuning_dump(synth, bank, program, name, 0)，其中 name 非空。
- **发现方法：** 对长度 0 的调用运行 ASan，基线重现越界；修复明确拒绝零长度，并保留长度 1 时的 NUL 终止。
- **发现时间：** 2026-10-25 约 02:47（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+1 / −0 行`
- **回归测试：** `test_tuning_dump_zero_name_length`

### ✦ F02 — 合法的 8 位 DLS 样本会触发未定义行为

- **区域 / 类型：** DLS 采样转换 / 未定义行为
- **严重度：** 3 / 10
- **输入场景：** 合法 DLS 格式，但少见
- **影响：** 8 位 PCM 转成 16 位时，负数被左移。C/C++ 把这个操作列为未定义行为；不同编译器或优化级别可能得到不同样本值。
- **触发条件：** 加载含 8 位 PCM 波形的 DLS。
- **发现方法：** 把随库提供的 DLS 样本转为 8 位 PCM，运行 UBSan；基线在样本值 −128 处报告有符号左移。
- **发现时间：** 2026-10-25 约 02:23（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+1 / −1 行`
- **回归测试：** `test_dls_parser`
- **✦ 复审更新：** 诊断与乘法替代左移的修复成立。项目 DLS loader 使用 C++17，负数左移确属 UB；现有证据证明 sanitizer 错误，未证明正常构建出现实际音频差异，排序分由 5 调为 3。

### ✦ F08 — 极端调音数值在 MTS 导出时转成整数会溢出

- **区域 / 类型：** MIDI Tuning Standard / 未定义行为
- **严重度：** 3 / 10
- **输入场景：** 合法 API，但极端数值少见
- **影响：** 调音 API 接收非常大的 double 或非有限数值；之后请求 MTS bulk dump 时，代码先把它转成整数，可能发生未定义行为。
- **触发条件：** 用 fluid_synth_tune_notes 设置 DBL_MAX、−DBL_MAX 或 NaN，再请求 MTS bulk tuning dump。
- **发现方法：** 公开调音 API 加 float-cast-overflow sanitizer 可重现极值转整数 UB。先在 double 域限制范围，再转换。复审把上限改为 12800−200/16384 cents，最大输出为 7F 7F 7E；避免把保留的 7F 7F 7F（不改变音高）当成最高频率。回归覆盖 DBL_MAX、负极值、12800 和临界舍入。非有限 API 输入另由 F43 拒绝。
- **发现时间：** 2026-10-25 约 02:44–02:45（新加坡时间）
- **生产代码改动：** `fluid_synth.c（转换前限幅，并排除 MTS 的 no-change 保留字）`
- **回归测试：** `test_mts_bulk_dump_pitch_range`
- **✦ 复审更新：** 原 UB 诊断成立，原补丁消除了越界转换，但其上限和测试误把 no change 当作饱和音高。现统一限制到最大有效频率字，并修正回归断言。

### ✦ F16 — DLS 的音高缩放能把 MIDI 音符变成数组外下标

- **区域 / 类型：** DLS 播放 / 内存安全
- **严重度：** 3 / 10
- **输入场景：** 构造的不合规 DLS 键号连接
- **影响：** DLS 的音高调制把按键 60 放大到 132。启用自定义音律时，内部音高表按这个值取数，越过 128 个 MIDI 音符的范围。
- **触发条件：** 在 instrument 级插入无专有条件守卫的 MIDI Note→Key 连接，将按键缩放为 2.2 倍，并启用自定义音律。
- **发现方法：** 构造可被旧加载器接受的两份 instrument LART，把按键 60 缩放到 132；ASan 重现 tuning 表越界。修复在播放路径限制内部按键为 0–127。DLS 2.2 §1.7.3 / Table 6 规定该隐含连接固定，普通文件不得这样重写。
- **发现时间：** 2026-10-25 约 03:22–03:24（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+1 / −1 行`
- **回归测试：** `test_dls_parser`
- **✦ 复审更新：** 越界与安全修复成立，但原报告“合法 DLS region 调制”错误：复现是 instrument 级不合规连接。输入分类纠正，排序分由 6 调为 3；此限幅是安全防护，不代表支持这种扩展的音高语义。

### ✦ F17 — 超范围固定键或根音能越过调音表

- **区域 / 类型：** SF2 播放 / 内存安全
- **严重度：** 3 / 10
- **输入场景：** 不合规 SF2 或超范围 API 值
- **影响：** 固定 key 或 overridingRootKey 超出 MIDI 0–127 后，自定义音律路径可能以越界下标访问 128 项音高表。文件加载校验不能覆盖全部公开 voice API。
- **触发条件：** 把 SF2 固定 key 写为 200，或调用频道 GEN_KEYNUM=200；也可对活动 voice 设置 GEN_OVERRIDEROOTKEY=200 / FLT_MAX，再更新参数。
- **发现方法：** 基线 ASan/UBSan 重现固定 key 路径越界。原补丁已修文件和 GEN_KEYNUM 路径；复审补出公开 voice API 仍可改变 overridingRootKey 的遗漏。现在 root_key 形成处统一限制为 0–127 后再换算为 cents，覆盖所有到达该路径的入口。
- **发现时间：** 2026-10-25 约 03:27–03:29；API 路径约 05:05（新加坡时间）
- **生产代码改动：** `fluid_sffile.c / fluid_voice.c（文件校验、实际 key 和 root_key 形成处约束）`
- **回归测试：** `test_sfont_keynum_bounds；test_voice_generator_bounds`
- **✦ 复审更新：** 原报告所列路径已修，但相同调音表下标不变量在 root-key voice API 上漏检。现补全共享形成点防护，回归覆盖 200 和 FLT_MAX，并验证后续音频有限。

### ✦ F19 — SF2 的固定力度 0 被忽略

- **区域 / 类型：** SF2 播放 / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 合法 SF2 参数，但较少见
- **影响：** 固定 velocity=0 是允许值，应覆盖调制器看到的实际力度。原判断把 0 当作未设置，继续使用输入音符力度，导致力度相关调制错误；是否静音取决于该音色的调制器。
- **触发条件：** 一个合法 SF2 区域把固定 velocity 生成器设为 0，然后播放落在区域里的音符。
- **发现方法：** 保留默认调制器下的音频对比，并直接断言实际力度为 0；另以 amount=0 覆盖默认 velocity→attenuation 调制器，验证固定力度 0 仍可合法发声。修复使用 x>=0 识别所有有效固定力度。
- **发现时间：** 2026-10-25 约 03:38（新加坡时间）
- **生产代码改动：** `fluid_voice.c：+1 / −1 行`
- **回归测试：** `test_sfont_fixed_velocity_zero`
- **✦ 复审更新：** 缺陷和 >=0 修复成立，但“固定力度 0 必定静音”不是 SF2 规则，不能套用 MIDI Note On velocity=0 的 Note Off 语义。改为有效力度覆盖问题，排序分由 5 调为 3。

### ✦ F21 — 小 DLS 文件能诱发数 GB 的未验证预分配

- **区域 / 类型：** DLS 载入 / 资源消耗 / 格式解析
- **严重度：** 3 / 10
- **输入场景：** 构造的恶意 DLS
- **影响：** 约 617 KB 的文件可把乐器数或区域数写成一亿，旧解析器直接 reserve。复审在声明基线拦截到 13,600,000,000 字节的 instrument 请求和 7,200,000,000 字节的 region 请求；大额地址空间申请可能失败或增加资源压力。
- **触发条件：** 把 colh 乐器数或 instrument header 的 cRegions 设为 100,000,000，远超父块实际能容纳的结构数。
- **发现方法：** ASan/libFuzzer 揭示过大的 vector::reserve；复审分别拦截基线的两个一亿计数用例，确认请求为约 13.6 GB / 7.2 GB。修复按父块和文件可容纳的最小结构数量限制计数。
- **发现时间：** 2026-10-25 约 04:00（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+19 / −0 行`
- **回归测试：** `test_dls_parser`
- **✦ 复审更新：** 不可信计数预分配的诊断与修复成立，但原“一亿项约 235 GB”数字不符。更正为对应两种结构的实测请求量；reserve 请求不等于同量物理内存已经占用，也不能仅凭 sanitizer 拒绝大分配断言机器已耗尽。

### F31 — CC 121 后气息同步的等待音符不会再启动

- **区域 / 类型：** MIDI 控制器 / Breath Sync / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 合法 CC 121；需启用气息同步单音模式
- **影响：** 控制器重置把 Breath MSB 设为 0，却保留内部旧值 127。下次气息从 0 恢复为正值时，状态机认为它一直有效，等待中的音符就不会触发。
- **触发条件：** 启用气息同步单音模式，先让音符在气息值为 127 时响起并释放；发送 CC 121 后按下新音符，再把气息从 0 提高到 127。
- **发现方法：** 基线回归在新气息值到达后仍没有活动音符；将上一气息值的清零移到两种控制器重置共用路径后，用例通过。
- **发现时间：** 2026-10-25 约 05:26（新加坡时间）
- **生产代码改动：** `fluid_chan.c：+1 / −1 行`
- **回归测试：** `test_synth_reset_cc`

### F32 — 控制器复位后，旧的定时音符会关闭新音符

- **区域 / 类型：** MIDI sequencer / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 合法 sequencer 事件；同声道同音高在时值内重叠
- **影响：** All Notes Off / All Sounds Off 会清掉重叠音符记录，却没移除队列里的延迟 Note Off。复位后同声道同音高再次发音时，旧的延迟关闭会落到新音符上，使它过早进入释放阶段。
- **触发条件：** 在声道 1 发送带时值的 Note；时值到期前对声道 0 或 1 发送 All Notes Off / All Sounds Off，再在声道 1 以相同音高发送新 Note 或 Note On。
- **发现方法：** 基线测试观察到新 voice 被旧 Note Off 标记；修复保留仍排队的关闭记录，并在同音高的新 Note On 成功后取消旧关闭。用例覆盖两个声道、两种复位和普通 Note On。
- **发现时间：** 2026-10-25 约 05:31–05:33（新加坡时间）
- **生产代码改动：** `fluid_seqbind.c：+11 / −3 行；fluid_seqbind_notes.cpp：−5 行；fluid_seqbind_notes.h：−1 行`
- **回归测试：** `test_seqbind_channel_all_notes_off`

### F33 — 优先级线程退出时用错释放方式

- **区域 / 类型：** 多核心合成线程 / 内存安全
- **严重度：** 3 / 10
- **输入场景：** 合法但较少见的多核心设置
- **影响：** 实时优先级线程的上下文用 C++ new 创建，线程退出时却交给 C free 释放。ASan 将其报告为分配器不匹配；在多核心合成设置下，合成器关闭时会走到这条路径。
- **触发条件：** 把 synth.cpu-cores 设为大于 1（audio.realtime-prio 默认大于 0），创建并关闭合成器。
- **发现方法：** ASan 在优先级线程回归用例中重现 operator new / free 不匹配；将上下文分配和释放统一到 FluidSynth 的分配器后，新增的 test_threading 检查通过。
- **发现时间：** 2026-10-25 约 05:38（新加坡时间）
- **生产代码改动：** `fluid_sys_cpp11.cpp：+6 / −2 行`
- **回归测试：** `test_threading`

### ✦ F42 — DLS 条件查询压栈错误，未知查询还会中止加载

- **区域 / 类型：** DLS-2 region 条件 / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 合法但较少见的 DLS-2 条件
- **影响：** DLS_CDL_QUERY 应只压入一个查询值，不应再压一个支持标志；未知 DLSID 按规范应压 FALSE，旧实现却抛异常，可能把应被条件跳过的 region 变成整库加载失败。
- **触发条件：** 比较 DLSID_ManufacturersID 返回值 0x4000，或在 region 条件中对未知 DLSID 执行 QUERY。
- **发现方法：** 基线 ManufacturersID 比较会丢失一个 voice；修复拆开 QUERY 与 QUERY_SUPPORTED。复审再构造未知 DLSID 条件，确认应压 0 并仅跳过该 region，现去掉未知 QUERY 的异常路径。回归验证两种查询语义及整库仍成功加载。此前 DLS 文件范围 Mull 78% 不是本轮单项修复分数。
- **发现时间：** 2026-10-25 约 07:42（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp（QUERY 单值压栈及未知查询返回 FALSE）`
- **回归测试：** `test_dls_parser`
- **✦ 复审更新：** 原多压栈诊断成立；复审发现同一条件执行器的未知 QUERY 仍违反 DLS §2.6，现一并补全。未知查询不是损坏的表达式，真正语法错误仍报错。

### F46 — 重复的 Sostenuto 踏板消息会把新音符也锁住

- **区域 / 类型：** MIDI CC 66 / Sostenuto / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 常规合法 MIDI 控制器消息
- **影响：** 代码每次收到 CC66 的按下值都会重设捕获音符的序号。若音符在第一次按下后开始、随后又收到一条仍表示按下的 CC66，它也会在 NOTE OFF 后被错误保留，直到踏板抬起才释放。
- **触发条件：** 先按下音符60，再发送 CC66=127；随后按下音符61，发送 CC66=64，再关闭音符61。
- **发现方法：** 从低覆盖、高圈复杂度的 CC 分支检查到踏板捕获序号会被重复前移。新增回归用公开 MIDI API 重现，并用 fluid_voice_is_sostenuto 检查：基线将音符61错误标为踏板保留；修复只在 CC66 从抬起变成按下时记录序号。未合并 fix 分支仅改了释放 Sostenuto 时与 Sustain 的交接，不涉及按下边缘。
- **发现时间：** 2026-10-25 约 08:42（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+8 / −7 行`
- **回归测试：** `test_synth_reset_cc`

### ✦ F47 — DLS articulation 条件为假时错误失败或泄漏参数

- **区域 / 类型：** DLS-2 articulation CDL / 格式解析
- **严重度：** 3 / 10
- **输入场景：** 合法但较少见的 DLS-2 条件程序
- **影响：** 条件结果为假时，应丢弃整个 articulation LIST。原代码把它当成加载错误；第一版只改 bypass 标志，仍会保留出现在条件前、已经写入共享 articulation 的参数，造成音色错误。
- **触发条件：** 在已有 articulation 后附加一份 LIST[lar2]，其中放有效的 CDL_CONST 0 和改变衰减的 art2；条件可以位于 art2 前面或后面。
- **发现方法：** 基线 false 条件导致 sfload 失败。复审仅交换 CDL 与 ART 顺序，第一版补丁在后置条件时仍使一个 voice 衰减增加 960。现先求值条件，再解析并写入参数，false 时整组跳过。回归比较 false 前置/后置时的实际 voice 参数、true 条件下参数生效、false 守卫的 ART 跳过及非法 CDL 拒绝。
- **发现时间：** 2026-10-25 约 08:48（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp（先求条件再提交参数，消除共享 articulation 的条件写入泄漏）`
- **回归测试：** `test_dls_parser`
- **✦ 复审更新：** 原加载失败诊断成立，但第一版未修整组参数应原子生效的根因。DLS §2.6 只推荐 CDL 置首，并未要求；现接受后置条件且确保其结果为假时不留下任何参数。

### F49 — 调音跨越音区后，单音连奏仍保留旧音区的声音

- **区域 / 类型：** 单音连奏 / 键盘调音 / 音频正确性
- **严重度：** 3 / 10
- **输入场景：** 合法但较少见的自定义调音与单音连奏组合
- **影响：** 单音连奏换音时，旧音符是否应继续响取决于新调音后的音高。代码却用原始 MIDI 键检查音区边界，造成旧音区声音未关、新音区声音又启动，两组采样同时播放。
- **触发条件：** 使用带 70–81 / 0–69 音区切分的音色，启用单音多次触发，将 key 71 调到 key 60，再触发 key 71。
- **发现方法：** 从高复杂度的单音连奏换音路径发现，它与新音符选区音色使用的音高不一致。基线播放后出现 4 个仍在发声的 voice，而预期只有 2 个；修复后用公共 MIDI API 回归在常规与 ASan / UBSan 构建中通过。未合并 fix 分支没有改动此音区判断。
- **发现时间：** 2026-10-25 约 09:00（新加坡时间）
- **生产代码改动：** `fluid_synth_monopoly.c：+18 / −1 行`
- **回归测试：** `test_monopoly_legato_tuning_split`

### F03 — DLS INFO 子块的长度能跨出父 LIST

- **区域 / 类型：** DLS 载入 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** 父 LIST 只有一小段内容，子 INAM 却把长度写到父块之外。解析器读到了后面的兄弟块，错误接受文件。
- **触发条件：** 在顶层追加一个很小的 LIST[INFO]，让其 INAM payload 声明长度跨进后面的有效 JUNK 块。
- **发现方法：** 基线把跨父块的文件成功加载；新增测试先复现接受行为，再用每层剩余字节数检查拒绝越界子块。
- **发现时间：** 2026-10-25 约 02:28（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+57 / −4 行（共享 chunk 边界检查）`
- **回归测试：** `test_dls_parser`

### F04 — SDL 尚未启动时，每次建 settings 都漏一个锁

- **区域 / 类型：** SDL3 音频驱动 / 资源泄漏
- **严重度：** 2 / 10
- **输入场景：** 常规合法初始化顺序
- **影响：** SDL 音频还没初始化，驱动设置函数已经创建了一个互斥锁，然后直接返回。每次新建 settings 都会留下锁。
- **触发条件：** 使用 SDL3 构建，在调用 SDL_Init 前创建一份或多份 FluidSynth settings。
- **发现方法：** LSan 在 DLS 测试创建的两个 settings 上报告 80 字节泄漏；修复删掉了这条不必要的锁分配路径。
- **发现时间：** 2026-10-25 约 02:32（新加坡时间）
- **生产代码改动：** `fluid_sdl3.c：+23 / −33 行（与下一项共用改动）`
- **回归测试：** `test_sdl3_settings_init`

### F05 — SDL 已启动时，设备枚举结果没有释放

- **区域 / 类型：** SDL3 音频驱动 / 资源泄漏
- **严重度：** 2 / 10
- **输入场景：** 合法但较少见的初始化顺序
- **影响：** SDL 已运行并成功列出播放设备后，临时设备数组、每个名字和列表结构都没被回收；重复创建 settings 会逐次泄漏。
- **触发条件：** 先初始化 SDL 音频，再注册 SDL3 音频驱动或创建 settings，让它枚举播放设备。
- **发现方法：** 用 SDL dummy audio driver 枚举设备并运行 LSan；基线报告 140 字节直接和间接泄漏，修复统一清理列表和名字。
- **发现时间：** 2026-10-25 约 02:35（新加坡时间）
- **生产代码改动：** `fluid_sdl3.c：+23 / −33 行（与上一项共用改动）`
- **回归测试：** `test_sdl3_settings_init`

### ✦ F06 — SoundFont NRPN 选择器累加很多次后会绕回到有效生成器

- **区域 / 类型：** MIDI 控制器 / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 合法控制器值，但序列非常长
- **影响：** NRPN 选择器不断累加，没有上限。大约 429,570 次选择器控制消息后，整数会绕回到有效生成器 8，后面的 Data Entry 会改错声音参数。
- **触发条件：** 连续发送 MSB=120 的 SoundFont NRPN selector 消息，再发送 Data Entry。
- **发现方法：** 构造超过 32 位回绕点的公开 CC 序列；基线改变了不应改变的滤波 cutoff，修复把选择器封顶在有效范围。
- **发现时间：** 2026-10-25 约 02:39（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+15 / −4 行`
- **回归测试：** `test_nrpn_selector_wrap`
- **✦ 复审更新：** 溢出与修复成立，但原报告把协议名称写错：此路径是 MSB=120 的 SoundFont NRPN；AWE32 使用另一个 MSB=127 分支。

### F07 — DLS wsmp 的大小相加会绕回去，读到旁边的数据

- **区域 / 类型：** DLS 载入 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** 样本参数块把自己的头大小和循环块大小写成极大值；32 位相加绕回一个很小的数，解析器就把下一个兄弟块当成循环数据。
- **触发条件：** 在 wsmp 中组合溢出的 cbSize 与 loop cbSize，并在后面放一个有效 data 块。
- **发现方法：** 手工变异随库 DLS 后，基线仍接受了跨块数据；ASan / UBSan 定点用例验证修复拒绝块外读数。
- **发现时间：** 2026-10-25 约 02:42（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+35 / −4 行（wsmp 相关）`
- **回归测试：** `test_dls_parser`

### F11 — DLS fmt 子块太短，解析器仍从后面拿格式字段

- **区域 / 类型：** DLS 载入 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** fmt 声明长度为 0，但解析器仍读取 WAVEFORMAT 字段；后面的样本数据可能被误当成格式字段，坏文件会被接受。
- **触发条件：** 把 DLS wave 的 fmt 子块改成零长度，并构造足够像字段的后续字节。
- **发现方法：** 基线加载了变异 DLS；解析器现在在读取 WAVEFORMAT 字段前要求至少 16 字节。
- **发现时间：** 2026-10-25 约 02:53（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+5 / −0 行`
- **回归测试：** `test_dls_parser`

### F15 — GS / XG SysEx 地址含高位时触发有符号移位 UB

- **区域 / 类型：** SysEx 解析 / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 不合规 MIDI SysEx 字节
- **影响：** 地址字节应是 7 位值。高位字节经过有符号 char 移位会触发未定义行为，导致不同构建结果不一致。
- **触发条件：** 在 Roland GS 或 Yamaha XG SysEx 地址字段放入大于 127 的字节。
- **发现方法：** 用 UBSan 逐个发送高位地址；修复在地址解码前拒绝非 7 位字节。
- **发现时间：** 2026-10-25 约 03:17（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+10 / −0 行`
- **回归测试：** `test_synth_sysex_address_high_bit`

### F20 — DLS unity note 大于 127 后会越过调音表

- **区域 / 类型：** DLS 载入 / 播放 / 内存安全
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** DLS 样本 unity note 被写成 255，弹音时这个根音会进入 128 项的调音表下标，造成越界读取。
- **触发条件：** 把 DLS 的 wsmp unity_note 改成 255，启用自定义音律并播放样本。
- **发现方法：** 基线 ASan / UBSan 重现 voice pitch 路径越界；加载器现在拒绝超过 MIDI 音符范围的 unity note。
- **发现时间：** 2026-10-25 约 03:50（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+5 / −0 行`
- **回归测试：** `test_dls_parser`

### F22 — DLS 的 alias 数量超过 PGAL 块长度

- **区域 / 类型：** DLS 载入 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** PGAL 声明有一个别名，但块里没有对应记录。解析器继续读后面的 lins 数据，把它当成别名并接受坏文件。
- **触发条件：** 插入仅含头部的 PGAL 块，同时将 nAlias 设为 1。
- **发现方法：** 基线仍接受这份 DLS；新增用例复现后，修复按 PGAL 剩余字节数验证 alias 数量。
- **发现时间：** 2026-10-25 约 04:07（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+7 / −0 行`
- **回归测试：** `test_dls_parser`

### F23 — 零长度 RGNH 会把相邻子块读成范围字段

- **区域 / 类型：** DLS 载入 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** Region header 子块声明没有数据，但解析器仍读固定的 12 字节，拿相邻 chunk 的头部填进 key / velocity 范围；文件仍有机会被接受。
- **触发条件：** 将 RGNH 长度设为 0，在其数据处放置一个能保持后续 WLNK 对齐的伪造子块。
- **发现方法：** 基线接受构造的 DLS；修复在读取 RGNH 与 WLNK 固定字段前检查各自至少有 12 字节。
- **发现时间：** 2026-10-25 约 04:13（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：+10 / −0 行（RGNH 与 WLNK）`
- **回归测试：** `test_dls_parser`

### F24 — MIDI 事件可以读出声明轨道之外的字节

- **区域 / 类型：** MIDI 文件解析 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 MIDI 文件
- **影响：** MTrk 长度写得比事件短时，解析器仍继续读后面的字节，并把轨道外的 Note On 加入播放列表。
- **触发条件：** 把轨道长度声明为 1 字节，实际在后面放完整的 Note On；另一个用例让 Meta 事件载荷跨出轨道边界。
- **发现方法：** 基线把轨道外事件加入轨道；新增测试分别覆盖逐字节消息和批量 Meta 载荷读取，修复在每次读取前核对剩余轨道字节。
- **发现时间：** 2026-10-25 约 04:26（新加坡时间）
- **生产代码改动：** `fluid_midi.c：+20 / −2 行`
- **回归测试：** `test_midi_file_track_bounds`

### F27 — 超大 MIDI 轨道长度被读成负数，坏文件被当成空轨道

- **区域 / 类型：** MIDI 文件解析 / 格式解析
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 MIDI 文件
- **影响：** 轨道长度字段是无符号 32 位数。高位长度原先会变成负数，于是小文件里的轨道没有任何数据也会被接受。
- **触发条件：** 在 MTrk 头写入长度 0x80000000，却不给文件添加轨道数据。
- **发现方法：** 新回归用例在基线解析器中触发 track overrun 并确认坏文件被加入播放列表；修复使用无符号解码，并要求声明长度不超过文件剩余字节。
- **发现时间：** 2026-10-25 约 04:44（新加坡时间）
- **生产代码改动：** `fluid_midi.c：+19 / −8 行`
- **回归测试：** `test_midi_file_track_bounds`

### F28 — 高位 GS SysEx 节奏参数被误认成鼓声道设置

- **区域 / 类型：** SysEx 解析 / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 SysEx 数据
- **影响：** GS Rhythm Part 参数只能用 7 位值。高位字节在 signed char 构建上会变成负数，旧判断仍把它当成有效鼓声道设置。
- **触发条件：** 先进入 GS 模式，再发送带有效校验码的 Rhythm Part 消息，并把参数值设为 0xFF。
- **发现方法：** 基线把 MIDI 声道 9 从 melodic 改为 drum 并返回成功；新增用例验证非法数据被拒绝，声道类型保持不变。
- **发现时间：** 2026-10-25 约 04:50（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+8 / −0 行`
- **回归测试：** `test_synth_sysex_address_high_bit`

### ✦ F29 — 超长 GS SysEx 校验和会让有符号累加溢出

- **区域 / 类型：** SysEx 解析 / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 不合规的超长 GS DT1 / 公开 API 消息
- **影响：** 校验和累加器是有符号整数。一个约 17 MB、数据字节都合法的消息就能让累加值超过 int 上限，触发未定义行为。
- **触发条件：** 通过 SysEx API 发送约 17 MB 的 GS DT1 形状消息，数据字节均为 7 位且校验和一致。
- **发现方法：** 用有效 7 位数据构造 17 MB 消息；UBSan 基线报告有符号加法溢出。修复在每个字节后按 128 取模，校验结果保持一致。
- **发现时间：** 2026-10-25 约 04:52（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+2 / −2 行`
- **回归测试：** `test_synth_sysex_address_high_bit`
- **✦ 复审更新：** 有符号累加溢出与逐字节模 128 的修复成立，但原“合法超长 GS”分类错误。Roland M-GS64 文档要求 DT1 数据超过 128 字节分包；7 位字节和有效校验和不足以证明整条消息合规。保持 2 分。

### ✦ F34 — 极端生成器数值在音高、力度和采样换算时溢出

- **区域 / 类型：** SoundFont generator API / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 不合规的 API 数值
- **影响：** 生成器 API 接受远超 SoundFont 范围的有限值。极大音高会在 SF2 / DLS 样本区选择时溢出整数转换；极大固定力度会溢出另一个转换，之后还可能使调制查表越界崩溃。
- **触发条件：** 把 GEN_FINETUNE 或 GEN_VELOCITY 设成 FLT_MAX 后弹音；把采样偏移或 GEN_PITCH 设成极值也会进入相关数值路径。
- **发现方法：** 基线 fuzz 分别触发样本区选择、固定力度及采样地址转换的 UB / 越界。原修复限制选区 key、力度和地址换算，并拒绝 voice API 非有限值；复审补上 GEN_OVERRIDEROOTKEY=200 / FLT_MAX 仍能绕过文件校验的同类遗漏，在 root_key 形成处统一约束后再转换。
- **发现时间：** 2026-10-25 约 06:01、06:30；新路径约 07:16–07:21（新加坡时间）
- **生产代码改动：** `fluid_synth.c / fluid_conv.c / fluid_voice.c / fluid_defsfont.c / fluid_dls.cpp（数值转换前约束，含 root_key 形成点）`
- **回归测试：** `test_sfont_gen_offset_bounds；test_voice_generator_bounds`
- **✦ 复审更新：** 数值 UB 诊断成立，原测试覆盖的各条路径确有修复。根音 voice API 属于同一转换/索引不变量的遗漏，现补齐；这些用例仍不构成所有 generator 组合均安全的证明。

### ✦ F35 — 高速 MIDI 与累计长 tick 暴露播放器计数范围不足

- **区域 / 类型：** MIDI player / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 合法但罕见的高速 tempo
- **影响：** 播放器原把累计 tick 直接转 int，极快合法 tempo 可在约 70 毫秒越界。原补丁只封顶 INT_MAX，使总 tick 超过此值的合法曲目后续事件和 EOT 永远无法到达；忽略的 Meta 事件累计 delta 也可溢出。
- **触发条件：** 使用 division=32767、外部 MIDI tempo=1；连续多个最大合法 delta 的事件或被忽略 Marker，可在很短实际时长内使累计 tick 超过 INT_MAX 乃至 UINT_MAX。
- **发现方法：** 基线 UBSan 重现 double→int 越界；复审构造 INT_MAX 之后仍有事件和 EOT 的曲目，证实封顶版卡在 PLAYING。现将 parser 累计 delta、event delay、track/player tick 统一为 uint64_t，内部播放持续推进，仅公开 int getter / tick callback 饱和。回归以 17 个 0x0FFFFFFF delta 覆盖普通事件、被跳过 Marker、末事件、EOT 及最终 DONE。
- **发现时间：** 2026-10-25 约 06:39（新加坡时间）
- **生产代码改动：** `fluid_midi.c / fluid_midi.h（内部 64 位累计计数；公开 int 位置饱和并说明契约）`
- **回归测试：** `test_midi_file_timesig_exponent`
- **✦ 复审更新：** 原转换溢出诊断成立，但封顶只压住 UB、未修计数范围根因，且引入无限等待。现扩大内部时间表示，保留公开 int API 的可表示范围，并验证完整播放终态。

### F36 — NaN 混响或合唱参数会污染后续音频

- **区域 / 类型：** 效果器 API / 数值设置 / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 不合规 API 参数或数值设置
- **影响：** 范围比较会让 NaN 绕过上下限检查。混响或合唱接受这个值后，之后渲染出的浮点音频会变成 NaN。
- **触发条件：** 调用 chorus / reverb 参数 API 传入 NaN，或尝试把 synth.chorus.level 设为 NaN。
- **发现方法：** 基线 API 接受 NaN；加载 SF2、播放音符后渲染 512 帧，两个声道的 1,024 个样本都变为 NaN。修复在数值设置和效果参数入队前拒绝非有限值；回归覆盖分组与旧版效果 API，ASan / UBSan / LSan 通过。
- **发现时间：** 2026-10-25 约 06:52（新加坡时间）
- **生产代码改动：** `fluid_settings.c / fluid_synth.c：+20 / −0 行`
- **回归测试：** `test_effect_nonfinite_controls`

### F37 — 非法时值音符会取消另一音符的关闭消息

- **区域 / 类型：** MIDI sequencer / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 不合规的 sequencer API 参数
- **影响：** 时值音符用声道和音高计算内部 ID。声道 1、音高 -68 与声道 0、音高 60 得到同一个 ID；旧代码会因此删掉合法音符已排队的 NOTE OFF，让它继续响。极端声道值还会让 ID 计算发生有符号整数溢出。
- **触发条件：** 先在声道 0 播放音高 60 的时值音符，再发送声道 1、音高 -68 的非法时值音符；第二个重现使用声道 33,554,432、音高 61，与声道 0 的音高 61 冲突。
- **发现方法：** 基线用例在处理原 NOTE OFF 后仍观察到音符没有收到关闭；对声道 33,554,432 启用 UBSan 则报告 128 × channel 的有符号溢出。修复在计算 ID 前检查声道和音高范围；回归同时覆盖 ID 冲突与极端声道，ASan / UBSan / LSan 通过。
- **发现时间：** 2026-10-25 约 07:03（新加坡时间）
- **生产代码改动：** `fluid_seqbind.c：+9 / −2 行`
- **回归测试：** `test_seqbind_note_bounds`

### F38 — NaN 合成器总增益会让后续音频变成 NaN

- **区域 / 类型：** 合成器增益 API / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 不合规的 API 浮点值
- **影响：** 总增益用上下限比较来夹值，但 NaN 与上下限比较都会为假，于是 NaN 被直接保存并传给活动音符；后续浮点输出可能全部变成 NaN。
- **触发条件：** 让合成器正在播放音符时调用 fluid_synth_set_gain(synth, NAN)，然后继续渲染。
- **发现方法：** 基线将合成器增益读取为 NaN，渲染检查也发现非有限样本。修复在增益钳位前忽略非有限输入；ASan / UBSan / LSan 回归检查增益调用后输出仍有限。
- **发现时间：** 2026-10-25 约 07:10（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+1 / −0 行`
- **回归测试：** `test_effect_nonfinite_controls`

### F39 — 公开 voice API 接受越界 generator 下标

- **区域 / 类型：** Voice API / 内存安全
- **严重度：** 2 / 10
- **输入场景：** 不合规的 API 下标
- **影响：** voice generator 的 get / set / increment 和参数更新函数直接按整数下标访问数组。传入 GEN_LAST、负数或很大的值会越界读写相邻 voice 状态。
- **触发条件：** 取得一个活动 voice 后，用 GEN_LAST 或 INT_MAX 调用 fluid_voice_gen_get、fluid_voice_gen_set、fluid_voice_gen_incr 或 fluid_voice_update_param。
- **发现方法：** 基线 ASan / UBSan 在 generator 数组访问处报告越界；普通测试也能观察到 GEN_LAST 写坏紧邻的 callback 字段。修复在公开入口检查 voice 和 generator 范围。
- **发现时间：** 2026-10-25 约 07:18（新加坡时间）
- **生产代码改动：** `fluid_voice.c：+20 / −2 行`
- **回归测试：** `test_voice_generator_bounds`

### F40 — 极端排他类别值导致浮点转整数溢出

- **区域 / 类型：** SoundFont generator API / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 不合规的 API 浮点值
- **影响：** GEN_EXCLUSIVECLASS 接受有限但超出整数范围的值。下一个音符开始时，代码把它直接转成 int，极大或极小值会触发未定义行为。
- **触发条件：** 先播放音符，再把 GEN_EXCLUSIVECLASS 设成 -FLT_MAX 或 FLT_MAX，并触发第二个音符。
- **发现方法：** 状态化公开 API fuzz 在 11,175 个输入后触发 fluid_synth.c:5509；回归用例在基线 UBSan 下重现浮点转整数溢出。修复在转换前检查有限值和 int 范围。
- **发现时间：** 2026-10-25 约 07:27（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+10 / −1 行`
- **回归测试：** `test_sfont_gen_offset_bounds`

### ✦ F41 — 极低音高让插值步长变成零并触发除零

- **区域 / 类型：** 音频插值 / 未定义行为
- **严重度：** 2 / 10
- **输入场景：** 不合规的 API 浮点值
- **影响：** 极低 FINETUNE 使定点插值步长归零，原边界计算因此除零。第一版零步长补丁还把播放头恰好等于 end+1 的情况当作可继续渲染，读过有效采样结束点。
- **触发条件：** 先播放音符，再调用 fluid_synth_set_gen(synth, 0, GEN_FINETUNE, -194642.0f)，随后渲染一块音频。
- **发现方法：** 保留公开 generator API 的除零回归；复审再按现有内部插值夹具先正常渲染完整 64 点样本，使 phase 到达 64，再将步长设为 0，ASan 重现 data[64] 越界。零步长现在按 phase>=boundary 返回 0，只有仍在有效区间内才填当前缓冲；另测最后有效采样点可停留渲染。
- **发现时间：** 2026-10-25 约 07:31（新加坡时间）
- **生产代码改动：** `fluid_rvoice_dsp.cpp（零步长处理包含等于结束边界的情况）`
- **回归测试：** `test_sfont_gen_offset_bounds；test_rvoice_dsp_interpolate`
- **✦ 复审更新：** 原除零诊断成立；第一版补丁新增等号边界错误。现补全零步长与正步长一致的结束边界语义。越界反例使用内部插值夹具，未把它夸大为已完成同等端到端公开 API 崩溃复现。

### F43 — 非有限调音值会让 MIDI 音符无声

- **区域 / 类型：** Tuning API / 音频正确性
- **严重度：** 2 / 10
- **输入场景：** 不合规 API 浮点值
- **影响：** 调音 API 接受 NaN。激活该音律后，按键音高变成非有限值，音区范围判断会跳过所有样本；NOTE ON 仍返回成功，但没有 voice 开始，听到的是静音。
- **触发条件：** 用 fluid_synth_tune_notes 把 key 60 的音高设成 NaN，激活该 tuning，再发送 key 60 的 NOTE ON。
- **发现方法：** 基线 tune_notes 接受 NaN；加载 VintageDreams SF2 后，NOTE ON 返回成功而 active voice count 为 0。新增回归检查 key、octave 和单音调音三种 API 都拒绝非有限输入；ASan / UBSan / LSan 通过。
- **发现时间：** 2026-10-25 约 07:49（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+19 / −0 行`
- **回归测试：** `test_tuning_nonfinite_input`

### F44 — 循环关闭后，插值仍读取失效的循环边界

- **区域 / 类型：** 采样插值 / voice 状态 / 内存安全
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 generator API 值
- **影响：** 对活动音符反复设置极大的负 GEN_ENDLOOPADDRCOARSEOFS，会把循环区域推到无效位置。样本检查因此关闭循环，却保留了“已进入循环”的标记；四阶插值随后把 loopend−1 当成有效样本地址，越界读取并可能崩溃。
- **触发条件：** 播放一个 SF2 循环样本，渲染 10 块后，连续 3 次把该 generator 设为 −65536 并各渲染一块。
- **发现方法：** 在干净的 upstream/master 基线 ASan 构建中，新增公开 API 回归用例在 fluid_rvoice_get_sample16 处触发越界读崩溃；修复清除已经失效的 looped 标记后，常规和 ASan 回归通过。已检查未合并 fix 分支；只有无关的短循环插值及注释改动。
- **发现时间：** 2026-10-25 约 08:00（新加坡时间）
- **生产代码改动：** `fluid_rvoice.c：+8 / −0 行`
- **回归测试：** `test_rvoice_loop_guard_reset`

### F45 — SDL3 音频设备枚举失败时释放未初始化的名字指针

- **区域 / 类型：** SDL3 音频初始化 / 内存安全
- **严重度：** 2 / 10
- **输入场景：** 合法但罕见的设备枚举内存不足
- **影响：** 枚举多个播放设备时，若后续设备名称分配失败，清理代码会释放尚未初始化的数组项，而不是前面已经分配的名称；设备驱动创建可能因此崩溃，临时数组和名称也会泄漏。
- **触发条件：** 启用 SDL3 音频驱动并枚举至少两个播放设备，让第二个名称格式化分配失败。
- **发现方法：** Linux 回归用例替换 SDL 名称分配和设备枚举。干净基线在 GetNumAudioDevices 清理时由 ASan 报告无效地址读取；修复逐个释放已创建的名称并释放临时数组，ASan / LSan 用例通过。未合并 fix 分支没有改动此 SDL3 清理路径。
- **发现时间：** 2026-10-25 约 08:29（新加坡时间）
- **生产代码改动：** `fluid_sdl3.c：+2 / −1 行`
- **回归测试：** `test_sdl3_asprintf_oom`

### F50 — 短小 MIDI 文件可诱发巨额元事件分配

- **区域 / 类型：** Standard MIDI File 解析 / 资源耗尽
- **严重度：** 2 / 10
- **输入场景：** 构造的不合规 Standard MIDI File
- **影响：** 元事件声明的数据长度可以远大于它所在的 MTrk。解析器虽最终会拒绝文件，却先按未验证的长度申请内存；一个仅几十字节的文件即可申请约 256 MiB，内存紧张时可能拖垮播放进程。
- **触发条件：** 在 7 字节的 MTrk 元事件中把数据长度写成 0x0FFFFFFF，却不提供相应内容。
- **发现方法：** 在高复杂度的 MIDI 事件解析分支加入分配监测回归。干净基线确实为几十字节的文件请求 268,435,456 字节，然后因轨道越界而拒绝；修复先验证剩余轨道和文件长度，ASan 回归确认解析时不再尝试大块分配。未合并 SysEx 修复只约束 SysEx 数据，不包含 Meta 事件分配。
- **发现时间：** 2026-10-25 约 09:08（新加坡时间）
- **生产代码改动：** `fluid_midi.c：+7 / −0 行`
- **回归测试：** `test_midi_file_meta_size_bounds`

### ✦ F10 — 调音名称缓冲区够长，最后一个字还是被截掉

- **区域 / 类型：** Tuning API / API 边界
- **严重度：** 1 / 10
- **输入场景：** 常规合法 API 使用
- **影响：** snprintf 的容量少传了 1。名称正好填满缓冲区时会少一个字符，虽然缓冲区本来能完整容纳它。
- **触发条件：** 写入 16 个字符的名称，调用者提供 17 字节缓冲区。
- **发现方法：** 用精确大小的缓冲区回归；基线稳定少最后一个字符，改用完整容量后字符串仍以 NUL 结尾。
- **发现时间：** 2026-10-25 约 02:50（新加坡时间）
- **生产代码改动：** `fluid_synth.c：+1 / −1 行`
- **回归测试：** `test_tuning_dump_zero_name_length`
- **✦ 复审更新：** 容量少传 1 的诊断与修复成立。影响是返回名称被截断，不影响音符播放；类型改为 API 边界，排序分由 3 调为 1。

### F25 — 时间签名分母指数过大时发生浮点转整数未定义行为

- **区域 / 类型：** MIDI 文件解析 / 未定义行为
- **严重度：** 1 / 10
- **输入场景：** 合法但罕见的时间签名字节值
- **影响：** 时间签名的分母指数是一个字节。指数为 255 时，2 的 255 次方超出 int 范围；原代码只为日志输出转换整数，触发未定义行为，不影响音符播放。
- **触发条件：** MIDI 文件包含时间签名 Meta 事件 FF 58 04，分母指数字节为 255。
- **发现方法：** 显式启用 float-cast-overflow sanitizer 后，基线报告越界 double-to-int 转换；修复后直接以 2^255 形式记录日志。
- **发现时间：** 2026-10-25 约 04:29（新加坡时间）
- **生产代码改动：** `fluid_midi.c：+17 / −5 行`
- **回归测试：** `test_midi_file_timesig_exponent`

### F48 — Meta 事件后错误沿用已取消的 MIDI 运行状态

- **区域 / 类型：** Standard MIDI File parser / 格式解析
- **严重度：** 1 / 10
- **输入场景：** 构造的不合规 Standard MIDI File
- **影响：** SMF 规定 Meta 和 SysEx 事件会取消通道运行状态。解析器保留了 Meta 之前的 Note On 状态，因此会把后续缺少状态字节的数据误读成另一个音符，而不是拒绝这段不合规事件。
- **触发条件：** 在显式 Note On 后放一个零长度 Marker Meta 事件，再用省略状态字节的音符数据开始下一个事件。
- **发现方法：** 对照 MIDI Association 的 Standard MIDI Files 规则构造文件；基线 player 接受并按旧 Note On 状态解析了额外事件。修复在 Meta / SysEx 和新轨道开始时清除运行状态；新增 test_midi_file_track_bounds 用例在常规和 ASan / UBSan 构建中通过。未合并 fix 分支没有修改运行状态处理。
- **发现时间：** 2026-10-25 约 08:54（新加坡时间）
- **生产代码改动：** `fluid_midi.c：+6 / −0 行`
- **回归测试：** `test_midi_file_track_bounds`

### ✦ F51 — 无效 DLS 采样偏移让解析器先预留 36 MB

- **区域 / 类型：** DLS 载入 / 资源消耗 / 资源耗尽
- **严重度：** 1 / 10
- **输入场景：** 构造的不合规 DLS
- **影响：** 解析器按 PTBL 中声明的采样数，提前为所有 sample 记录分配内存。即使第一个采样偏移就是无效值，约 2.6 MB 的文件仍会先申请约 36 MB。
- **触发条件：** 构造 500,000 条格式长度自洽但指向无效位置的 pool cue；解析器在检查第一个偏移前预留整个 sample 数组。
- **发现方法：** 新回归临时记录分配请求。干净基线在拒绝第一个无效偏移前请求 36,000,000 字节；去掉按不可信 cue 数预留的操作后，测试确认最大单次申请低于 8 MiB，文件仍被拒绝，ASan / UBSan / LSan 通过。检查过的未合并修复分支没有改动这条 DLS 路径。
- **发现时间：** 2026-10-25 约 09:20（新加坡时间）
- **生产代码改动：** `fluid_dls.cpp：−1 行（移除过早 reserve）`
- **回归测试：** `test_dls_ptbl_allocation`
- **✦ 复审更新：** 这项是无效首偏移被拒绝前的过早预留；删除 reserve 正好消除该额外分配，1 分合理。它不提供通用 DLS 内存配额，不能外推为重复有效 cue、逐个解码或所有资源耗尽路径均已解决。

### ✦ F26 — 基线已处理实时 MIDI 插入，撤回此项基线缺陷归因

- **区域 / 类型：** 实时 MIDI 输入 / 音频正确性
- **严重度：** 撤回，不计分（声明基线已有正确行为）
- **输入场景：** 常规合法 MIDI 消息顺序
- **影响：** 报告声明的 c7ba7b49 基线已在 F8–FE 到达时保留待解析状态，仅 FF System Reset 清状态。本项不能计入该基线的新缺陷数量。
- **触发条件：** 发送 Note On 状态和音高，插入 MIDI Clock 后再发送力度；此序列在声明基线正确解析。
- **发现方法：** 从 git show c7ba7b49:src/midi/fluid_midi.c 单独编译解析器，与 test_midi_parser_realtime 链接运行，测试通过。当前 HEAD a1e76704 曾另行回退此行为；工作区保留对该后续回归的修复和测试。
- **发现时间：** 2026-10-25 约 04:33（新加坡时间）
- **生产代码改动：** `fluid_midi.c（相对 c7ba7b49 无新增行为修复；相对当前 HEAD 恢复实时消息语义）`
- **回归测试：** `test_midi_parser_realtime`
- **✦ 复审更新：** 明确撤回“现有实时消息测试在声明基线失败”的原结论。保留编号作为更正记录，不计入 50 项成立缺陷；当前 HEAD 的后续回归确实需要工作区中的恢复。

## 参考资料

- [SoundFont 2.04 技术规格](https://musescore.org/sites/musescore.org/files/2023-01/sfspec24.pdf)：包括非音高样本根音与固定力度生成器规则。
- [MIDI Association：Standard MIDI Files](https://midi.org/standard-midi-files-specification)
- [MIDI Association：DLS Level 1](https://midi.org/dls-level-1)
- [MIDI Association：DLS Level 2](https://midi.org/dls-level-2)
- [MIDI Association：MIDI System Real-Time 消息](https://midi.org/about-midi-part-3midi-messages)

“修复行数”只统计对应生产代码差异，不把回归测试代码计入；同一测试文件可能覆盖多项发现。

- [MIDI Tuning Messages 原规范镜像](https://musescore.org/sites/musescore.org/files/2018-06/midituning.pdf)：7F 7F 7F 是 no change，最大有效频率字为 7F 7F 7E。
- [MIDI Association：MIDI Tuning Updated Specification](https://midi.org/midi-tuning-updated-specification)。
- [Roland M-GS64 官方手册](https://cdn.roland.com/assets/media/pdf/M-GS64_OM.pdf)：DT1 长数据需分包。
