PAYATHON 2026

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 内容,应按真实字节编码设置参数并单独验证,避免同时改变多个条件。

备注:内容仅供参考。