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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions