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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- WebAssembly rejects reentrant calls from callbacks instead of allowing a
callback to close or mutate a document during an active native operation.

- WebAssembly rejects fractional, nonfinite and out-of-range element IDs,
sheet coordinates, rendering limits and view indices before conversion.

Expand Down
2 changes: 2 additions & 0 deletions wasm/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ structured-cloned. Three rules follow.
is called during a render, and one that needs the main thread deadlocks
behind a `postMessage` round trip. The same holds for a future resource
locator.
- `guarded` rejects reentrant calls: callbacks must not invalidate an active
operation's session, document or view. `CallScope` resets during unwinding.
- The package is plain JavaScript with a hand-written `js/index.d.ts`. Keep it
in step with `js/index.js` by hand.
- Test inputs are built in memory. `tests/helper.mjs` has a zip writer.
Expand Down
11 changes: 11 additions & 0 deletions wasm/src/odr_wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ namespace odr::wasm {

namespace {

bool active_call{false};

std::unordered_map<Handle, Session> &sessions() {
static std::unordered_map<Handle, Session> instance;
return instance;
Expand All @@ -30,6 +32,15 @@ emscripten::val error_for(const std::exception &e) {

} // namespace

CallScope::CallScope() {
if (active_call) {
throw std::logic_error("reentrant calls into odr are not supported");
}
active_call = true;
}

CallScope::~CallScope() { active_call = false; }

Session &session(const Handle handle) {
const auto it = sessions().find(handle);
if (it == sessions().end()) {
Expand Down
10 changes: 10 additions & 0 deletions wasm/src/odr_wasm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,18 @@ emscripten::val error(ErrorCode code, const std::string &message);
/// The envelope for the exception being handled. Call from a `catch` block.
emscripten::val current_exception_error();

/// Prevents callbacks from invalidating a session used by an active call.
class CallScope final {
public:
CallScope();
~CallScope();
CallScope(const CallScope &) = delete;
CallScope &operator=(const CallScope &) = delete;
};

template <typename F> emscripten::val guarded(F &&f) {
try {
const CallScope scope;
return std::forward<F>(f)();
} catch (...) {
return current_exception_error();
Expand Down
28 changes: 28 additions & 0 deletions wasm/tests/lifetime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@ describe('lifetimes', () => {
});
after(() => odr.closeAll());

it('refuses reentrant callbacks and recovers after JavaScript exceptions', () => {
const doc = odr.open(minimalOdt(), { editable: true });
try {
const id = Number(doc.render().html.match(/<x-s [^>]*data-odr-id="(\d+)"/)[1]);
let called = false;
doc.setTextStyle(id, {
toJSON() {
called = true;
for (const close of [() => doc.close(), () => odr.closeAll()]) {
assert.throws(close, (error) =>
error instanceof OdrError && /reentrant/.test(error.message));
}
return { bold: true };
},
});
assert.equal(called, true);
assert.match(doc.render().html, /font-weight:bold/);

const failure = new Error('style serialization failed');
assert.throws(() => doc.setTextStyle(id, {
toJSON() { throw failure; },
}), (error) => error === failure);
assert.equal(doc.meta().fileType, 'odt');
} finally {
doc.close();
}
});

it('refuses a handle that has been closed', () => {
const doc = odr.open(minimalOdt());
doc.render(0);
Expand Down
Loading