DHLibxls/DHxlsReader libxls PSXLSReader 溢出闪退和数据错乱问题

| /

最近我们的 iOS APP 遇到较多闪退的问题,崩溃在 xls 解析相关代码,经过分析应该是使用的解析库 DHLibxls/DHxlsReader 内部的问题。

初步分析

先看崩溃栈,部分信息如下

1
2
3
4
5
6
7
8
SIGSEGV
SEGV_ACCERR

xls_getfcell (xlstool.c:639)
xls_addCell (xls.c:512)
xls_parseWorkSheet (xls.c:1145)
-[DHxlsReader openSheet:] (DHxlsReader.m:150)
-[DHxlsReader numberOfColsInSheet:] (DHxlsReader.m:0)

可以看出是用来解析 xls 的库 DHlibxls/DHxlsReader 发生了闪退,而闪退的位置实际位于其使用的 libxls 库。

留意 DHlibxls/DHxlsReader 用的 libxls 并不是我去查信息时的master分支,当然最新的master分支也有问题,以下会说明。

直接看崩溃的实际代码,基本可以断定是溢出导致的。

1
2
3
4
5
6
7
8
9
// typedef uint16_t WORD;
// WORD *label

// DHxlsReader 用的 libxls 分支
// line 638
asprintf(&ret,"%s",pWB->sst.string[shortVal(*label)].str);

// libxls 的 master 分支
asprintf(&ret,"%s",pWB->sst.string[xlsShortVal(*label)].str);

shortVal 方法所在代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// DHxlsReader 用的 libxls 分支
// 返回类型是 short
short shortVal (short s)
{
unsigned char c1, c2;

if (is_bigendian()) {
c1 = s & 255;
c2 = (s >> 8) & 255;

return (c1 << 8) + c2;
} else {
return s;
}
}

// 这个是 master 分支的代码
// 返回类型是 unsigned short
unsigned short xlsShortVal (short s)
{
unsigned char c1, c2;

if (xls_is_bigendian()) {
c1 = s & 255;
c2 = (s >> 8) & 255;

return (c1 << 8) + c2;
} else {
return s;
}
}

很显然,master 分支已经留意到,当处理成 short (-32,768~32,767)类型之后,超过 32,767unsigned short 会被处理成负数,进而引发溢出崩溃。

所以改动之一即是参考 master 的改动,把 shortVal/xlsShortVal 入参和出参类型都改成 unsigned short/uint16_t。排查下来,这个方法调用的地方,入参基本都是 WORD/uint16_t,这个改动是合理的。

留意不能只对报错的地方(字符串表下标溢出)做修改,否则还会遇到行数溢出的崩溃问题,代码里有很多的地方都调用 shortVal/xlsShortVal 了,包括行数,所以直接修改这个方法才是正确的。

新的问题

此处修复完成之后,崩溃问题得到解决。但回测发现对于大表格,数据发生了错位,似乎取错了 cell 的数据,排查之后发现原因其实还是在于之前崩溃这一行。

1
asprintf(&ret,"%s",pWB->sst.string[shortVal(*label)].str);

这一行是获取对应的cell的数据,而下标是字符串表的位置。众所周知,xls 行数最大是支持到 65535 的,意味着 cell 的个是会超过 65535(unsigned short 上界)的 ,即字符串表原则上应该也会超过 unsigned short 上界。所以虽然修复了崩溃的问题,但只是不再为负数了,一旦超过 65535,就会从 0 开始重新计数,进而发生错位现象。

先排查下调用栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 原崩溃的代码位置
BYTE *xls_getfcell(xlsWorkBook* pWB,struct st_cell_data* cell,WORD *label)
{
// ...
switch (cell->id)
{
// ...
case XLS_RECORD_LABELSST:
// 报错的代码
asprintf(&ret,"%s",pWB->sst.string[xlsShortVal(*label)].str);
break;
// ...
}

// 翻一下代码,发现调用的地方有多处,但其中传入 label 的地方不多,如下:
struct st_cell_data *xls_addCell(xlsWorkSheet* pWS,BOF* bof,BYTE* buf)
{
// ...
case XLS_RECORD_LABELSST:
case XLS_RECORD_LABEL:
cell->str=xls_getfcell(pWS->workbook,cell,(WORD_UA *)&((LABEL*)buf)->value);
// ...
}

// 而调用 xls_addCell 的地方位于
void xls_parseWorkSheet(xlsWorkSheet* pWS)
{
// ...
do
{
long lastPos = offset;
ole2_read(&tmp, 1,4,pWS->workbook->olestr);
xlsConvertBof((BOF *)&tmp);
buf=(BYTE *)malloc(tmp.size);
ole2_read(buf, 1,tmp.size,pWS->workbook->olestr);
offset += 4 + tmp.size;

// ...
cell = xls_addCell(pWS,&tmp,buf);
// ...
} while ((!pWS->workbook->olestr->eof)&&(tmp.id!=XLS_RECORD_EOF));
// ...
}

// LABEL 的类型定义
typedef struct LABEL
{
WORD row;
WORD col;
WORD xf;
BYTE value[1]; // var
}
LABEL;
typedef LABEL LABELSST;

// 其他类型定义
typedef unsigned char BYTE;
typedef uint16_t WORD;
typedef uint16_t WORD_UA;

虽然 LABEL 类型里定义的是 BYTE value[1];,但实际使用是转成了 WORD_UA/uint16_t,到底是个多大的存储,由外侧读取的地方才能知道。根据以上代码,可以知道的是,报错的地方是 LABELSST 类型的 cell。

查阅了下微软的xls官方文档/PDF,关于 LABELSST 的内存布局描述,大约在 PDF 的 326 页左右,摘抄部分信息

2.4.149 LabelSst
The LabelSst record specifies a cell that contains a string.

cell (6 bytes): A Cell structure that specifies the cell containing the string from the shared string table.
isst (4 bytes): An unsigned integer that specifies the zero-based index of an element in the array of XLUnicodeRichExtendedString structure in the rgb field of the SST record in this Workbook Stream ABNF that specifies the string contained in the cell. MUST be greater than or equal to zero and less than the number of elements in the rgb field of the SST record.

其中这个 6 bytescell 对应就是代码里的 LABEL 类型前面3个部分,之后的 isst 则对应后面的 value 部分,用来从字符串表取下标找字符串。文档指出是 4 bytesunsigned integer 类型,所以用 WORD_UA/uint16_t 来处理是不对的,应当用 uint32_t 来处理。

所以另一个需要修的地方,就是将取下标处的类型转换,改为 uint32_t.

1
2
3
4
5
6
// 原代码
asprintf(&ret,"%s",pWB->sst.string[shortVal(*label)].str);

// 修复后,其中 DWORD 就是定义的 uint32_t,4 bytes 的 unsigned integer 类型
uint32_t p = *((DWORD *)label);
asprintf(&ret,"%s",pWB->sst.string[p].str);

总结

先总结下改动,一共需2处:

  1. shortVal 方法的入参和出参,都从 short 改为 unsigned short。此处改动能解决 cell 过多、row 过多等引起的溢出闪退问题。
  2. xls_getfcell 方法内,对于 XLS_RECORD_LABELSST 类型的 cell 处理,字符串表的下标的类型转换,从 unsinged short / uint16_t 改为 uint32_t 类型。此处能解决 cell 数量过多时读取 cell 数据错位的问题,准确的说是字符串类型的 cell 过多时。

另外,排查过程中,我们也尝试使用 PSXLSReader 库,这是一个从 DHlibxls/DHxlsReader fork 出来优化过的库,发现有完全一样的闪退问题和错乱问题。检查源码,发现以上2个问题同样存在。使用 DHlibxls/DHxlsReaderlibxlsPSXLSReader 进行 xls 解析的同学,如果发现有类似闪退或 cell 错乱问题,可以参考此文档进行排查和修复。

对于 iOS ,我们的实际修复方案是 fork PSXLSReader 仓库到本地,修复完成后,源码集成。

另考虑到上述几个仓库最近一次更新时间都比较早了,暂时没有提 PR 到相关的仓库。