Skip to content

Guard Fd2dBlackScholesVanillaEngine against a non-basket payoff - #3000

Open
leafyoung wants to merge 2 commits into
lballabio:masterfrom
leafyoung:fix-fd2dblackscholesvanillaengine-payoff-check
Open

leafyoung wants to merge 2 commits into
lballabio:masterfrom
leafyoung:fix-fd2dblackscholesvanillaengine-payoff-check

Conversation

@leafyoung

Copy link
Copy Markdown

Summary

Fd2dBlackScholesVanillaEngine::calculate() does:

const ext::shared_ptr<BasketPayoff> payoff =
    ext::dynamic_pointer_cast<BasketPayoff>(arguments_.payoff);

with no check that the cast succeeded. Its sibling FdNdimBlackScholesVanillaEngine performs the identical cast and immediately follows it with QL_REQUIRE(payoff, "basket payoff expected"); — Fd2dBlackScholesVanillaEngine is missing that guard. A failed cast leaves payoff null, which is stored and later dereferenced unconditionally in FdmLogBasketInnerValue::innerValue() via (*payoff_)(x) — undefined behavior instead of a clean, actionable error.

BasketOption's own constructor only accepts an ext::shared_ptr<BasketPayoff>, so this isn't reachable through normal BasketOption construction — but the engine's own getArguments()/calculate() interface is public API independent of any specific instrument wrapper (as the N-dim engine's existing guard already anticipates), and nothing stops a non-basket payoff from being set directly through it.

Fix

Added the same QL_REQUIRE(payoff, "basket payoff expected"); the N-dim sibling already has, right after the cast.

Test

Added testFd2dBlackScholesVanillaEngineRejectsNonBasketPayoff to test-suite/basketoption.cpp: drives the engine directly through getArguments()/calculate() with a non-basket payoff after confirming normal BasketOption usage still works. Without the fix this hits an uncaught Boost shared_ptr null-dereference assertion (px != 0) rather than the library's own clean error; with the fix it throws a QuantLib::Error as expected. Full BasketOptionTests suite (33 cases) passes with no regressions.

🤖 Co-worked with Claude Code

Fd2dBlackScholesVanillaEngine::calculate() dynamic_pointer_casts
arguments_.payoff to BasketPayoff but, unlike its sibling
FdNdimBlackScholesVanillaEngine (which already QL_REQUIREs the cast
to succeed), had no check at all. A failed cast silently produced a
null payoff_, later dereferenced unconditionally in
FdmLogBasketInnerValue::innerValue() -- undefined behavior instead of
a clean error.

Added a regression test that drives the engine directly through its
own getArguments()/calculate() interface (the same entry point
BasketOption itself uses, but without its constructor-level type
safety) with a non-basket payoff. Confirmed it fails (an uncaught
Boost shared_ptr null-dereference assertion, not a crash) without the
fix and passes (a clean QuantLib::Error) with it.

Co-worked with Claude Code
Copilot AI balanced review requested due to automatic review settings November 1, 2026 13:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new quote, process, exercise, and engine allocations use raw new wrapped in shared pointers instead of the repository-required auto with ext::make_shared convention.

1 open issue
What changed in this PR

This PR makes Fd2dBlackScholesVanillaEngine reject non-basket payoffs with a clear QuantLib error instead of risking a null-pointer dereference, matching its N-dimensional sibling.

Changes:

  • Adds a guard immediately after the basket-payoff cast.
  • Adds a regression test covering normal basket pricing and rejection of a non-basket payoff through the engine’s public interface.
File Description
test-suite/​basketoption.cpp Tests valid basket pricing and rejection of an incompatible payoff.
ql/​pricingengines/​basket/​fd2dblackscholesvanillaengine.cpp Requires the basket-payoff cast to succeed before proceeding.

Review effort: Balanced

Comment thread test-suite/basketoption.cpp Resolved
The new quote/process/exercise/engine allocations wrapped raw new
expressions in shared pointers instead of using ext::make_shared, per
the repository's own factory convention. Switched all of them.

Co-worked with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants