libxml2 从字符串解析 DTD 时为何在 ATTLIST 附近报错?
结论
建议优先排查当前 libxml2 版本对静态输入缓冲区的处理。label CDATA #IMPLIED 是合法的属性声明;错误发生在 DTD 解析阶段,此时还没有进入 xmlValidateDtd 的 XML 验证阶段。
可以先将 xmlParserInputBufferCreateStatic 换成会复制输入的 xmlParserInputBufferCreateMem,保留原来的 DTD、XML 和编码参数进行对照。如果错误消失,就可以进一步排查静态缓冲区相关路径。这是一种候选绕过方案,仍需在实际环境中验证,不能仅凭 Ubuntu 24.04 就认定具体缺陷或修复版本。
为什么错误会指向 ATTLIST?
报错 ContentDecl : ',' '|' or ')' expected 表示解析器当时仍在按元素内容模型读取字符,例如:
<!ELEMENT processor (interface)*>
但它遇到的是后面属性声明中的 label CDATA #IMPLIED。这与解析位置异常的现象相符,但仅凭错误文本,还无法确认内部指针在哪里发生了偏移。
删除任意一组 ATTLIST 会改变输入长度及后续内容的位置,可能使缓冲区缺陷暂时不再触发。这并不能证明被删除的声明存在语法错误。
原代码中的 dtdstr 指向字符串字面量,在整个解析过程中都有效,符合静态缓冲区对输入生命周期的要求。CreateStatic 不复制输入,CreateMem 会复制输入,因此可以用二者比较不同的处理路径。参见 libxml2 输入缓冲区文档。
先确认程序实际使用的库
在原程序中临时输出编译期和运行期版本:
#include <libxml/xmlversion.h>
std::cout << "Headers: " << LIBXML_DOTTED_VERSION << '\n';
std::cout << "Runtime: " << xmlParserVersion << '\n';
Ubuntu 上还可以检查程序和命令行工具的动态库依赖:
ldd ./a.out
ldd "$(command -v xmlstarlet)"
比较它们加载的 libxml2.so 路径。即使二者加载同一个库,文件输入和静态内存输入也可能走不同的缓冲区路径,所以 xmlstarlet 成功不能排除库内部问题。
用复制缓冲区进行对照
保留问题中的 dtdstr 和 xmlstr 定义,增加以下头文件,再用下面的 main 替换原来的实现。示例保留原来的 ASCII 参数,以便先只比较缓冲区创建方式。
#include <libxml/xmlIO.h>
#include <libxml/valid.h>
#include <limits>
int main()
{
const std::size_t length = std::strlen(dtdstr);
if (length > static_cast<std::size_t>(
std::numeric_limits<int>::max())) {
std::cerr << "DTD is too large for this API\n";
return 1;
}
xmlDocPtr doc = xmlParseDoc(
reinterpret_cast<const xmlChar*>(xmlstr));
if (doc == nullptr) {
std::cerr << "XML parsing failed\n";
return 2;
}
xmlParserInputBufferPtr input = xmlParserInputBufferCreateMem(
dtdstr,
static_cast<int>(length),
XML_CHAR_ENCODING_NONE);
if (input == nullptr) {
xmlFreeDoc(doc);
return 3;
}
// xmlIOParseDTD takes ownership of input.
xmlDtdPtr dtd = xmlIOParseDTD(
nullptr, input, XML_CHAR_ENCODING_ASCII);
if (dtd == nullptr) {
std::cerr << "DTD parsing failed\n";
xmlFreeDoc(doc);
return 4;
}
xmlValidCtxt context {};
const int result = xmlValidateDtd(&context, doc, dtd);
std::cout << (result == 1 ? "valid\n" : "invalid\n");
xmlFreeDtd(dtd);
xmlFreeDoc(doc);
return result == 1 ? 0 : 5;
}
传给长度参数的是 DTD 的实际字节数,不包括末尾的 \0。原来的 std::strlen(dtdstr) 在这点上没有问题。
xmlIOParseDTD 会接管并释放输入缓冲区,调用后不要再对同一个缓冲区执行 xmlFreeParserInputBuffer。返回的独立 DTD 和 XML 文档需要分别释放。参见 xmlIOParseDTD 的所有权说明。
如何判断后续修复方向?
如果 CreateStatic 失败而 CreateMem 成功,可以暂时采用复制缓冲区,并用原始复现程序验证包含相关修复的 libxml2 构建。升级后还要确认运行时确实加载了新库;只替换头文件不会改变旧动态库中的行为。
如果两种方式都失败,应继续比较保存到文件的实际字节、运行时库路径和完整错误信息。当前材料没有提供 libxml2 的精确版本,也没有实际运行对照结果,因此无法保证上述替换在该环境中一定成功,也无法指定未经核实的最低修复版本。
这份输入全部是 ASCII 字符,不能仅凭 XML_CHAR_ENCODING_NONE 与 XML_CHAR_ENCODING_ASCII 的组合就断定存在编码错误。后续如果嵌入中文等非 ASCII 内容,应按真实字节编码设置参数并单独验证,避免同时改变多个条件。
备注:内容仅供参考。
