Possible crash when (not) writing the compact size of the script/prevector #122

issue Sjors opened this issue on December 2, 2024
  1. Sjors commented at 7:27 PM on December 2, 2024: member

    Discovered while testing the Template Provider.

    https://github.com/Sjors/bitcoin/issues/71#issuecomment-2512541161

    There's a chance this bug isn't in multiprocess but in a rebasing or other mistake on my end, but it's probably worth looking into.

  2. maflcko commented at 7:29 PM on December 2, 2024: contributor

    You can reproduce with https://github.com/bitcoin/bitcoin/pull/30437 and

    diff --git a/src/bitcoin-mine.cpp b/src/bitcoin-mine.cpp
    index 4d5faea23e..08281b57d4 100644
    --- a/src/bitcoin-mine.cpp
    +++ b/src/bitcoin-mine.cpp
    @@ -120,5 +120,7 @@ MAIN_FUNCTION
             tfm::format(std::cout, "Tip hash is null.\n");
         }
     
    +    (void)mining->createNewBlock({});
    +
         return EXIT_SUCCESS;
     }
    
  3. Sjors commented at 7:39 PM on December 2, 2024: member

    @maflcko does it crash with {CScript() << OP_TRUE} as well?

  4. maflcko commented at 7:58 PM on December 2, 2024: contributor

    Yes, with:

    ==48391==    by 0x11CCFD7: SpanReader::read(Span<std::byte>) 
    ==48391==    by 0x9466E5: ParamsStream<SpanReader&, TransactionSerParams>::read(Span<std::byte>) (serialize.h:1134)
    ==48391==    by 0x991269: void Unserialize<ParamsStream<SpanReader&, TransactionSerParams>, 28u, unsigned char>(ParamsStream<SpanReader&, TransactionSerParams>&, prevector<28u, unsigned char, unsigned int, int>&) (serialize.h:830)
    ==48391==    by 0x99117D: void UnserializeMany<ParamsStream<SpanReader&, TransactionSerParams>, prevector<28u, unsigned char, unsigned int, int>&>(ParamsStream<SpanReader&, TransactionSerParams>&, prevector<28u, unsigned char, unsigned int, int>&) (serialize.h:1000)
    ==48391==    by 0x99112D: void ActionUnserialize::SerReadWriteMany<ParamsStream<SpanReader&, TransactionSerParams>, prevector<28u, unsigned char, unsigned int, int>&>(ParamsStream<SpanReader&, TransactionSerParams>&, prevector<28u, unsigned char, unsigned int, int>&) (serialize.h:1032)
    ==48391==    by 0x9910DD: void CScript::SerializationOps<ParamsStream<SpanReader&, TransactionSerParams>, CScript, ActionUnserialize>(CScript&, ParamsStream<SpanReader&, TransactionSerParams>&, ActionUnserialize) (script.h:465)
    ==48391==    by 0x99107D: void CScript::Unser<ParamsStream<SpanReader&, TransactionSerParams> >(ParamsStream<SpanReader&, TransactionSerParams>&, CScript&) (script.h:465)
    ==48391==    by 0x99102D: void CScript::Unserialize<ParamsStream<SpanReader&, TransactionSerParams> >(ParamsStream<SpanReader&, TransactionSerParams>&) (script.h:465)
    ==48391==    by 0xA1049F: auto decltype(auto) mp::CustomReadField<CScript, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>, mp::ReadDestEmplace<CScript, mp::PassField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, CScript const&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&> >(mp::Priority<0>, mp::TypeList<CScript const&>, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&>&&)::{lambda((auto:1&&)...)#1}> >(mp::TypeList<CScript>, mp::Priority<1>, mp::InvokeContext&, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>&&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&&) requires (Unserializable<CScript, DataStream>)&&(!(ipc::capnp::Deserializable<CScript>))::{lambda(auto:1&)#1}::operator()<CScript>(CScript&) const (common-types.h:78)
    ==48391==    by 0xA1037A: decltype(auto) mp::ReadDestEmplace<CScript, mp::PassField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, CScript const&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&> >(mp::Priority<0>, mp::TypeList<CScript const&>, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&>&&)::{lambda((auto:1&&)...)#1}>::update<decltype(auto) mp::CustomReadField<CScript, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>, mp::ReadDestEmplace<CScript, mp::PassField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, CScript const&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&> >(mp::Priority<0>, mp::TypeList<CScript const&>, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&>&&)::{lambda((auto:1&&)...)#1}> >(mp::TypeList<CScript>, mp::Priority<1>, mp::InvokeContext&, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>&&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&&) requires (Unserializable<CScript, DataStream>)&&(!(ipc::capnp::Deserializable<CScript>))::{lambda(auto:1&)#1}>(decltype(auto) mp::CustomReadField<CScript, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>, mp::ReadDestEmplace<CScript, mp::PassField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, CScript const&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&> >(mp::Priority<0>, mp::TypeList<CScript const&>, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&>&&)::{lambda((auto:1&&)...)#1}> >(mp::TypeList<CScript>, mp::Priority<1>, mp::InvokeContext&, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>&&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&&) requires (Unserializable<CScript, DataStream>)&&(!(ipc::capnp::Deserializable<CScript>))::{lambda(auto:1&)#1}&&) (proxy-types.h:266)
    ==48391==    by 0xA10319: decltype(auto) mp::CustomReadField<CScript, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>, mp::ReadDestEmplace<CScript, mp::PassField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, CScript const&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&> >(mp::Priority<0>, mp::TypeList<CScript const&>, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&, mp::ServerField<1, mp::Accessor<mp::mining_fields::Options, 17>, mp::ServerRet<mp::Accessor<mp::mining_fields::Result, 18>, mp::ServerCall> > const&, mp::TypeList<node::BlockCreateOptions const&>&&)::{lambda((auto:1&&)...)#1}> >(mp::TypeList<CScript>, mp::Priority<1>, mp::InvokeContext&, mp::StructField<mp::Accessor<mp::mining_fields::ScriptPubKey, 17>, ipc::capnp::messages::Mining::CreateNewBlockParams::Reader const>&&, mp::ServerInvokeContext<mp::ProxyServer<ipc::capnp::messages::Mining>, capnp::CallContext<ipc::capnp::messages::Mining::CreateNewBlockParams, ipc::capnp::messages::Mining::CreateNewBlockResults> >&&) requires (Unserializable<CScript, DataStream>)&&(!(ipc::capnp::Deserializable<CScript>)) (common-types.h:73)
    
  5. ryanofsky commented at 8:38 PM on December 2, 2024: collaborator

    I'm guessing serialization code is confused by CScript being convertible to a span of bytes but also having serialize and deserialize methods. So it is being sent as raw bytes:

    https://github.com/bitcoin/bitcoin/blob/ebe4cac38bf6c510b00b8e080acab079c54016d6/src/ipc/capnp/common-types.h#L151-L157

    which receiving code tries to pass to Unserialize

    https://github.com/bitcoin/bitcoin/blob/ebe4cac38bf6c510b00b8e080acab079c54016d6/src/ipc/capnp/common-types.h#L69-L80

    Probably the easiest way to fix this would be to add a Priority<2> CustomReadField for CScript that calls the CScript constructor constructing it from raw bytes:

    https://github.com/bitcoin/bitcoin/blob/ebe4cac38bf6c510b00b8e080acab079c54016d6/src/script/script.h#L463

    Alternately, it might make sense for cscript to have a std::span<std::byte> constructor and for there to be a generic CustomReadField function which calls that constructor

  6. maflcko commented at 9:31 AM on December 5, 2024: contributor

    I am not sure about fixing this with additional bandages on top. If code can lead to runtime errors, it should ideally fail compilation in the first place. Otherwise, this seems like a cat-and-mouse game where a user runs into a random crash and then has to wait for developers to fix it in the next release, if it is reported at all. Or even worse, a roundrtip mismatch will silently corrupt the data.

    My suggested fix would be:

    • Use the same requires-constraints for serialization and deserialization, to ensure that serialize.h-serialization or span-serialization is used both ways, or not at all.
    • If neither is available for a consistent round-trip serialization, fail compilation
  7. ryanofsky commented at 4:54 PM on December 5, 2024: collaborator

    Those are good thoughts.

    I do think this problem is mostly just caused by the unusual CustomBuildField span<byte> overload. Normally Build/Read overloads (or Build/Pass overloads) are paired together, but this Build overload is unpaired. I originally added it to support passing GCSFilter::ElementSet arguments, and it got used more places after that like BlockTemplate::getCoinbaseCommitment method currently, and I never got around to cleaning it up and implementing TODO there. I think if I added a corresponding Read overload to that Build overload, and restricted them both to only work on the same types, it would be a reasonable and safe fix for this problem.

    Another possible improvement and fix could be to change capnp proto schema. Instead of allowing bitcoin serialization be applied to any capnp Data field, the schema could define a new type like struct SerializedData { data [@0](/bitcoin-core-multiprocess/contributor/0/) :Data; } and only allow bitcoin serialization to and from SerializedData fields instead of Data fields. This would add a little bit extra overhead to the representation though, which is why I didn't implement it that way initially.

    The fix you suggest also could make sense but I would have to think more about how to implement it.

  8. ryanofsky referenced this in commit 5fdab5ce89 on Jan 27, 2025
  9. ryanofsky referenced this in commit 7d59b8d14c on Jan 27, 2025
  10. ryanofsky closed this on Jan 27, 2025

  11. ryanofsky referenced this in commit 9983eb6f6b on Jan 27, 2025
  12. ryanofsky referenced this in commit 54c490c883 on Jan 27, 2025
  13. ryanofsky referenced this in commit 3dea1d555a on Jan 27, 2025
  14. ryanofsky referenced this in commit 4e0aa1835b on Jan 27, 2025
  15. Sjors referenced this in commit 3a05843d3c on Jan 28, 2025
  16. janus referenced this in commit 72a74acddd on Sep 1, 2025
  17. bitcoin-core locked this on Jan 27, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/libmultiprocess. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-20 18:30 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me