|
4 | 4 | "bytes" |
5 | 5 | "compress/zlib" |
6 | 6 | "fmt" |
| 7 | + "runtime" |
7 | 8 | "testing" |
8 | 9 | ) |
9 | 10 |
|
@@ -214,6 +215,188 @@ func TestObjStmRejectsHostileHeader(t *testing.T) { |
214 | 215 | } |
215 | 216 | } |
216 | 217 |
|
| 218 | +// buildObjStmHugeN builds a PDF reachable via an xref stream where object 4 |
| 219 | +// is a type-2 entry inside ObjStm 3. The ObjStm decodes to contentLen zero |
| 220 | +// bytes (cheap: FlateDecode compresses them to a few KB) but declares |
| 221 | +// /N == contentLen. A reader that trusts /N as a slice capacity balloons the |
| 222 | +// few-KB file into contentLen*sizeof(pair) bytes before parsing a single entry. |
| 223 | +func buildObjStmHugeN(t *testing.T, contentLen int) []byte { |
| 224 | + t.Helper() |
| 225 | + var zbuf bytes.Buffer |
| 226 | + zw := zlib.NewWriter(&zbuf) |
| 227 | + if _, err := zw.Write(make([]byte, contentLen)); err != nil { |
| 228 | + t.Fatal(err) |
| 229 | + } |
| 230 | + zw.Close() |
| 231 | + compressed := zbuf.Bytes() |
| 232 | + |
| 233 | + var buf bytes.Buffer |
| 234 | + buf.WriteString("%PDF-1.5\n%\xe2\xe3\xcf\xd3\n") |
| 235 | + |
| 236 | + off3 := buf.Len() |
| 237 | + fmt.Fprintf(&buf, "3 0 obj\n<< /Type /ObjStm /N %d /First 4 /Filter /FlateDecode /Length %d >>\nstream\n", |
| 238 | + contentLen, len(compressed)) |
| 239 | + buf.Write(compressed) |
| 240 | + buf.WriteString("\nendstream\nendobj\n") |
| 241 | + |
| 242 | + off2 := buf.Len() |
| 243 | + rows := []byte{ |
| 244 | + 0x01, byte(off3 >> 8), byte(off3), 0x00, // obj 3: type 1 @ off3 |
| 245 | + 0x02, 0x00, 0x03, 0x00, // obj 4: type 2 -> ObjStm 3, index 0 |
| 246 | + } |
| 247 | + fmt.Fprintf(&buf, "2 0 obj\n<< /Type /XRef /W [ 1 2 1 ] /Index [ 3 2 ] /Size 5 /Root 1 0 R /Length %d >>\nstream\n", |
| 248 | + len(rows)) |
| 249 | + buf.Write(rows) |
| 250 | + buf.WriteString("\nendstream\nendobj\n") |
| 251 | + |
| 252 | + fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", off2) |
| 253 | + return buf.Bytes() |
| 254 | +} |
| 255 | + |
| 256 | +func TestObjStmHugeNDoesNotAmplifyAllocation(t *testing.T) { |
| 257 | + const contentLen = 16 << 20 // == DefaultMaxStreamSize, the largest /N can be |
| 258 | + data := buildObjStmHugeN(t, contentLen) |
| 259 | + r, err := Open(bytes.NewReader(data)) |
| 260 | + if err != nil { |
| 261 | + t.Fatalf("Open: %v", err) |
| 262 | + } |
| 263 | + defer r.Close() |
| 264 | + |
| 265 | + var before, after runtime.MemStats |
| 266 | + runtime.GC() |
| 267 | + runtime.ReadMemStats(&before) |
| 268 | + _, err = r.Resolve(Reference{Number: 4, Generation: 0}) |
| 269 | + runtime.ReadMemStats(&after) |
| 270 | + if err == nil { |
| 271 | + t.Fatal("expected an error resolving the hostile ObjStm entry, got nil") |
| 272 | + } |
| 273 | + // Decoding contentLen zero bytes legitimately costs ~2*contentLen; the |
| 274 | + // 256 MiB the /N prealloc would add is well past this ceiling. |
| 275 | + const limit = 128 << 20 |
| 276 | + if used := after.TotalAlloc - before.TotalAlloc; used > limit { |
| 277 | + t.Fatalf("resolving a %d-byte ObjStm allocated %d bytes (> %d limit); /N is amplifying allocation", |
| 278 | + contentLen, used, limit) |
| 279 | + } |
| 280 | +} |
| 281 | + |
| 282 | +// Capping the prealloc must not truncate a legitimate ObjStm whose entry count |
| 283 | +// exceeds the cap: object 100+i carries the value 100+i, and resolving one past |
| 284 | +// the cap must still return its exact value (proving append grew the slice). |
| 285 | +func TestObjStmManyObjectsResolvePastPrealloc(t *testing.T) { |
| 286 | + const m = 5000 // > the internal maxObjStmPrealloc (4096) |
| 287 | + |
| 288 | + var body bytes.Buffer |
| 289 | + offsets := make([]int, m) |
| 290 | + for i := 0; i < m; i++ { |
| 291 | + offsets[i] = body.Len() |
| 292 | + fmt.Fprintf(&body, "%d ", 100+i) |
| 293 | + } |
| 294 | + var head bytes.Buffer |
| 295 | + for i := 0; i < m; i++ { |
| 296 | + fmt.Fprintf(&head, "%d %d ", 100+i, offsets[i]) |
| 297 | + } |
| 298 | + content := head.String() + body.String() |
| 299 | + |
| 300 | + var buf bytes.Buffer |
| 301 | + buf.WriteString("%PDF-1.5\n%\xe2\xe3\xcf\xd3\n") |
| 302 | + off1 := buf.Len() |
| 303 | + fmt.Fprintf(&buf, "1 0 obj\n<< /Type /ObjStm /N %d /First %d /Length %d >>\nstream\n", |
| 304 | + m, head.Len(), len(content)) |
| 305 | + buf.WriteString(content) |
| 306 | + buf.WriteString("\nendstream\nendobj\n") |
| 307 | + |
| 308 | + last := 100 + m - 1 |
| 309 | + off2 := buf.Len() |
| 310 | + rows := []byte{ |
| 311 | + 0x01, byte(off1 >> 8), byte(off1), 0x00, 0x00, // obj 1: type 1 @ off1 |
| 312 | + 0x02, 0x00, 0x01, byte((m - 1) >> 8), byte((m - 1) & 0xff), // last obj: type 2 -> ObjStm 1, idx m-1 |
| 313 | + } |
| 314 | + fmt.Fprintf(&buf, "2 0 obj\n<< /Type /XRef /W [ 1 2 2 ] /Index [ 1 1 %d 1 ] /Size %d /Root 1 0 R /Length %d >>\nstream\n", |
| 315 | + last, last+1, len(rows)) |
| 316 | + buf.Write(rows) |
| 317 | + buf.WriteString("\nendstream\nendobj\n") |
| 318 | + fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", off2) |
| 319 | + |
| 320 | + r, err := Open(bytes.NewReader(buf.Bytes())) |
| 321 | + if err != nil { |
| 322 | + t.Fatalf("Open: %v", err) |
| 323 | + } |
| 324 | + defer r.Close() |
| 325 | + v, err := r.Resolve(Reference{Number: last, Generation: 0}) |
| 326 | + if err != nil { |
| 327 | + t.Fatalf("Resolve object %d: %v", last, err) |
| 328 | + } |
| 329 | + n, ok := v.(Integer) |
| 330 | + if !ok || int(n) != last { |
| 331 | + t.Fatalf("object %d resolved to %v (%T), want Integer %d", last, v, v, last) |
| 332 | + } |
| 333 | +} |
| 334 | + |
| 335 | +// With no startxref and no "trailer" keyword, recovery must scan the rebuilt |
| 336 | +// objects for a /Type /Catalog and synthesise a trailer pointing at it. |
| 337 | +func TestRecoverXrefViaCatalogScan(t *testing.T) { |
| 338 | + var buf bytes.Buffer |
| 339 | + buf.WriteString("%PDF-1.7\n%\xe2\xe3\xcf\xd3\n") |
| 340 | + buf.WriteString("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") |
| 341 | + buf.WriteString("2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n") |
| 342 | + buf.WriteString("%%EOF\n") |
| 343 | + |
| 344 | + r, err := Open(bytes.NewReader(buf.Bytes())) |
| 345 | + if err != nil { |
| 346 | + t.Fatalf("Open: %v", err) |
| 347 | + } |
| 348 | + defer r.Close() |
| 349 | + cat, err := r.Catalog() |
| 350 | + if err != nil { |
| 351 | + t.Fatalf("Catalog: %v", err) |
| 352 | + } |
| 353 | + if n, ok := cat.Name("Type"); !ok || n != "Catalog" { |
| 354 | + t.Fatalf("/Type %q ok=%v, want Catalog", n, ok) |
| 355 | + } |
| 356 | +} |
| 357 | + |
| 358 | +// An incremental update appends a second xref section whose /Prev points back |
| 359 | +// at the first. The newer section must win: object 1 resolves to its updated |
| 360 | +// body, and trailer keys present only in the older section still resolve. |
| 361 | +func TestPrevChainNewestSectionWins(t *testing.T) { |
| 362 | + var buf bytes.Buffer |
| 363 | + w := func(s string) int { off := buf.Len(); buf.WriteString(s); return off } |
| 364 | + w("%PDF-1.7\n%\xe2\xe3\xcf\xd3\n") |
| 365 | + off1v1 := w("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") |
| 366 | + off2 := w("2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n") |
| 367 | + |
| 368 | + xref1 := buf.Len() |
| 369 | + fmt.Fprintf(&buf, "xref\n0 3\n%010d %05d f \n%010d %05d n \n%010d %05d n \n", |
| 370 | + 0, 65535, off1v1, 0, off2, 0) |
| 371 | + buf.WriteString("trailer\n<< /Size 3 /Root 1 0 R /Info 2 0 R >>\n") |
| 372 | + fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", xref1) |
| 373 | + |
| 374 | + off1v2 := buf.Len() |
| 375 | + buf.WriteString("1 0 obj\n<< /Type /Catalog /Pages 2 0 R /Lang (en-US) >>\nendobj\n") |
| 376 | + xref2 := buf.Len() |
| 377 | + fmt.Fprintf(&buf, "xref\n1 1\n%010d %05d n \n", off1v2, 0) |
| 378 | + fmt.Fprintf(&buf, "trailer\n<< /Size 3 /Root 1 0 R /Prev %d >>\n", xref1) |
| 379 | + fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", xref2) |
| 380 | + |
| 381 | + r, err := Open(bytes.NewReader(buf.Bytes())) |
| 382 | + if err != nil { |
| 383 | + t.Fatalf("Open: %v", err) |
| 384 | + } |
| 385 | + defer r.Close() |
| 386 | + |
| 387 | + cat, err := r.Catalog() |
| 388 | + if err != nil { |
| 389 | + t.Fatalf("Catalog: %v", err) |
| 390 | + } |
| 391 | + if lang, ok := cat.String("Lang"); !ok || lang != "en-US" { |
| 392 | + t.Errorf("/Lang = %q ok=%v, want en-US (updated object 1 not used)", lang, ok) |
| 393 | + } |
| 394 | + // /Info lives only in the older trailer; the merge must preserve it. |
| 395 | + if _, ok := r.Trailer().Get("Info"); !ok { |
| 396 | + t.Error("older trailer's /Info lost after /Prev merge") |
| 397 | + } |
| 398 | +} |
| 399 | + |
217 | 400 | // A classical xref subsection declaring far more entries than the file holds |
218 | 401 | // must be handled gracefully (recover), not over-read the buffer. The bound is |
219 | 402 | // also overflow-safe on 32-bit by construction (untestable on a 64-bit run). |
|
0 commit comments