Skip to content

Possible use-after-free in PDF stream chunks #2603

Description

@OvOhao

Possible use-after-free in PDF stream chunks

I found a possible use-after-free in PDF stream chunks returned by streamPDFSync().

Files: src/closure.h, src/Canvas.cc, lib/pdfstream.js

Functions: Closure::writeVec, Canvas::StreamPDFSync, streamPDF, Canvas::destroySurface

Relevant code:

struct Closure {
  std::vector<uint8_t> vec;

  static cairo_status_t writeVec(void *c, const uint8_t *odata, unsigned len) {
    Closure* closure = static_cast<Closure*>(c);
    closure->vec.insert(closure->vec.end(), odata, odata + len);
    return CAIRO_STATUS_SUCCESS;
  }
};

StreamPDFSync() streams pointers into that vector:

PdfSvgClosure *closure = static_cast<PdfSvgClosure *>(_closure);
PdfStreamInfo streaminfo;
streaminfo.fn = fn;
streaminfo.data = &closure->vec[0];
streaminfo.len = closure->vec.size();

cairo_status_t status = canvas_write_to_pdf_stream(ensureSurface(), streamPDF, &streaminfo);

The stream callback creates an external Buffer over the supplied pointer:

Napi::Value buf = Napi::Buffer<uint8_t>::New(env, (uint8_t *)(data), len);
streaminfo->fn.MakeCallback(env.Global(), { env.Null(), buf, Napi::Number::New(env, len) }, async);

The source comment already points out the lifetime issue:

// TODO this is technically wrong, we're returning a pointer to the data in a
// vector in a class with automatic storage duration. If the canvas goes out
// of scope while we're in the handler, a use-after-free could happen.

The closure storage is freed when the canvas surface is destroyed:

if (_closure) {
  delete _closure;
  _closure = nullptr;
}

JavaScript receives the chunks through the public stream API:

this.canvas.streamPDFSync((err, chunk, len) => {
  if (len) {
    this.push(chunk)
  } else {
    this.push(null)
  }
}, this.options)

If user code retains a chunk after the canvas has been collected or destroyed,
the Buffer can still point into the freed Closure::vec storage.

Suggested fix: make each PDF chunk own its bytes, for example by using
Napi::Buffer<uint8_t>::Copy(env, data, len), or attach an owner/finalizer that
keeps the backing storage alive until the JS Buffer is finalized.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions