From 6d95e7d78d5509a9cfb1b99d4440b21df4336fe0 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Mon, 10 Aug 2026 22:31:19 -0700 Subject: [PATCH] First pass of security fixes --- src/CMakeLists.txt | 9 +- src/Opus/DDESRVR.C | 8 + src/Opus/command2.c | 5 + src/Opus/elmisc.c | 11 + src/Opus/init2.c | 2 + src/Opus/rulerdrw.c | 7 + src/Opus/wproc.c | 5 + src/port/original/opus_asm_file2.cpp | 11 +- src/port/original/opus_asm_wproc.cpp | 6 + src/port/original/opus_modern_formats.cpp | 421 ++++++++++++++---- .../original/opus_modern_formats_test.cpp | 61 ++- .../original/opus_original_startup_probe.cpp | 31 +- src/port/original/opus_sdm_runtime.cpp | 178 ++++++-- src/port/original/opus_win95_chrome.cpp | 6 +- src/port/original/opus_word1_ui_test.cpp | 12 +- src/port/original/opus_x64_heap.cpp | 108 ++++- src/port/original/opus_x64_runtime_test.cpp | 3 +- 17 files changed, 726 insertions(+), 158 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ff4fb42..bd45655 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -689,7 +689,7 @@ target_include_directories(opus_original_engine PRIVATE ${OPUS_ORIGINAL_INCLUDE_ if(MSVC) # /J preserves the original compiler's unsigned-char default. Do not use # the C++ compiler for K&R-era original C translation units. - target_compile_options(opus_original_engine PRIVATE /J /W3 /wd4005 /Gy) + target_compile_options(opus_original_engine PRIVATE /J /W3 /wd4005 /Gy /guard:cf) endif() set_target_properties(opus_original_engine PROPERTIES FOLDER "Original Opus x64" @@ -731,13 +731,14 @@ target_compile_definitions(opus_x64_runtime PRIVATE NONATIVE OPUS_X64 NOMINMAX + $<$:OPUS_TEST_HOOKS> ) target_include_directories(opus_x64_runtime PRIVATE ${OPUS_ORIGINAL_INCLUDE_DIRS}) target_link_libraries(opus_x64_runtime PUBLIC ole32 comdlg32 gdi32) if(MSVC) # Opus was built with unsigned plain characters; the byte-oriented string # and international-character assembly routines depend on that behavior. - target_compile_options(opus_x64_runtime PRIVATE /J /W4 /permissive-) + target_compile_options(opus_x64_runtime PRIVATE /J /W4 /permissive- /guard:cf /sdl) endif() set_target_properties(opus_x64_runtime PROPERTIES FOLDER "Original Opus x64/runtime" @@ -866,8 +867,8 @@ target_compile_definitions(WORD1 PRIVATE ) target_link_libraries(WORD1 PRIVATE opus_original_engine opus_x64_runtime user32 dbghelp) if(MSVC) - target_compile_options(WORD1 PRIVATE /J /W3 /wd4005 /utf-8) - target_link_options(WORD1 PRIVATE /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT /MANIFEST:NO) + target_compile_options(WORD1 PRIVATE /J /W3 /wd4005 /utf-8 /guard:cf /sdl) + target_link_options(WORD1 PRIVATE /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT /guard:cf /CETCOMPAT /MANIFEST:NO) endif() set_target_properties(WORD1 PROPERTIES FOLDER "Original Opus x64/product" diff --git a/src/Opus/DDESRVR.C b/src/Opus/DDESRVR.C index e4ae3f3..d85d29f 100644 --- a/src/Opus/DDESRVR.C +++ b/src/Opus/DDESRVR.C @@ -81,6 +81,10 @@ int wLow, wHigh; int das; BOOL f; +#ifdef OPUS_X64 + /* DDE execute is unauthenticated cross-process macro execution. */ + goto LExeNACK; +#endif if (fTerminating || PdcldDcl(dcl)->fExecuting || fElActive) goto LExeNACK; @@ -117,6 +121,10 @@ LExeNACK: int das = dasNACK; int ibkmk; +#ifdef OPUS_X64 + /* Do not let another process overwrite an open document through DDE. */ + goto LNackPoke; +#endif if (fTerminating) /* not much point in receiving while terminating */ { diff --git a/src/Opus/command2.c b/src/Opus/command2.c index 95dd21a..e533cc4 100644 --- a/src/Opus/command2.c +++ b/src/Opus/command2.c @@ -2479,6 +2479,11 @@ uns atm; Assert(atm < atmMax); +#ifdef OPUS_X64 + /* Never execute document/template macros merely because a file opened, + closed, or Word started. See the secure default in elmisc.c. */ + return cmdOK; +#endif /* Skip auto macro if shift key is down */ if (vfDisableAutoMacros || (GetAsyncKeyState(VK_SHIFT) & 0x8000)) diff --git a/src/Opus/elmisc.c b/src/Opus/elmisc.c index 6ba2904..c2a8f06 100644 --- a/src/Opus/elmisc.c +++ b/src/Opus/elmisc.c @@ -80,7 +80,18 @@ extern BOOL vfAwfulNoise; extern int vfSeeSel; +#ifdef OPUS_X64 +/* + * Legacy Opus documents and templates can contain AutoOpen/AutoExec macros. + * The original application ran them without a trust boundary. That is not + * an acceptable default for documents received from modern file systems, so + * the x64 port keeps automatic macros disabled. Explicitly invoked macros + * continue to use the normal command path. + */ +BOOL vfDisableAutoMacros = fTrue; +#else BOOL vfDisableAutoMacros = fFalse; +#endif /* Simple Cursor Movement / Selection Commands */ diff --git a/src/Opus/init2.c b/src/Opus/init2.c index 9080d9f..e3532ba 100644 --- a/src/Opus/init2.c +++ b/src/Opus/init2.c @@ -620,6 +620,7 @@ BOOL fTutorial; if (!fTutorial) { /* BLOCK: now run macros specified on command line */ +#ifndef OPUS_X64 { int isz; CHAR * szArg; @@ -639,6 +640,7 @@ BOOL fTutorial; } } } +#endif /* !OPUS_X64 */ } diff --git a/src/Opus/rulerdrw.c b/src/Opus/rulerdrw.c index 4d5b418..9198485 100644 --- a/src/Opus/rulerdrw.c +++ b/src/Opus/rulerdrw.c @@ -104,6 +104,7 @@ extern struct REB * vpreb; static int vWin95ZoomPercent = 100; static int vWin95BaseDxsInch = 0; static int vWin95BaseDysInch = 0; +int vOpusPdfExportStage = 0; int OpusExportCurrentDocumentPdf() { @@ -120,6 +121,7 @@ int OpusExportCurrentDocumentPdf() extern int OpusPdfSnapshotExportDialog(); extern void OpusX64FontNameFromFtc(); + vOpusPdfExportStage = 1; if (selCur.doc == docNil) return fFalse; doc = DocMother(selCur.doc); @@ -127,6 +129,7 @@ int OpusExportCurrentDocumentPdf() if (cpMac < cp0 || cpMac > 0x3fffffffL) return fFalse; pdod = PdodMother(doc); + vOpusPdfExportStage = 2; if (!OpusPdfSnapshotBegin(pdod->dop.xaPage, pdod->dop.yaPage, pdod->dop.dxaLeft, pdod->dop.dxaRight, pdod->dop.dyaTop < 0 ? -pdod->dop.dyaTop : pdod->dop.dyaTop, @@ -141,6 +144,7 @@ int OpusExportCurrentDocumentPdf() cpParaLim = CpMin(caPara.cpLim, cpMac); if (cpParaLim <= cpPara) cpParaLim = cpPara + 1; + vOpusPdfExportStage = 1000 + (int)cpPara; if (!OpusPdfSnapshotAddParagraph(vpapFetch.jc, vpapFetch.dxaLeft, vpapFetch.dxaRight, vpapFetch.dxaLeft1, vpapFetch.dyaBefore, @@ -185,6 +189,7 @@ int OpusExportCurrentDocumentPdf() rgch[cchOutput] = '\0'; OpusX64FontNameFromFtc(vchpFetch.ftc, szFont, sizeof(szFont)); + vOpusPdfExportStage = 100000 + (int)cpRun; if (!OpusPdfSnapshotAddRun(rgch, cchOutput, szFont, vchpFetch.hps, vchpFetch.fBold, vchpFetch.fItalic, vchpFetch.kul != kulNone, @@ -197,7 +202,9 @@ int OpusExportCurrentDocumentPdf() } cpPara = cpParaLim; } + vOpusPdfExportStage = 3; result = OpusPdfSnapshotExportDialog(vhwndApp); + vOpusPdfExportStage = result ? 4 : -4; return result; } diff --git a/src/Opus/wproc.c b/src/Opus/wproc.c index 4bab5e4..999ffa2 100644 --- a/src/Opus/wproc.c +++ b/src/Opus/wproc.c @@ -1829,6 +1829,11 @@ LONG lParam; pffn = (struct FFN *)PstFromSttb(vhsttbFont, ibst); return (LRESULT)(unsigned char)ChsPffn(pffn); } + case 105: + { + extern int vOpusPdfExportStage; + return (LRESULT)vOpusPdfExportStage; + } } return (LRESULT) -1; #endif diff --git a/src/port/original/opus_asm_file2.cpp b/src/port/original/opus_asm_file2.cpp index 702d23a..4d01fa8 100644 --- a/src/port/original/opus_asm_file2.cpp +++ b/src/port/original/opus_asm_file2.cpp @@ -143,14 +143,21 @@ int CchCurSzPathNat(char* path, const int drive) { if (path == nullptr) { return 0; } + /* The original ABI requires callers to provide exactly 67 bytes. Do not + copy a modern MAX_PATH-sized current directory into that legacy buffer. */ + constexpr int kLegacyPathCapacity = 67; char current[MAX_PATH]{}; if (_getdcwd(drive, current, static_cast(sizeof(current))) == nullptr) { path[0] = '\0'; return 0; } - std::strcpy(path, current); - std::size_t length = std::strlen(path); + std::size_t length = std::strlen(current); + if (length + 2 > kLegacyPathCapacity) { + path[0] = '\0'; + return 0; + } + std::memcpy(path, current, length + 1); if (length == 2 && path[1] == ':') { path[length++] = '\\'; path[length] = '\0'; diff --git a/src/port/original/opus_asm_wproc.cpp b/src/port/original/opus_asm_wproc.cpp index 5325805..0a3fbd5 100644 --- a/src/port/original/opus_asm_wproc.cpp +++ b/src/port/original/opus_asm_wproc.cpp @@ -137,6 +137,12 @@ constexpr UINT kPromptMessages[] = { extern "C" { LRESULT CALLBACK NatAppWndProc(HWND h, UINT m, WPARAM w, LPARAM l) { + if (m == kWmDdeInitiate) { + // The legacy DDE server exposes document contents and macro commands + // to any process in the desktop session. The modern port does not + // accept inbound DDE conversations. + return 0; + } return Dispatch(h, m, w, l, AppWndProc, kAppMessages); } LRESULT CALLBACK NatMwdWndProc(HWND h, UINT m, WPARAM w, LPARAM l) { diff --git a/src/port/original/opus_modern_formats.cpp b/src/port/original/opus_modern_formats.cpp index 1c83f90..b131f0f 100644 --- a/src/port/original/opus_modern_formats.cpp +++ b/src/port/original/opus_modern_formats.cpp @@ -7,14 +7,18 @@ #include #include +#include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -23,6 +27,50 @@ using Microsoft::WRL::ComPtr; namespace { +constexpr std::size_t kMaxDocumentXmlBytes = 64u * 1024u * 1024u; +constexpr std::size_t kMaxStylesXmlBytes = 8u * 1024u * 1024u; +constexpr std::size_t kMaxRtfBytes = 64u * 1024u * 1024u; +constexpr std::size_t kMaxTextBytes = 32u * 1024u * 1024u; +constexpr std::size_t kMaxGeneratedBytes = 256u * 1024u * 1024u; +constexpr std::size_t kMaxStyles = 4096; +constexpr std::size_t kMaxParagraphs = 200000; +constexpr std::size_t kMaxRuns = 1000000; +constexpr std::size_t kMaxTableRows = 4096; +constexpr std::size_t kMaxTableColumns = 256; +constexpr std::size_t kMaxTableCells = 262144; + +void require_parse_limit(const bool condition) { + if (!condition) throw std::length_error("document exceeds import limits"); +} + +bool parse_bounded_int(std::string_view text, const int minimum, + const int maximum, int& result) { + if (text.empty()) return false; + int parsed = 0; + const auto conversion = std::from_chars( + text.data(), text.data() + text.size(), parsed, 10); + if (conversion.ec != std::errc{} || + conversion.ptr != text.data() + text.size() || + parsed < minimum || parsed > maximum) { + return false; + } + result = parsed; + return true; +} + +bool parse_rgb(std::string_view text, unsigned& result) { + if (text.size() != 6) return false; + unsigned parsed = 0; + const auto conversion = std::from_chars( + text.data(), text.data() + text.size(), parsed, 16); + if (conversion.ec != std::errc{} || + conversion.ptr != text.data() + text.size() || parsed > 0xffffff) { + return false; + } + result = parsed; + return true; +} + struct ComApartment { HRESULT result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); ~ComApartment() { @@ -180,11 +228,37 @@ bool has_extension(const std::string& path, const char* extension) { _stricmp(path.c_str() + path.size() - length, extension) == 0; } -bool read_stream(IStream* stream, std::string& data) { +bool safe_file_path_syntax(std::wstring_view path) { + if (path.empty() || path.size() >= 32760 || + path.starts_with(LR"(\\.\)") || + path.starts_with(LR"(\\?\GLOBALROOT\)")) return false; + for (std::size_t index = 0; index < path.size(); ++index) { + if (path[index] == L':' && index != 1) return false; + } + return true; +} + +bool regular_file_within_limit(const std::wstring& path, + const std::size_t maximum_size) { + if (!safe_file_path_syntax(path)) return false; + WIN32_FILE_ATTRIBUTE_DATA attributes{}; + if (!GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, + &attributes) || + (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + return false; + } + const ULONGLONG size = + (static_cast(attributes.nFileSizeHigh) << 32) | + attributes.nFileSizeLow; + return size <= maximum_size; +} + +bool read_stream(IStream* stream, std::string& data, + const std::size_t maximum_size) { STATSTG status{}; if (stream == nullptr || FAILED(stream->Stat(&status, STATFLAG_NONAME)) || - status.cbSize.QuadPart < 0 || - status.cbSize.QuadPart > static_cast(256 * 1024 * 1024)) { + status.cbSize.QuadPart > static_cast(maximum_size) || + maximum_size > MAXDWORD) { return false; } data.resize(static_cast(status.cbSize.QuadPart)); @@ -197,7 +271,8 @@ bool read_stream(IStream* stream, std::string& data) { } bool read_opc_part(const std::wstring& path, const wchar_t* part_name, - std::string& data) { + std::string& data, const std::size_t maximum_size) { + if (!regular_file_within_limit(path, kMaxGeneratedBytes)) return false; ComApartment apartment; if (!apartment.usable()) return false; ComPtr factory; @@ -221,7 +296,7 @@ bool read_opc_part(const std::wstring& path, const wchar_t* part_name, SUCCEEDED(factory->CreatePartUri(part_name, &uri)) && SUCCEEDED(parts->GetPart(uri.Get(), &part)) && SUCCEEDED(part->GetContentStream(&content)) && - read_stream(content.Get(), data); + read_stream(content.Get(), data, maximum_size); } std::string tag_attribute(std::string_view tag, std::string_view name) { @@ -371,7 +446,7 @@ void apply_run_properties(RunStyle& style, std::string_view props) { apply_switch("caps", style.all_caps); apply_switch("vanish", style.hidden); if (const std::string size = first_property_value(props, "sz"); !size.empty()) { - style.half_points = (std::max)(2, std::atoi(size.c_str())); + parse_bounded_int(size, 2, 254, style.half_points); } if (const std::string font = first_property_value(props, "rFonts", "ascii"); !font.empty()) { @@ -379,9 +454,12 @@ void apply_run_properties(RunStyle& style, std::string_view props) { } if (const std::string color = first_property_value(props, "color"); !color.empty() && color != "auto" && color.size() == 6) { - const unsigned rgb = std::strtoul(color.c_str(), nullptr, 16); - style.color = RGB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); - style.auto_color = false; + unsigned rgb = 0; + if (parse_rgb(color, rgb)) { + style.color = RGB((rgb >> 16) & 0xff, + (rgb >> 8) & 0xff, rgb & 0xff); + style.auto_color = false; + } } else if (first_property_value(props, "color") == "auto") { style.auto_color = true; style.color = RGB(0, 0, 0); @@ -402,19 +480,22 @@ void apply_paragraph_properties(Paragraph& paragraph, std::string_view props) { const std::string right = tag_attribute(indent, "right"); const std::string first = tag_attribute(indent, "firstLine"); const std::string hanging = tag_attribute(indent, "hanging"); - if (!left.empty()) paragraph.left_indent = std::atoi(left.c_str()); - if (!right.empty()) paragraph.right_indent = std::atoi(right.c_str()); - if (!first.empty()) paragraph.first_line_indent = std::atoi(first.c_str()); - if (!hanging.empty()) paragraph.first_line_indent = -std::atoi(hanging.c_str()); + parse_bounded_int(left, -31680, 31680, paragraph.left_indent); + parse_bounded_int(right, -31680, 31680, paragraph.right_indent); + parse_bounded_int(first, -31680, 31680, + paragraph.first_line_indent); + int hanging_indent = 0; + if (parse_bounded_int(hanging, 0, 31680, hanging_indent)) + paragraph.first_line_indent = -hanging_indent; } if (const std::string_view spacing = element_block(props, "spacing"); !spacing.empty()) { const std::string before = tag_attribute(spacing, "before"); const std::string after = tag_attribute(spacing, "after"); const std::string line = tag_attribute(spacing, "line"); - if (!before.empty()) paragraph.space_before = std::atoi(before.c_str()); - if (!after.empty()) paragraph.space_after = std::atoi(after.c_str()); - if (!line.empty()) paragraph.line_spacing = std::atoi(line.c_str()); + parse_bounded_int(before, 0, 31680, paragraph.space_before); + parse_bounded_int(after, 0, 31680, paragraph.space_after); + parse_bounded_int(line, 0, 31680, paragraph.line_spacing); } const auto apply_switch = [&](const char* name, bool& target) { const int state = property_state(props, name); @@ -473,6 +554,7 @@ StyleCatalog parse_style_catalog(std::string_view xml) { const std::string id = tag_attribute(opening, "styleId"); const std::string_view block = xml.substr(position, close + 10 - position); if (!id.empty()) { + require_parse_limit(catalog.definitions.size() < kMaxStyles); StyleDefinition definition; definition.based_on = first_property_value(block, "basedOn"); if (const std::string_view props = element_block(block, "rPr"); @@ -522,6 +604,7 @@ std::wstring parse_run_text(std::string_view run) { const std::size_t close = run.find("", tag_end + 1); if (close == std::string_view::npos) break; text += xml_unescape(run.substr(tag_end + 1, close - tag_end - 1)); + require_parse_limit(text.size() <= kMaxTextBytes); position = close + 6; } else { if (name == "tab") text.push_back(L'\t'); @@ -538,7 +621,8 @@ std::wstring parse_run_text(std::string_view run) { } std::vector parse_paragraphs_flat( - std::string_view xml, const StyleCatalog& catalog = {}) { + std::string_view xml, const StyleCatalog& catalog, + std::size_t& total_paragraphs, std::size_t& total_runs) { std::vector paragraphs; std::size_t position = 0; while ((position = xml.find(" parse_paragraphs_flat( const std::string_view run = block.substr( run_position, run_close + 6 - run_position); std::wstring text = parse_run_text(run); - if (!text.empty()) paragraph.runs.push_back( - {parse_run_style(run, paragraph_run, catalog), std::move(text)}); + if (!text.empty()) { + require_parse_limit(total_runs < kMaxRuns); + paragraph.runs.push_back( + {parse_run_style(run, paragraph_run, catalog), + std::move(text)}); + ++total_runs; + } run_position = run_close + 6; } + require_parse_limit(total_paragraphs < kMaxParagraphs); paragraphs.push_back(std::move(paragraph)); + ++total_paragraphs; position = close + 6; } if (paragraphs.empty()) paragraphs.push_back({}); @@ -615,6 +706,8 @@ std::vector parse_document_xml( std::vector* table_layouts = nullptr) { std::vector paragraphs; if (table_layouts != nullptr) table_layouts->clear(); + std::size_t total_paragraphs = 0; + std::size_t total_runs = 0; std::size_t cursor = 0; while (cursor < xml.size()) { const std::size_t table_start = find_word_tag(xml, "tbl", cursor); @@ -622,10 +715,12 @@ std::vector parse_document_xml( const std::string_view remainder = xml.substr(cursor); if (find_word_tag(remainder, "p", 0) != std::string_view::npos) { std::vector tail = - parse_paragraphs_flat(remainder, catalog); + parse_paragraphs_flat(remainder, catalog, + total_paragraphs, total_runs); paragraphs.insert(paragraphs.end(), std::make_move_iterator(tail.begin()), std::make_move_iterator(tail.end())); + require_parse_limit(paragraphs.size() <= kMaxParagraphs); } break; } @@ -633,10 +728,12 @@ std::vector parse_document_xml( xml.substr(cursor, table_start - cursor); if (find_word_tag(prefix, "p", 0) != std::string_view::npos) { std::vector before = - parse_paragraphs_flat(prefix, catalog); + parse_paragraphs_flat(prefix, catalog, + total_paragraphs, total_runs); paragraphs.insert(paragraphs.end(), std::make_move_iterator(before.begin()), std::make_move_iterator(before.end())); + require_parse_limit(paragraphs.size() <= kMaxParagraphs); } const std::size_t table_close = xml.find("", table_start); if (table_close == std::string_view::npos) break; @@ -646,12 +743,15 @@ std::vector parse_document_xml( TableLayout layout; layout.first_paragraph = paragraphs.size(); std::size_t row_cursor = 0; + std::size_t table_cell_count = 0; while (true) { const std::size_t row_start = find_word_tag(table, "tr", row_cursor); if (row_start == std::string_view::npos) break; const std::size_t row_close = table.find("", row_start); if (row_close == std::string_view::npos) break; const std::size_t row_end = row_close + std::strlen(""); + require_parse_limit(static_cast(layout.rows) < + kMaxTableRows); const std::string_view row = table.substr(row_start, row_end - row_start); std::vector> cells; @@ -662,8 +762,12 @@ std::vector parse_document_xml( const std::size_t cell_close = row.find("", cell_start); if (cell_close == std::string_view::npos) break; const std::size_t cell_end = cell_close + std::strlen(""); + require_parse_limit(cells.size() < kMaxTableColumns && + table_cell_count < kMaxTableCells); cells.push_back(parse_paragraphs_flat( - row.substr(cell_start, cell_end - cell_start), catalog)); + row.substr(cell_start, cell_end - cell_start), catalog, + total_paragraphs, total_runs)); + ++table_cell_count; cell_cursor = cell_end; } std::size_t line_count = 0; @@ -678,6 +782,8 @@ std::vector parse_document_xml( paragraphs.push_back(std::move(cell[line])); else paragraphs.push_back({}); + require_parse_limit(paragraphs.size() <= + kMaxParagraphs); } ++layout.rows; } @@ -691,10 +797,12 @@ std::vector parse_document_xml( if (table_layouts != nullptr) table_layouts->push_back(layout); } else { paragraphs.resize(layout.first_paragraph); - std::vector flat = parse_paragraphs_flat(table, catalog); + std::vector flat = parse_paragraphs_flat( + table, catalog, total_paragraphs, total_runs); paragraphs.insert(paragraphs.end(), std::make_move_iterator(flat.begin()), std::make_move_iterator(flat.end())); + require_parse_limit(paragraphs.size() <= kMaxParagraphs); } cursor = table_end; } @@ -707,20 +815,22 @@ DocumentSettings parse_document_settings(std::string_view xml) { const std::string_view section = element_block(xml, "sectPr"); const std::string_view size = element_block(section, "pgSz"); const std::string_view margins = element_block(section, "pgMar"); - const auto integer_attribute = [](std::string_view tag, - const char* name) { + const auto integer_attribute = [](std::string_view tag, const char* name, + const int minimum, const int maximum) { const std::string value = tag_attribute(tag, name); - return value.empty() ? 0 : std::atoi(value.c_str()); + int result = 0; + parse_bounded_int(value, minimum, maximum, result); + return result; }; - settings.page_width = integer_attribute(size, "w"); - settings.page_height = integer_attribute(size, "h"); - settings.margin_left = integer_attribute(margins, "left"); - settings.margin_right = integer_attribute(margins, "right"); - settings.margin_top = integer_attribute(margins, "top"); - settings.margin_bottom = integer_attribute(margins, "bottom"); + settings.page_width = integer_attribute(size, "w", 720, 63360); + settings.page_height = integer_attribute(size, "h", 720, 63360); + settings.margin_left = integer_attribute(margins, "left", 0, 31680); + settings.margin_right = integer_attribute(margins, "right", 0, 31680); + settings.margin_top = integer_attribute(margins, "top", 0, 31680); + settings.margin_bottom = integer_attribute(margins, "bottom", 0, 31680); settings.valid = settings.page_width > 0 && settings.page_height > 0 && - settings.margin_left >= 0 && settings.margin_right >= 0 && - settings.margin_top >= 0 && settings.margin_bottom >= 0; + settings.margin_left + settings.margin_right < settings.page_width && + settings.margin_top + settings.margin_bottom < settings.page_height; return settings; } @@ -756,6 +866,8 @@ PendingDocxImport pending_docx_import; struct PendingPdfExport { std::vector paragraphs; DocumentSettings settings; + std::size_t run_count = 0; + std::size_t text_bytes = 0; }; PendingPdfExport pending_pdf_export; @@ -780,6 +892,7 @@ std::string paragraphs_to_text(const std::vector& paragraphs, std::string text; if (pending != nullptr) *pending = {}; for (std::size_t index = 0; index < paragraphs.size(); ++index) { + require_parse_limit(text.size() <= kMaxTextBytes); PendingParagraphFormat paragraph_format; paragraph_format.cp_first = static_cast(text.size()); paragraph_format.paragraph = paragraphs[index]; @@ -788,12 +901,17 @@ std::string paragraphs_to_text(const std::vector& paragraphs, PendingRunFormat run_format; run_format.cp_first = static_cast(text.size()); run_format.style = run.style; - text += ansi_run_text(run.text); + const std::string encoded = ansi_run_text(run.text); + require_parse_limit(encoded.size() <= kMaxTextBytes - text.size()); + text += encoded; run_format.cp_lim = static_cast(text.size()); if (pending != nullptr && run_format.cp_lim > run_format.cp_first) pending->runs.push_back(std::move(run_format)); } - if (index + 1 < paragraphs.size()) text += "\r\n"; + if (index + 1 < paragraphs.size()) { + require_parse_limit(text.size() <= kMaxTextBytes - 2); + text += "\r\n"; + } paragraph_format.cp_lim = static_cast(text.size()); if (pending != nullptr) pending->paragraphs.push_back(std::move(paragraph_format)); @@ -824,9 +942,13 @@ bool load_docx_paragraphs(const char* path, std::vector* tables = nullptr) { std::string document; std::string styles; - if (!read_opc_part(wide_path(path), L"/word/document.xml", document)) + const std::wstring document_path = wide_path(path); + if (document_path.empty() || + !read_opc_part(document_path, L"/word/document.xml", document, + kMaxDocumentXmlBytes)) return false; - read_opc_part(wide_path(path), L"/word/styles.xml", styles); + read_opc_part(document_path, L"/word/styles.xml", styles, + kMaxStylesXmlBytes); paragraphs = parse_document_xml(document, parse_style_catalog(styles), tables); if (settings != nullptr) *settings = parse_document_settings(document); @@ -971,27 +1093,86 @@ std::string paragraphs_to_rtf(const std::vector& paragraphs) { return rtf; } -bool write_bytes(const std::wstring& path, std::string_view bytes) { - HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (file == INVALID_HANDLE_VALUE) return false; - DWORD written = 0; - const bool ok = bytes.size() <= MAXDWORD && - WriteFile(file, bytes.data(), static_cast(bytes.size()), - &written, nullptr) && written == bytes.size(); - CloseHandle(file); - if (!ok) DeleteFileW(path.c_str()); - return ok; +bool reserve_sibling_temporary_file(const std::wstring& path, + std::wstring& temporary, + HANDLE& file) { + if (!safe_file_path_syntax(path)) return false; + static std::atomic sequence{0}; + for (int attempt = 0; attempt < 64; ++attempt) { + temporary = path + L".word1tmp-" + + std::to_wstring(GetCurrentProcessId()) + L"-" + + std::to_wstring(++sequence); + file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file != INVALID_HANDLE_VALUE) return true; + if (GetLastError() != ERROR_FILE_EXISTS && + GetLastError() != ERROR_ALREADY_EXISTS) return false; + } + return false; } -bool read_bytes(const std::wstring& path, std::string& bytes) { +bool commit_sibling_temporary_file(const std::wstring& temporary, + const std::wstring& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + bool replaced = false; + if (attributes != INVALID_FILE_ATTRIBUTES && + (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + replaced = ReplaceFileW(path.c_str(), temporary.c_str(), nullptr, + REPLACEFILE_WRITE_THROUGH, nullptr, nullptr) != + FALSE; + if (!replaced) { + replaced = MoveFileExW( + temporary.c_str(), path.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != FALSE; + } + } else if (attributes == INVALID_FILE_ATTRIBUTES && + GetLastError() == ERROR_FILE_NOT_FOUND) { + replaced = MoveFileExW(temporary.c_str(), path.c_str(), + MOVEFILE_WRITE_THROUGH) != FALSE; + } + if (!replaced) DeleteFileW(temporary.c_str()); + return replaced; +} + +bool write_bytes(const std::wstring& path, std::string_view bytes) { + if (bytes.size() > MAXDWORD) return false; + std::wstring temporary; + HANDLE file = INVALID_HANDLE_VALUE; + if (!reserve_sibling_temporary_file(path, temporary, file)) return false; + std::size_t position = 0; + bool ok = true; + while (position < bytes.size()) { + DWORD written = 0; + const DWORD requested = static_cast((std::min)( + bytes.size() - position, + static_cast((std::numeric_limits::max)()))); + if (!WriteFile(file, bytes.data() + position, requested, &written, + nullptr) || written == 0) { + ok = false; + break; + } + position += written; + } + if (ok) ok = FlushFileBuffers(file) != FALSE; + CloseHandle(file); + if (!ok) { + DeleteFileW(temporary.c_str()); + return false; + } + return commit_sibling_temporary_file(temporary, path); +} + +bool read_bytes(const std::wstring& path, std::string& bytes, + const std::size_t maximum_size = kMaxRtfBytes) { + if (!regular_file_within_limit(path, maximum_size)) return false; HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return false; LARGE_INTEGER size{}; bool ok = GetFileSizeEx(file, &size) && size.QuadPart >= 0 && - size.QuadPart <= 256 * 1024 * 1024; + size.QuadPart <= static_cast(maximum_size) && + maximum_size <= MAXDWORD; if (ok) { bytes.resize(static_cast(size.QuadPart)); DWORD read = 0; @@ -1005,7 +1186,17 @@ bool read_bytes(const std::wstring& path, std::string& bytes) { struct StreamCookie { const char* data; LONG length; LONG position; }; DWORD CALLBACK rich_edit_stream_in(DWORD_PTR cookie, LPBYTE buffer, LONG requested, LONG* copied) { + if (cookie == 0 || buffer == nullptr || copied == nullptr || + requested <= 0) { + if (copied != nullptr) *copied = 0; + return 1; + } auto& source = *reinterpret_cast(cookie); + if (source.data == nullptr || source.position < 0 || + source.position > source.length) { + *copied = 0; + return 1; + } *copied = (std::min)(requested, source.length - source.position); if (*copied > 0) { std::memcpy(buffer, source.data + source.position, *copied); @@ -1017,7 +1208,10 @@ DWORD CALLBACK rich_edit_stream_in(DWORD_PTR cookie, LPBYTE buffer, class RichEditDocument { public: bool load(std::string_view rtf) { - module_ = LoadLibraryW(L"Msftedit.dll"); + if (rtf.size() > kMaxRtfBytes || + rtf.size() > static_cast(LONG_MAX)) return false; + module_ = LoadLibraryExW(L"Msftedit.dll", nullptr, + LOAD_LIBRARY_SEARCH_SYSTEM32); if (module_ == nullptr) return false; window_ = CreateWindowExW(0, MSFTEDIT_CLASS, L"", WS_POPUP | ES_MULTILINE, 0, 0, 100, 100, nullptr, nullptr, @@ -1238,13 +1432,13 @@ bool write_docx(const std::wstring& path, const std::vector& paragrap L"/word/settings.xml", L"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings")) return false; - DeleteFileW(path.c_str()); - return SUCCEEDED(factory->CreateStreamOnFile(path.c_str(), OPC_STREAM_IO_WRITE, - nullptr, FILE_ATTRIBUTE_NORMAL, - &output)) && - SUCCEEDED(factory->WritePackageToStream(package.Get(), - OPC_WRITE_DEFAULT, - output.Get())); + if (FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &output)) || + FAILED(factory->WritePackageToStream(package.Get(), + OPC_WRITE_DEFAULT, + output.Get()))) return false; + std::string package_bytes; + return read_stream(output.Get(), package_bytes, kMaxGeneratedBytes) && + write_bytes(path, package_bytes); } bool rtf_to_docx(const std::wstring& rtf_path, const std::wstring& docx_path) { @@ -1620,11 +1814,18 @@ int export_paragraphs_to_pdf_dialog( dialog.lpstrDefExt = L"pdf"; dialog.Flags = OFN_EXPLORER | OFN_ENABLESIZING | OFN_LONGNAMES | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT; - const DWORD test_path_length = GetEnvironmentVariableW( + DWORD test_path_length = 0; +#ifdef OPUS_TEST_HOOKS + test_path_length = GetEnvironmentVariableW( L"WORD1_TEST_PDF_PATH", path, static_cast(std::size(path))); +#endif const bool accepted = test_path_length > 0 && test_path_length < std::size(path) ? +#ifdef OPUS_TEST_HOOKS (SetEnvironmentVariableW(L"WORD1_TEST_PDF_PATH", nullptr), true) : +#else + true : +#endif GetSaveFileNameW(&dialog) != FALSE; if (!accepted) { return CommDlgExtendedError() == 0 ? -1 : false; @@ -1652,14 +1853,16 @@ extern "C" int OpusModernPathIsDocx(const char* path) { } extern "C" int OpusModernDocxToRtfFile(const char* docx_path, - const char* rtf_path) { + const char* rtf_path) try { std::vector paragraphs; return load_docx_paragraphs(docx_path, paragraphs) && write_bytes(wide_path(rtf_path), paragraphs_to_rtf(paragraphs)); +} catch (...) { + return false; } extern "C" int OpusModernDocxToTextFile(const char* docx_path, - const char* text_path) { + const char* text_path) try { std::vector paragraphs; std::vector tables; DocumentSettings settings; @@ -1675,6 +1878,9 @@ extern "C" int OpusModernDocxToTextFile(const char* docx_path, return false; } return true; +} catch (...) { + pending_docx_import = {}; + return false; } extern "C" int OpusModernPendingDocxRunCount() { @@ -1732,7 +1938,8 @@ extern "C" int OpusModernGetPendingDocxPage( extern "C" int OpusModernGetPendingDocxRun( const int index, long* cp_first, long* cp_lim, int* bold, int* italic, int* underline, int* strike, int* small_caps, int* all_caps, int* hidden, - int* half_points, int* color_index, char* font, const int font_capacity) { + int* half_points, int* color_index, char* font, + const int font_capacity) try { if (index < 0 || static_cast(index) >= pending_docx_import.runs.size()) return false; const PendingRunFormat& record = pending_docx_import.runs[index]; @@ -1752,6 +1959,9 @@ extern "C" int OpusModernGetPendingDocxRun( lstrcpynA(font, ansi_font.c_str(), font_capacity); } return true; +} catch (...) { + if (font != nullptr && font_capacity > 0) font[0] = '\0'; + return false; } extern "C" int OpusModernGetPendingDocxParagraph( @@ -1795,16 +2005,23 @@ extern "C" int OpusPdfSnapshotBegin( const int margin_right, const int margin_top, const int margin_bottom) { pending_pdf_export = {}; DocumentSettings& settings = pending_pdf_export.settings; + const bool dimensions_valid = + page_width >= 720 && page_width <= 63360 && + page_height >= 720 && page_height <= 63360 && + margin_left >= 0 && margin_left <= 31680 && + margin_right >= 0 && margin_right <= 31680 && + margin_top >= 0 && margin_top <= 31680 && + margin_bottom >= 0 && margin_bottom <= 31680; settings.page_width = page_width; settings.page_height = page_height; - settings.margin_left = (std::max)(0, margin_left); - settings.margin_right = (std::max)(0, margin_right); - settings.margin_top = (std::max)(0, margin_top); - settings.margin_bottom = (std::max)(0, margin_bottom); - settings.valid = page_width > 0 && page_height > 0 && + settings.margin_left = margin_left; + settings.margin_right = margin_right; + settings.margin_top = margin_top; + settings.margin_bottom = margin_bottom; + settings.valid = dimensions_valid && settings.margin_left + settings.margin_right < page_width && settings.margin_top + settings.margin_bottom < page_height; - return true; + return settings.valid; } extern "C" int OpusPdfSnapshotAddParagraph( @@ -1812,7 +2029,18 @@ extern "C" int OpusPdfSnapshotAddParagraph( const int first_line_indent, const int space_before, const int space_after, const int line_spacing, const int keep_together, const int keep_with_next, - const int page_break_before, const int bottom_border) { + const int page_break_before, const int bottom_border) try { + if (pending_pdf_export.paragraphs.size() >= kMaxParagraphs) return false; + /* These values come from the original PAP: indentation and line spacing + are signed 16-bit fields, while before/after spacing are unsigned. + Validate their real storage domains instead of rejecting legitimate + exact-line-spacing and legacy sentinel values. */ + if (left_indent < INT16_MIN || left_indent > INT16_MAX || + right_indent < INT16_MIN || right_indent > INT16_MAX || + first_line_indent < INT16_MIN || first_line_indent > INT16_MAX || + space_before < 0 || space_before > UINT16_MAX || + space_after < 0 || space_after > UINT16_MAX || + line_spacing < INT16_MIN || line_spacing > INT16_MAX) return false; Paragraph paragraph; paragraph.alignment = alignment == 1 ? PFA_CENTER : alignment == 2 ? PFA_RIGHT : @@ -1829,15 +2057,21 @@ extern "C" int OpusPdfSnapshotAddParagraph( paragraph.bottom_border = bottom_border != 0; pending_pdf_export.paragraphs.push_back(std::move(paragraph)); return true; +} catch (...) { + return false; } extern "C" int OpusPdfSnapshotAddRun( const char* text, const int length, const char* font, const int half_points, const int bold, const int italic, const int underline, const int strike, const int small_caps, - const int all_caps, const int hidden, const int color_index) { + const int all_caps, const int hidden, const int color_index) try { if (text == nullptr || length < 0 || - pending_pdf_export.paragraphs.empty()) return false; + static_cast(length) > kMaxTextBytes || + pending_pdf_export.paragraphs.empty() || + pending_pdf_export.run_count >= kMaxRuns || + static_cast(length) > + kMaxTextBytes - pending_pdf_export.text_bytes) return false; RunStyle style; style.bold = bold != 0; style.italic = italic != 0; @@ -1846,8 +2080,13 @@ extern "C" int OpusPdfSnapshotAddRun( style.small_caps = small_caps != 0; style.all_caps = all_caps != 0; style.hidden = hidden != 0; - style.half_points = half_points >= 8 ? half_points : 20; - if (font != nullptr && *font != '\0') style.font = ansi_to_wide(font); + style.half_points = half_points >= 8 && half_points <= 254 ? + half_points : 20; + if (font != nullptr && *font != '\0') { + const std::size_t font_length = strnlen_s(font, 256); + if (font_length == 256) return false; + style.font = ansi_to_wide(std::string_view(font, font_length)); + } apply_legacy_color(color_index, style); std::wstring run_text = ansi_to_wide( std::string_view(text, static_cast(length))); @@ -1861,38 +2100,56 @@ extern "C" int OpusPdfSnapshotAddRun( } pending_pdf_export.paragraphs.back().runs.push_back( {style, std::move(run_text)}); + ++pending_pdf_export.run_count; + pending_pdf_export.text_bytes += static_cast(length); return true; +} catch (...) { + return false; } -extern "C" int OpusPdfSnapshotExportDialog(HWND owner) { +extern "C" int OpusPdfSnapshotExportDialog(HWND owner) try { if (pending_pdf_export.paragraphs.empty()) return false; const int result = export_paragraphs_to_pdf_dialog( owner, pending_pdf_export.paragraphs, pending_pdf_export.settings); pending_pdf_export = {}; return result; +} catch (...) { + pending_pdf_export = {}; + return false; } extern "C" int OpusModernRtfFileToDocx(const char* rtf_path, - const char* docx_path) { + const char* docx_path) try { return rtf_to_docx(wide_path(rtf_path), wide_path(docx_path)); +} catch (...) { + return false; } extern "C" int OpusModernRtfFileToPdf(const char* rtf_path, - const char* pdf_path) { + const char* pdf_path) try { std::string rtf; return read_bytes(wide_path(rtf_path), rtf) && rtf_to_pdf(rtf, wide_path(pdf_path)); +} catch (...) { + return false; } -extern "C" int OpusExportRtfToPdfDialog(HWND owner, const char* rtf) { - return rtf == nullptr ? false : - export_rtf_to_pdf_dialog(owner, rtf); +extern "C" int OpusExportRtfToPdfDialog(HWND owner, const char* rtf) try { + if (rtf == nullptr) return false; + const std::size_t length = strnlen_s(rtf, kMaxRtfBytes + 1); + return length <= kMaxRtfBytes && + export_rtf_to_pdf_dialog(owner, std::string_view(rtf, length)); +} catch (...) { + return false; } extern "C" int OpusExportTextToPdfDialog(HWND owner, const char* text, - const int length) { - if (text == nullptr || length < 0) return false; + const int length) try { + if (text == nullptr || length < 0 || + static_cast(length) > kMaxTextBytes) return false; return export_rtf_to_pdf_dialog( owner, ansi_text_to_rtf(std::string_view( text, static_cast(length)))); +} catch (...) { + return false; } diff --git a/src/port/original/opus_modern_formats_test.cpp b/src/port/original/opus_modern_formats_test.cpp index ba42c51..2c3a398 100644 --- a/src/port/original/opus_modern_formats_test.cpp +++ b/src/port/original/opus_modern_formats_test.cpp @@ -1,15 +1,35 @@ #define WIN32_LEAN_AND_MEAN #include +#include +#include #include #include #include extern "C" int OpusModernDocxToRtfFile(const char*, const char*); +extern "C" int OpusModernDocxToTextFile(const char*, const char*); extern "C" int OpusModernRtfFileToDocx(const char*, const char*); extern "C" int OpusModernRtfFileToPdf(const char*, const char*); +extern "C" int OpusPdfSnapshotBegin(int, int, int, int, int, int); +extern "C" int OpusPdfSnapshotAddParagraph(int, int, int, int, int, int, + int, int, int, int, int); +extern "C" int OpusPdfSnapshotAddRun(const char*, int, const char*, int, + int, int, int, int, int, int, int, int); + +std::string read_file(const std::string& path) { + std::ifstream input(path, std::ios::binary); + return {std::istreambuf_iterator(input), {}}; +} int main(const int argument_count, char** arguments) { + if (argument_count == 4 && std::strcmp(arguments[1], "--text") == 0) { + const bool converted = + OpusModernDocxToTextFile(arguments[2], arguments[3]) != 0; + std::cout << "DOCX text import " + << (converted ? "passed" : "failed") << '\n'; + return converted ? 0 : 4; + } if (argument_count == 3) { const bool converted = OpusModernDocxToRtfFile(arguments[1], arguments[2]) != 0; @@ -32,6 +52,8 @@ int main(const int argument_count, char** arguments) { const std::string rtf = base + ".rtf"; const std::string docx = base + ".docx"; const std::string roundtrip = base + ".roundtrip.rtf"; + const std::string oversized = base + ".oversized.rtf"; + const std::string preserved = base + ".preserved.pdf"; char requested_pdf[32768]{}; const DWORD requested_pdf_length = GetEnvironmentVariableA( "WORD1_TEST_KEEP_PDF", requested_pdf, @@ -51,6 +73,35 @@ int main(const int argument_count, char** arguments) { const bool read = written && OpusModernDocxToRtfFile(docx.c_str(), roundtrip.c_str()) != 0; const bool pdf_written = OpusModernRtfFileToPdf(rtf.c_str(), pdf.c_str()) != 0; + { + std::ofstream output(preserved, std::ios::binary); + output << "ORIGINAL"; + } + HANDLE oversized_file = CreateFileA( + oversized.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY, nullptr); + bool oversized_created = oversized_file != INVALID_HANDLE_VALUE; + if (oversized_created) { + LARGE_INTEGER hostile_size{}; + hostile_size.QuadPart = 64ll * 1024ll * 1024ll + 1; + oversized_created = SetFilePointerEx(oversized_file, hostile_size, + nullptr, FILE_BEGIN) && + SetEndOfFile(oversized_file); + CloseHandle(oversized_file); + } + const bool oversized_rejected = oversized_created && + OpusModernRtfFileToPdf(oversized.c_str(), preserved.c_str()) == 0 && + read_file(preserved) == "ORIGINAL"; + const bool invalid_snapshot_rejected = + OpusPdfSnapshotBegin(INT_MAX, INT_MAX, INT_MAX, INT_MAX, + INT_MAX, INT_MAX) == 0 && + OpusPdfSnapshotBegin(12240, 15840, 1440, 1440, 1440, 1440) != 0 && + OpusPdfSnapshotAddParagraph(0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0) != 0 && + OpusPdfSnapshotAddParagraph(0, 0, 0, 0, 0, 0, -240, + 0, 0, 0, 0) != 0 && + OpusPdfSnapshotAddRun("x", INT_MAX, "Arial", 20, + 0, 0, 0, 0, 0, 0, 0, 0) == 0; std::string result; if (read) { std::ifstream input(roundtrip, std::ios::binary); @@ -64,6 +115,8 @@ int main(const int argument_count, char** arguments) { DeleteFileA(rtf.c_str()); DeleteFileA(docx.c_str()); DeleteFileA(roundtrip.c_str()); + DeleteFileA(oversized.c_str()); + DeleteFileA(preserved.c_str()); if (!keep_pdf) DeleteFileA(pdf.c_str()); if (!written || !read || result.find("Bold") == std::string::npos || result.find("Second paragraph") == std::string::npos || @@ -75,10 +128,14 @@ int main(const int argument_count, char** arguments) { pdf_data.find("Second paragraph") == std::string::npos || pdf_data.find("/Helvetica-Bold") == std::string::npos || pdf_data.find("xref") == std::string::npos || - pdf_data.find("%%EOF") == std::string::npos) { + pdf_data.find("%%EOF") == std::string::npos || + !oversized_rejected || !invalid_snapshot_rejected) { std::cerr << "DOCX round trip failed: write=" << written << " read=" << read << " pdf=" << pdf_written - << " bytes=" << result.size() << '\n'; + << " bytes=" << result.size() + << " oversizedRejected=" << oversized_rejected + << " invalidSnapshotRejected=" + << invalid_snapshot_rejected << '\n'; return 2; } std::cout << "DOCX round trip passed (" << result.size() << " RTF bytes)\n"; diff --git a/src/port/original/opus_original_startup_probe.cpp b/src/port/original/opus_original_startup_probe.cpp index f54e32c..e376307 100644 --- a/src/port/original/opus_original_startup_probe.cpp +++ b/src/port/original/opus_original_startup_probe.cpp @@ -374,28 +374,6 @@ LONG WINAPI ObserveVectoredException(EXCEPTION_POINTERS* exception) { } // namespace -extern "C" void OpusX64TraceRibbon(const char* stage, int message, int tmc, - int first_value, int second_value, - long cp_first, long cp_limit, - int insertion) { - char trace_path[MAX_PATH] = {}; - BuildDiagnosticPath("WORD1-ribbon.txt", trace_path, sizeof(trace_path)); - HANDLE file = CreateFileA(trace_path, FILE_APPEND_DATA, - FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, - OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (file == INVALID_HANDLE_VALUE) { - return; - } - char line[384] = {}; - std::snprintf(line, sizeof(line), - "%llu %s msg=%d tmc=%d a=%d b=%d sel=%ld,%ld ins=%d\r\n", - static_cast(GetTickCount64()), - stage != nullptr ? stage : "", message, tmc, first_value, - second_value, cp_first, cp_limit, insertion); - WriteCrashText(file, line); - CloseHandle(file); -} - int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, PWSTR command_line, int show_command) { if ((command_line != nullptr && @@ -404,6 +382,15 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, return 0; } + /* Exclude the current directory and PATH from DLL resolution. The app + directory remains available for intentionally deployed components and + system DLLs are resolved only from System32. */ + SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | + LOAD_LIBRARY_SEARCH_SYSTEM32 | + LOAD_LIBRARY_SEARCH_USER_DIRS); + SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE | + BASE_SEARCH_PATH_PERMANENT); + SetUnhandledExceptionFilter(WriteCrashStack); AddVectoredExceptionHandler(1, ObserveVectoredException); _RTC_SetErrorFuncW(WriteRtcFailure); diff --git a/src/port/original/opus_sdm_runtime.cpp b/src/port/original/opus_sdm_runtime.cpp index 7f8d760..c359e69 100644 --- a/src/port/original/opus_sdm_runtime.cpp +++ b/src/port/original/opus_sdm_runtime.cpp @@ -1,11 +1,13 @@ #include "opus_x64_compat.h" #include "opus_x64_heap.h" #include +#include extern "C" { #include "dac.h" } #include +#include #include #include #include @@ -21,12 +23,18 @@ extern std::uintptr_t wRefDlgCur; extern HWND vhWndMsgBoxParent; void GetCabSz(void**, char*, std::uint16_t, std::uint16_t); int FSetCabSz(void**, const char*, std::uint16_t); -void OpusX64TraceRibbon(const char*, int, int, int, int, long, long, int); int OpusModernPathIsDocx(const char*); int OpusModernDocxToTextFile(const char*, const char*); int OpusModernRtfFileToDocx(const char*, const char*); } +extern "C" void OpusX64TraceRibbon(const char*, int, int, int, int, + long, long, int) { + /* Ribbon tracing used during the bring-up of the x64 port. Keep the ABI + for original call sites without writing diagnostic files in product or + test processes. */ +} + /* * Flat, stateful implementation of the public SDM 2.21 dialog API used by * Opus. The archive contains the SDM headers and 16-bit .obj files, but not @@ -131,6 +139,7 @@ struct Win95SaveAlias { Win95SaveAlias g_win95_save_alias; std::unordered_map g_win95_saved_aliases; +std::string g_win95_staging_directory; struct Win95AliasCleanup { ~Win95AliasCleanup() { @@ -141,6 +150,9 @@ struct Win95AliasCleanup { for (const auto& saved : g_win95_saved_aliases) { DeleteFileA(saved.first.c_str()); } + if (!g_win95_staging_directory.empty()) { + RemoveDirectoryA(g_win95_staging_directory.c_str()); + } } }; @@ -154,43 +166,131 @@ std::string win95_alias_key(std::string path) { return path; } +bool safe_dialog_file_path(const std::string& path, const bool must_exist) { + if (path.empty() || path.size() >= 32760 || + path.starts_with(R"(\\.\)") || + path.starts_with(R"(\\?\GLOBALROOT\)")) return false; + for (std::size_t index = 0; index < path.size(); ++index) { + if (path[index] == ':' && index != 1) return false; + } + const DWORD attributes = GetFileAttributesA(path.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES) + return (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; + return !must_exist && (GetLastError() == ERROR_FILE_NOT_FOUND || + GetLastError() == ERROR_PATH_NOT_FOUND); +} + +bool import_file_within_limit(const std::string& path) { + if (!safe_dialog_file_path(path, true)) return false; + WIN32_FILE_ATTRIBUTE_DATA attributes{}; + if (!GetFileAttributesExA(path.c_str(), GetFileExInfoStandard, + &attributes)) return false; + constexpr ULONGLONG kMaximumImportBytes = 256ull * 1024ull * 1024ull; + const ULONGLONG size = + (static_cast(attributes.nFileSizeHigh) << 32) | + attributes.nFileSizeLow; + return size <= kMaximumImportBytes; +} + bool make_win95_staging_path(std::string& path, const char* desired_extension = ".DOC") { - char module_path[32768]{}; - const DWORD module_length = GetModuleFileNameA( - nullptr, module_path, static_cast(std::size(module_path))); - if (module_length == 0 || module_length >= std::size(module_path)) { - return false; - } - char* separator = std::strrchr(module_path, '\\'); - if (separator == nullptr) { - return false; - } - *separator = '\0'; - std::string directory = std::string(module_path) + "\\W95TEMP"; - if (!CreateDirectoryA(directory.c_str(), nullptr) && - GetLastError() != ERROR_ALREADY_EXISTS) { - return false; - } - for (int attempt = 0; attempt < 32; ++attempt) { - char temporary[MAX_PATH]{}; - if (GetTempFileNameA(directory.c_str(), "W95", 0, temporary) == 0) { + if (_stricmp(desired_extension, ".DOC") != 0 && + _stricmp(desired_extension, ".TXT") != 0) return false; + if (g_win95_staging_directory.empty()) { + char temporary_root[32768]{}; + const DWORD root_length = GetTempPathA( + static_cast(std::size(temporary_root)), temporary_root); + if (root_length == 0 || root_length >= std::size(temporary_root)) return false; + for (int attempt = 0; attempt < 32; ++attempt) { + GUID identifier{}; + if (FAILED(CoCreateGuid(&identifier))) return false; + char leaf[9]{}; + _snprintf_s(leaf, std::size(leaf), _TRUNCATE, "W%07lX", + identifier.Data1 & 0x0ffffffful); + const std::string candidate = std::string(temporary_root) + leaf; + /* The original normalizer requires DOS 8.3-safe path components + even though the native file APIs support long names. */ + if (candidate.size() >= 52) return false; + if (CreateDirectoryA(candidate.c_str(), nullptr)) { + const DWORD attributes = GetFileAttributesA(candidate.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || + (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + RemoveDirectoryA(candidate.c_str()); + return false; + } + g_win95_staging_directory = candidate; + break; + } + if (GetLastError() != ERROR_ALREADY_EXISTS) return false; } - DeleteFileA(temporary); - char* existing_extension = std::strrchr(temporary, '.'); - if (existing_extension != nullptr) { - lstrcpyA(existing_extension, desired_extension); - } - if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES && - std::strlen(temporary) < 120) { - path = temporary; + if (g_win95_staging_directory.empty()) return false; + } + static std::atomic sequence{0}; + for (int attempt = 0; attempt < 32; ++attempt) { + char leaf[18]{}; + _snprintf_s(leaf, std::size(leaf), _TRUNCATE, "\\D%07lX%s", + ++sequence & 0x0ffffffful, desired_extension); + const std::string candidate = g_win95_staging_directory + leaf; + if (candidate.size() >= 120) return false; + const DWORD attributes = GetFileAttributesA(candidate.c_str()); + const DWORD attribute_error = GetLastError(); + if (attributes == INVALID_FILE_ATTRIBUTES && + (attribute_error == ERROR_FILE_NOT_FOUND || + attribute_error == ERROR_PATH_NOT_FOUND)) { + path = candidate; return true; } } return false; } +bool atomic_copy_file(const std::string& source, const std::string& target) { + if (source.empty() || target.empty()) return false; + static std::atomic sequence{0}; + std::string temporary; + bool copied = false; + for (int attempt = 0; attempt < 64; ++attempt) { + temporary = target + ".word1tmp-" + + std::to_string(GetCurrentProcessId()) + "-" + + std::to_string(++sequence); + if (CopyFileA(source.c_str(), temporary.c_str(), TRUE)) { + copied = true; + break; + } + if (GetLastError() != ERROR_FILE_EXISTS && + GetLastError() != ERROR_ALREADY_EXISTS) return false; + } + if (!copied) return false; + HANDLE file = CreateFileA(temporary.c_str(), GENERIC_WRITE, + FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + const bool flushed = file != INVALID_HANDLE_VALUE && + FlushFileBuffers(file) != FALSE; + if (file != INVALID_HANDLE_VALUE) CloseHandle(file); + if (!flushed) { + DeleteFileA(temporary.c_str()); + return false; + } + const DWORD attributes = GetFileAttributesA(target.c_str()); + bool committed = false; + if (attributes != INVALID_FILE_ATTRIBUTES && + (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + committed = ReplaceFileA(target.c_str(), temporary.c_str(), nullptr, + REPLACEFILE_WRITE_THROUGH, nullptr, nullptr) != FALSE; + if (!committed) { + committed = MoveFileExA(temporary.c_str(), target.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != FALSE; + } + } else if (attributes == INVALID_FILE_ATTRIBUTES && + GetLastError() == ERROR_FILE_NOT_FOUND) { + committed = MoveFileExA(temporary.c_str(), target.c_str(), + MOVEFILE_WRITE_THROUGH) != FALSE; + } + if (!committed) DeleteFileA(temporary.c_str()); + return committed; +} + std::string counted_path(const unsigned char* st_file) { if (st_file == nullptr || st_file[0] == 0 || st_file[0] >= 120) { return {}; @@ -2187,9 +2287,13 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { } char test_path[32768]{}; +#ifdef OPUS_TEST_HOOKS const DWORD test_path_length = GetEnvironmentVariableA( "WORD1_TEST_FILE_DIALOG_PATH", test_path, static_cast(std::size(test_path))); +#else + const DWORD test_path_length = 0; +#endif BOOL accepted = FALSE; if (test_path_length > 0 && test_path_length < std::size(test_path)) { lstrcpynA(file_buffer.data(), test_path, @@ -2197,7 +2301,9 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { if (saving && OpusModernPathIsDocx(test_path)) { file_dialog.nFilterIndex = 2; } +#ifdef OPUS_TEST_HOOKS SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr); +#endif accepted = TRUE; } else { accepted = opening ? GetOpenFileNameA(&file_dialog) : @@ -2232,6 +2338,16 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { ".docx"); } } + if ((opening && !import_file_within_limit(selected_path)) || + (saving && !safe_dialog_file_path(selected_path, false))) { + MessageBoxA(owner, + opening ? + "This document is not a regular file or is too large to open safely." : + "This is not a safe document file name.", + opening ? "Open" : "Save As", + MB_OK | MB_ICONEXCLAMATION); + continue; + } std::string legacy_path; const bool docx = OpusModernPathIsDocx(selected_path.c_str()) != 0; if (!make_win95_staging_path(legacy_path, @@ -2428,8 +2544,8 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, OpusModernRtfFileToDocx( g_win95_save_alias.legacy_path.c_str(), g_win95_save_alias.selected_path.c_str()) != 0 : - CopyFileA(g_win95_save_alias.legacy_path.c_str(), - g_win95_save_alias.selected_path.c_str(), FALSE) != 0; + atomic_copy_file(g_win95_save_alias.legacy_path, + g_win95_save_alias.selected_path); } if (!success) { DeleteFileA(g_win95_save_alias.legacy_path.c_str()); @@ -2450,7 +2566,7 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, } return OpusModernPathIsDocx(saved->second.c_str()) ? OpusModernRtfFileToDocx(path.c_str(), saved->second.c_str()) != 0 : - CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0; + atomic_copy_file(path, saved->second); } int OpusWin95SaveAliasRequiresRtf(const unsigned char* st_file) { diff --git a/src/port/original/opus_win95_chrome.cpp b/src/port/original/opus_win95_chrome.cpp index 63c061e..08c16a5 100644 --- a/src/port/original/opus_win95_chrome.cpp +++ b/src/port/original/opus_win95_chrome.cpp @@ -227,7 +227,8 @@ int scale(HWND window, int value) { void set_window_classic(HWND window) { using SetWindowThemeProc = HRESULT(WINAPI*)(HWND, LPCWSTR, LPCWSTR); - static HMODULE theme_module = LoadLibraryW(L"uxtheme.dll"); + static HMODULE theme_module = LoadLibraryExW( + L"uxtheme.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); static const auto set_theme = theme_module != nullptr ? reinterpret_cast( GetProcAddress(theme_module, "SetWindowTheme")) : nullptr; @@ -454,7 +455,8 @@ void configure_word95_menus(HWND window) { void apply_caption_colors(HWND window) { using DwmSetWindowAttributeProc = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); - HMODULE module = LoadLibraryW(L"dwmapi.dll"); + HMODULE module = LoadLibraryExW( + L"dwmapi.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); if (module == nullptr) { return; } diff --git a/src/port/original/opus_word1_ui_test.cpp b/src/port/original/opus_word1_ui_test.cpp index 056531f..9759eca 100644 --- a/src/port/original/opus_word1_ui_test.cpp +++ b/src/port/original/opus_word1_ui_test.cpp @@ -959,8 +959,10 @@ int wmain(const int argument_count, wchar_t** arguments) { } } bool rich_pdf_valid = !docx_pdf_export_mode; - if (docx_pdf_export_mode && - PostMessageW(main_window, kWmCommand, kExportPdf, 0)) { + DWORD_PTR export_result = 0; + if (docx_pdf_export_mode && SendMessageTimeoutW( + main_window, kWmCommand, kExportPdf, 0, + SMTO_ABORTIFHUNG | SMTO_BLOCK, 30000, &export_result)) { const ULONGLONG pdf_deadline = GetTickCount64() + 15000; do { std::ifstream input(pdf_path, std::ios::binary); @@ -1001,7 +1003,11 @@ int wmain(const int argument_count, wchar_t** arguments) { << "} bodyFont={index:" << body_font_index << ",charset:" << body_font_charset << "}\n"; if (docx_pdf_export_mode) - std::cerr << "richPdf=" << rich_pdf_valid << '\n'; + std::cerr << "richPdf=" << rich_pdf_valid + << " exportStage=" + << (pane != nullptr ? SendMessageW( + pane, kWmOpusX64QuerySelection, 105, 0) : -1) + << '\n'; if (!keep_pdf && !pdf_path.empty()) DeleteFileA(pdf_path.c_str()); return 84; } diff --git a/src/port/original/opus_x64_heap.cpp b/src/port/original/opus_x64_heap.cpp index 113b8be..aa3e2c5 100644 --- a/src/port/original/opus_x64_heap.cpp +++ b/src/port/original/opus_x64_heap.cpp @@ -29,14 +29,18 @@ static_assert(offsetof(NativeHandle, data) == 0); constexpr int kPrcTokenNil = 0x3fff; constexpr int kPrcTokenMac = kPrcTokenNil - 1; constexpr int kCompactHandleMac = 0x3fff; +constexpr std::size_t kNativeHandleSlots = 131071; constexpr std::size_t kSegmentSlots = 65536; constexpr std::size_t kGuardBytes = 64; +constexpr std::size_t kMaxAllocationBytes = 256u * 1024u * 1024u; constexpr unsigned char kGuardValue = 0xa5; SRWLOCK prc_registry_lock = SRWLOCK_INIT; SRWLOCK compact_handle_lock = SRWLOCK_INIT; +SRWLOCK native_handle_lock = SRWLOCK_INIT; SRWLOCK segment_registry_lock = SRWLOCK_INIT; void** prc_handles[kPrcTokenNil]{}; void** compact_handles[kCompactHandleMac + 1]{}; +void** native_handles[kNativeHandleSlots]{}; std::array native_segments{}; std::atomic_size_t heap_bytes_used{0}; @@ -47,6 +51,7 @@ std::atomic_size_t heap_bytes_used{0}; [[nodiscard]] constexpr bool guarded_size( const std::size_t logical_size, std::size_t* result) noexcept { + if (logical_size > kMaxAllocationBytes) return false; const std::size_t payload = payload_size(logical_size); if (payload > (std::numeric_limits::max)() - kGuardBytes) { return false; @@ -55,6 +60,75 @@ std::atomic_size_t heap_bytes_used{0}; return true; } +[[nodiscard]] bool valid_native_handle(void** opaque_handle) noexcept { + if (opaque_handle == nullptr) return false; + void** const tombstone = reinterpret_cast(static_cast(1)); + const std::size_t start = + (reinterpret_cast(opaque_handle) >> 4) % kNativeHandleSlots; + AcquireSRWLockShared(&native_handle_lock); + bool valid = false; + for (std::size_t probe = 0; probe < kNativeHandleSlots; ++probe) { + void** const candidate = + native_handles[(start + probe) % kNativeHandleSlots]; + if (candidate == opaque_handle) { + valid = true; + break; + } + if (candidate == nullptr) break; + if (candidate == tombstone) continue; + } + ReleaseSRWLockShared(&native_handle_lock); + return valid; +} + +[[nodiscard]] bool register_native_handle(void** opaque_handle) noexcept { + void** const tombstone = reinterpret_cast(static_cast(1)); + const std::size_t start = + (reinterpret_cast(opaque_handle) >> 4) % kNativeHandleSlots; + AcquireSRWLockExclusive(&native_handle_lock); + std::size_t first_tombstone = kNativeHandleSlots; + for (std::size_t probe = 0; probe < kNativeHandleSlots; ++probe) { + const std::size_t index = (start + probe) % kNativeHandleSlots; + if (native_handles[index] == opaque_handle) { + ReleaseSRWLockExclusive(&native_handle_lock); + return true; + } + if (native_handles[index] == tombstone && + first_tombstone == kNativeHandleSlots) first_tombstone = index; + if (native_handles[index] == nullptr) { + native_handles[first_tombstone == kNativeHandleSlots ? + index : first_tombstone] = opaque_handle; + ReleaseSRWLockExclusive(&native_handle_lock); + return true; + } + } + if (first_tombstone != kNativeHandleSlots) { + native_handles[first_tombstone] = opaque_handle; + ReleaseSRWLockExclusive(&native_handle_lock); + return true; + } + ReleaseSRWLockExclusive(&native_handle_lock); + return false; +} + +[[nodiscard]] bool unregister_native_handle(void** opaque_handle) noexcept { + void** const tombstone = reinterpret_cast(static_cast(1)); + const std::size_t start = + (reinterpret_cast(opaque_handle) >> 4) % kNativeHandleSlots; + AcquireSRWLockExclusive(&native_handle_lock); + for (std::size_t probe = 0; probe < kNativeHandleSlots; ++probe) { + const std::size_t index = (start + probe) % kNativeHandleSlots; + if (native_handles[index] == opaque_handle) { + native_handles[index] = tombstone; + ReleaseSRWLockExclusive(&native_handle_lock); + return true; + } + if (native_handles[index] == nullptr) break; + } + ReleaseSRWLockExclusive(&native_handle_lock); + return false; +} + void set_guard(void* const allocation, const std::size_t logical_size) noexcept { std::memset(static_cast(allocation) + @@ -126,9 +200,11 @@ extern "C" HP OpusHpOfSbIbImpl(const SB segment, const uintptr_t offset) { return reinterpret_cast(static_cast(segment) + offset); } AcquireSRWLockShared(&segment_registry_lock); - void* base = native_segments[static_cast(segment)].data; + const auto& slot = native_segments[static_cast(segment)]; + void* base = slot.data; + const std::size_t size = slot.size; ReleaseSRWLockShared(&segment_registry_lock); - return base == nullptr + return base == nullptr || offset > size ? nullptr : reinterpret_cast(reinterpret_cast(base) + offset); @@ -323,7 +399,7 @@ extern "C" int FAssureHcb(void*** handle_address, const int byte_count_needed, int* byte_count, int* byte_capacity) { if (handle_address == nullptr || byte_count_needed < 0 || - byte_capacity == nullptr) { + byte_capacity == nullptr || *byte_capacity < 0) { return 0; } if (byte_count != nullptr) { @@ -332,7 +408,10 @@ extern "C" int FAssureHcb(void*** handle_address, if (*handle_address != nullptr && byte_count_needed <= *byte_capacity) { return 1; } - const int doubled = *byte_capacity > 0 ? *byte_capacity * 2 : 16; + const int doubled = *byte_capacity > + (std::numeric_limits::max)() / 2 ? + (std::numeric_limits::max)() : + (*byte_capacity > 0 ? *byte_capacity * 2 : 16); const int new_capacity = (std::max)(byte_count_needed, doubled); if (*handle_address == nullptr) { *handle_address = OpusHAllocateCb(static_cast(new_capacity)); @@ -397,6 +476,11 @@ extern "C" void** OpusHAllocateCb(const std::size_t byte_count) { } set_guard(handle->data, byte_count); handle->size = byte_count; + if (!register_native_handle(reinterpret_cast(handle))) { + HeapFree(process_heap(), 0, handle->data); + HeapFree(process_heap(), 0, handle); + return nullptr; + } heap_bytes_used.fetch_add(byte_count, std::memory_order_relaxed); return reinterpret_cast(handle); } @@ -405,6 +489,9 @@ extern "C" void OpusFreeH(void** opaque_handle) { if (opaque_handle == nullptr) { return; } + if (!unregister_native_handle(opaque_handle)) { + report_guard_failure(); + } unregister_prc_handle(opaque_handle); unregister_compact_handle(opaque_handle); auto* handle = reinterpret_cast(opaque_handle); @@ -433,6 +520,9 @@ extern "C" int OpusFChngSizeHCb(void** opaque_handle, if (opaque_handle == nullptr) { return 0; } + if (!valid_native_handle(opaque_handle)) { + report_guard_failure(); + } auto* handle = reinterpret_cast(opaque_handle); if (!allow_shrink && byte_count <= handle->size) { return 1; @@ -470,7 +560,7 @@ extern "C" int OpusFChngSizePhqLcb(void*** handle_address, } extern "C" size_t OpusCbOfH(void** opaque_handle) { - return opaque_handle == nullptr + return !valid_native_handle(opaque_handle) ? 0 : reinterpret_cast(opaque_handle)->size; } @@ -480,12 +570,13 @@ extern "C" size_t OpusHeapBytesUsed() { } extern "C" void* OpusDerefH(void** opaque_handle) { - return opaque_handle == nullptr + return !valid_native_handle(opaque_handle) ? nullptr : reinterpret_cast(opaque_handle)->data; } extern "C" void* OpusHpAlloc(const std::size_t byte_count) { + if (byte_count > kMaxAllocationBytes) return nullptr; void* allocation = HeapAlloc(process_heap(), 0, byte_count == 0 ? 1 : byte_count); if (allocation != nullptr) { @@ -500,9 +591,8 @@ extern "C" void* OpusHpAlloc(const std::size_t byte_count) { extern "C" void OpusFreeHp(void* pointer) { if (pointer != nullptr) { const SIZE_T size = HeapSize(process_heap(), 0, pointer); - if (size != static_cast(-1)) { - heap_bytes_used.fetch_sub(size, std::memory_order_relaxed); - } + if (size == static_cast(-1)) report_guard_failure(); + heap_bytes_used.fetch_sub(size, std::memory_order_relaxed); HeapFree(process_heap(), 0, pointer); } } diff --git a/src/port/original/opus_x64_runtime_test.cpp b/src/port/original/opus_x64_runtime_test.cpp index b7e0677..8992742 100644 --- a/src/port/original/opus_x64_runtime_test.cpp +++ b/src/port/original/opus_x64_runtime_test.cpp @@ -325,7 +325,8 @@ int main() { EndSdm(); return 23; } - TestDltHeader modal_template{{8, 24, 206, 104}, 3, 0x8400, + /* Character is native-modal but is not routed to a common file dialog. */ + TestDltHeader modal_template{{8, 24, 206, 104}, 16, 0x8400, reinterpret_cast(ModalRuntimeProbe), 11, 4}; auto* modal_template_pointer = &modal_template;