Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/pdfium-tsan-thread-soak.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: PDFium TSAN Thread Soak

on:
workflow_dispatch:
inputs:
ref:
description: Git ref to test
required: false
default: embedpdf/main
pdf_path:
description: PDF fixture to render during the soak
required: false
default: testing/resources/hello_world.pdf
threads:
description: Number of worker threads
required: false
default: '2'
iterations:
description: Iterations per worker thread
required: false
default: '2'

permissions:
contents: read

jobs:
tsan-thread-soak:
name: Linux x64 TSAN thread soak
runs-on: ubuntu-24.04
timeout-minutes: 90
env:
PDF_RUNTIME_SYNC: auto
PDF_RUNTIME_TARGET_OS_LIST: linux
EMBEDPDF_TLS_GLOBALS: true
EMBEDPDF_TSAN: 1
PDF_PATH: ${{ inputs.pdf_path }}
SOAK_THREADS: ${{ inputs.threads }}
SOAK_ITERATIONS: ${{ inputs.iterations }}
SOAK_OUT: out/embedpdf-runtime-thread-soak-linux-x64

steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref_name }}

- name: Install depot_tools
shell: bash
run: |
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git "$RUNNER_TEMP/depot_tools"
echo "$RUNNER_TEMP/depot_tools" >> "$GITHUB_PATH"

- name: Install Linux base deps
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends cmake clang lld curl g++ ninja-build pkg-config tar

- name: Run TSAN thread soak
shell: bash
run: |
set -euo pipefail
mkdir -p "$SOAK_OUT"
scripts/embedpdf-runtime/thread-soak-target.sh \
linux-x64 \
"$PDF_PATH" \
-- \
--threads="$SOAK_THREADS" \
--iterations="$SOAK_ITERATIONS" \
2>&1 | tee "$SOAK_OUT/thread-soak.log"

- name: Upload TSAN logs
if: always()
uses: actions/upload-artifact@v6
with:
name: pdfium-tsan-thread-soak-logs
path: |
out/embedpdf-runtime-thread-soak-linux-x64/thread-soak.log
out/embedpdf-runtime-thread-soak-linux-x64/args.gn
if-no-files-found: ignore
11 changes: 11 additions & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ config("pdfium_common_config") {
defines += [ "PDF_USE_PARTITION_ALLOC" ]
}

# EmbedPDF: thread-confined runtime. Translates the embedpdf_thread_local_globals
# GN arg into the EPDF_THREAD_LOCAL_GLOBALS define consumed by EPDF_TLS
# (core/fxcrt/epdf_tls.h). Default off; see pdfium.gni.
if (embedpdf_thread_local_globals) {
defines += [ "EPDF_THREAD_LOCAL_GLOBALS" ]
}

if (is_win) {
# Assume UTF-8 by default to avoid code page dependencies.
cflags += [ "/utf-8" ]
Expand Down Expand Up @@ -191,6 +198,7 @@ source_set("pdfium_public_headers_impl") {
sources = [
"public/cpp/fpdf_deleters.h",
"public/cpp/fpdf_scopers.h",
"public/epdf_redact.h",
"public/fpdf_annot.h",
"public/fpdf_attachment.h",
"public/fpdf_catalog.h",
Expand Down Expand Up @@ -431,6 +439,9 @@ group("pdfium_all") {
":pdfium_unittests",
"testing:pdfium_test",
"testing/fuzzers",
"testing/tools:epdf_layer_memory_benchmark",
"testing/tools:epdf_layer_replay_soak",
"testing/tools:epdf_thread_soak",
]

if (pdf_is_standalone) {
Expand Down
11 changes: 8 additions & 3 deletions core/fpdfapi/edit/cpdf_creator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
}

RetainPtr<CPDF_Dictionary> current_info = document_->GetInfo();
const uint32_t current_info_objnum =
current_info ? current_info->GetObjNum() : 0;
const uint32_t parser_info_objnum = parser_ ? parser_->GetInfoObjNum() : 0;
const bool should_write_current_info =
current_info_objnum != 0 && current_info_objnum != parser_info_objnum;
if (parser_) {
CPDF_DictionaryLocker locker(parser_->GetCombinedTrailer());
for (const auto& it : locker) {
Expand All @@ -553,7 +558,7 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
if (key == "Encrypt" || key == "Size" || key == "Filter" ||
key == "Index" || key == "Length" || key == "Prev" || key == "W" ||
key == "XRefStm" || key == "ID" || key == "DecodeParms" ||
key == "Type" || (key == "Info" && current_info)) {
key == "Type" || (key == "Info" && should_write_current_info)) {
continue;
}
if (!archive_->WriteString(("/")) ||
Expand All @@ -571,9 +576,9 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
return Stage::kInvalid;
}
}
if (current_info && current_info->GetObjNum() != 0) {
if (should_write_current_info) {
if (!archive_->WriteString("/Info ") ||
!archive_->WriteDWord(current_info->GetObjNum()) ||
!archive_->WriteDWord(current_info_objnum) ||
!archive_->WriteString(" 0 R\r\n")) {
return Stage::kInvalid;
}
Expand Down
5 changes: 4 additions & 1 deletion core/fpdfapi/font/cpdf_fontglobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
#include "core/fpdfapi/parser/cpdf_document.h"
#include "core/fxcrt/check.h"
#include "core/fxcrt/containers/contains.h"
#include "core/fxcrt/epdf_tls.h"

namespace {

CPDF_FontGlobals* g_FontGlobals = nullptr;
// EmbedPDF: thread-confined runtime - each worker thread owns its own font
// globals (stock fonts + predefined CMaps), created/destroyed on that thread.
EPDF_TLS CPDF_FontGlobals* g_FontGlobals = nullptr;

RetainPtr<const CPDF_CMap> LoadPredefinedCMap(ByteStringView name) {
if (!name.IsEmpty() && name[0] == '/') {
Expand Down
5 changes: 4 additions & 1 deletion core/fpdfapi/page/cpdf_colorspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "core/fxcrt/check_op.h"
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/containers/contains.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/data_vector.h"
#include "core/fxcrt/fx_2d_size.h"
#include "core/fxcrt/fx_safe_types.h"
Expand Down Expand Up @@ -450,7 +451,9 @@ class StockColorSpaces {
RetainPtr<CPDF_PatternCS> pattern_;
};

StockColorSpaces* g_stock_colorspaces = nullptr;
// EmbedPDF: thread-confined runtime - per-thread stock device colorspaces
// (gray/rgb/cmyk/pattern), created/destroyed on the owning thread.
EPDF_TLS StockColorSpaces* g_stock_colorspaces = nullptr;

} // namespace

Expand Down
5 changes: 4 additions & 1 deletion core/fpdfapi/page/cpdf_streamcontentparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "core/fxcrt/check.h"
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/containers/contains.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/fx_safe_types.h"
#include "core/fxcrt/scoped_set_insertion.h"
#include "core/fxcrt/span.h"
Expand All @@ -68,7 +69,9 @@ const char kPathOperatorClosePath = 'h';
const char kPathOperatorRectangle[] = "re";

using OpCodes = std::map<uint32_t, void (CPDF_StreamContentParser::*)()>;
OpCodes* g_opcodes = nullptr;
// EmbedPDF: thread-confined runtime - per-thread content-operator dispatch
// table, lazily built and torn down on the owning thread.
EPDF_TLS OpCodes* g_opcodes = nullptr;

CFX_FloatRect GetShadingBBox(CPDF_ShadingPattern* pShading,
const CFX_Matrix& matrix) {
Expand Down
2 changes: 1 addition & 1 deletion core/fpdfapi/parser/cpdf_syntax_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class ReadableSubStream final : public IFX_SeekableReadStream {
} // namespace

// static
int CPDF_SyntaxParser::s_CurrentRecursionDepth = 0;
EPDF_TLS int CPDF_SyntaxParser::s_CurrentRecursionDepth = 0;

// static
std::unique_ptr<CPDF_SyntaxParser> CPDF_SyntaxParser::CreateForTesting(
Expand Down
3 changes: 2 additions & 1 deletion core/fpdfapi/parser/cpdf_syntax_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fxcrt/data_vector.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/fx_types.h"
#include "core/fxcrt/retain_ptr.h"
#include "core/fxcrt/span.h"
Expand Down Expand Up @@ -96,7 +97,7 @@ class CPDF_SyntaxParser {
friend class cpdf_syntax_parser_ReadHexString_Test;

static constexpr int kParserMaxRecursionDepth = 64;
static int s_CurrentRecursionDepth;
static EPDF_TLS int s_CurrentRecursionDepth;

bool ReadBlockAt(FX_FILESIZE read_pos);
bool GetCharAtBackward(FX_FILESIZE pos, uint8_t* ch);
Expand Down
5 changes: 4 additions & 1 deletion core/fpdfapi/render/cpdf_renderstatus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/containers/contains.h"
#include "core/fxcrt/data_vector.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/fx_2d_size.h"
#include "core/fxcrt/fx_safe_types.h"
#include "core/fxcrt/fx_system.h"
Expand All @@ -80,7 +81,9 @@
namespace {

constexpr int kRenderMaxRecursionDepth = 64;
int g_CurrentRecursionDepth = 0;
// EmbedPDF: thread-confined runtime - per-thread render recursion counter so
// concurrent renders on different workers don't corrupt each other's depth.
EPDF_TLS int g_CurrentRecursionDepth = 0;

CFX_FillRenderOptions GetFillOptionsForDrawPathWithBlend(
const CPDF_RenderOptions::Options& options,
Expand Down
16 changes: 16 additions & 0 deletions core/fxcodec/icc/icc_transform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ namespace fxcodec {

namespace {

// EmbedPDF: thread-confined runtime - LCMS verification gate.
//
// The cms* calls below (cmsOpenProfileFromMem, cmsCreate_sRGBProfile,
// cmsCreateTransform, cmsDoTransform, ...) use the default/null cmsContext,
// which is a process-global in lcms2. We intentionally do NOT rewrite this for
// the first thread_local slice: per-profile/per-transform work on the default
// context is independent across threads in practice, and our workers create
// and use their own profiles/transforms.
//
// Gate (not a code change): run the threaded soak (testing/tools:epdf_thread_
// soak) under ThreadSanitizer with ICC-heavy PDFs before lifting the server
// pool cap. If TSAN flags contention/races in the default context, move to a
// per-thread cmsContext via cmsCreateContext + the cms*THR APIs (or guard
// transform creation with a mutex). Do not enable ICC-heavy concurrency in
// production until this gate is green.

// For use with std::unique_ptr<cmsHPROFILE>.
struct CmsProfileDeleter {
inline void operator()(cmsHPROFILE p) { cmsCloseProfile(p); }
Expand Down
7 changes: 6 additions & 1 deletion core/fxcrt/cfx_timer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
#include <map>

#include "core/fxcrt/check.h"
#include "core/fxcrt/epdf_tls.h"

namespace {

using TimerMap = std::map<int32_t, CFX_Timer*>;
TimerMap* g_pwl_timer_map = nullptr;
// EmbedPDF: thread-confined runtime - per-thread timer map. The PWL timer
// subsystem is inactive in headless server rendering, but the global is still
// made per-thread so per-thread InitializeGlobals()/DestroyGlobals() (and the
// CHECK(!g_pwl_timer_map) inside Init) hold independently on each worker.
EPDF_TLS TimerMap* g_pwl_timer_map = nullptr;

} // namespace

Expand Down
30 changes: 30 additions & 0 deletions core/fxcrt/epdf_tls.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2025 The EmbedPDF Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// EmbedPDF: thread-confined runtime support.
//
// EPDF_TLS expands to `thread_local` when the build is configured with the
// `embedpdf_thread_local_globals` GN arg (which defines
// EPDF_THREAD_LOCAL_GLOBALS), and to nothing otherwise. It is used to give
// each worker thread its own copy of PDFium's process-global singletons so
// that N threads can run shared-nothing in a single process.
//
// Contract: a thread that touches any EPDF_TLS-backed global must initialize
// PDFium on that thread (EPDF_InitThread), use only handles created on that
// thread, and tear down on the same thread (EPDF_ShutdownThread). PDFium
// handles must never cross threads.
//
// Default (flag off) keeps every global as an ordinary process-global, so
// targets that do not opt in and upstream rebases are byte-for-byte unchanged.

#ifndef CORE_FXCRT_EPDF_TLS_H_
#define CORE_FXCRT_EPDF_TLS_H_

#if defined(EPDF_THREAD_LOCAL_GLOBALS)
#define EPDF_TLS thread_local
#else
#define EPDF_TLS
#endif

#endif // CORE_FXCRT_EPDF_TLS_H_
16 changes: 15 additions & 1 deletion core/fxcrt/fx_extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "core/fxcrt/fx_extension.h"

#include <time.h>
#include <wchar.h>

#include <array>
Expand All @@ -24,7 +25,20 @@ time_t DefaultTimeFunction() {
}

struct tm* DefaultLocaltimeFunction(const time_t* tp) {
return localtime(tp);
// EmbedPDF: thread-confined runtime. Plain localtime() returns a pointer to
// shared static storage, which is a data race across worker threads. Fill a
// thread_local tm via the reentrant localtime_r/localtime_s instead so each
// thread gets its own result. struct tm is trivially destructible, so the
// thread_local adds no per-thread teardown cost.
thread_local struct tm result;
#if defined(_WIN32)
if (localtime_s(&result, tp) != 0) {
return nullptr;
}
return &result;
#else
return localtime_r(tp, &result);
#endif
}

time_t (*g_time_func)() = DefaultTimeFunction;
Expand Down
7 changes: 5 additions & 2 deletions core/fxcrt/fx_random.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <array>

#include "build/build_config.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/fx_memory.h"
#include "core/fxcrt/fx_string.h"
#include "core/fxcrt/fx_system.h"
Expand All @@ -33,8 +34,10 @@ struct MTContext {
std::array<uint32_t, MT_N> mt;
};

bool g_bHaveGlobalSeed = false;
uint32_t g_nGlobalSeed = 0;
// EmbedPDF: thread-confined runtime - per-thread RNG seed state so concurrent
// workers don't race on the lazy global-seed initialization.
EPDF_TLS bool g_bHaveGlobalSeed = false;
EPDF_TLS uint32_t g_nGlobalSeed = 0;

#if BUILDFLAG(IS_WIN)
bool GenerateSeedFromCryptoRandom(uint32_t* pSeed) {
Expand Down
5 changes: 4 additions & 1 deletion core/fxcrt/fx_system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@

#include "build/build_config.h"
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/epdf_tls.h"
#include "core/fxcrt/fx_extension.h"

namespace {

#if !BUILDFLAG(IS_WIN)
uint32_t g_last_error = 0;
// EmbedPDF: thread-confined runtime - per-thread last-error, written during
// parse/render and read back by FPDF_GetLastError on the same thread.
EPDF_TLS uint32_t g_last_error = 0;
#endif

template <typename IntType, typename CharType>
Expand Down
Loading