diff --git a/CHANGELOG.md b/CHANGELOG.md index b6cc3077f449..4ed8721eda01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,8 +72,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes +* Fix [ABS-0043/ABS-0044](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-8wcc-m6j2-qxvm) Limit recursion depth for unknown field detection and unpack any * (sims) [21906](https://github.com/cosmos/cosmos-sdk/pull/21906) Skip sims test when running dry on validators * (cli) [#21919](https://github.com/cosmos/cosmos-sdk/pull/21919) Query address-by-acc-num by account_id instead of id. +* (cli) [#22656](https://github.com/cosmos/cosmos-sdk/pull/22656) Prune cmd should disable async pruning. ## [v0.50.10](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.10) - 2024-09-20 diff --git a/baseapp/options.go b/baseapp/options.go index 9e066c94c1c4..d4d6052addd1 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -84,6 +84,11 @@ func SetIAVLDisableFastNode(disable bool) func(*BaseApp) { return func(bapp *BaseApp) { bapp.cms.SetIAVLDisableFastNode(disable) } } +// SetIAVLSyncPruning set sync/async pruning in the IAVL store. +func SetIAVLSyncPruning(syncPruning bool) func(*BaseApp) { + return func(bapp *BaseApp) { bapp.cms.SetIAVLSyncPruning(syncPruning) } +} + // SetInterBlockCache provides a BaseApp option function that sets the // inter-block cache. func SetInterBlockCache(cache storetypes.MultiStorePersistentCache) func(*BaseApp) { diff --git a/client/pruning/main.go b/client/pruning/main.go index 51dc5f9c2162..324deb674e82 100644 --- a/client/pruning/main.go +++ b/client/pruning/main.go @@ -44,6 +44,9 @@ Supported app-db-backend types include 'goleveldb', 'rocksdb', 'pebbledb'.`, return err } + // must disable async pruning + vp.Set(server.FlagIAVLSyncPruning, true) + // use the first argument if present to set the pruning method if len(args) > 0 { vp.Set(server.FlagPruning, args[0]) @@ -69,6 +72,7 @@ Supported app-db-backend types include 'goleveldb', 'rocksdb', 'pebbledb'.`, if err != nil { return err } + defer db.Close() logger := log.NewLogger(cmd.OutOrStdout()) app := appCreator(logger, db, nil, vp) diff --git a/codec/types/interface_registry.go b/codec/types/interface_registry.go index daf08a00401c..93fbca7c8850 100644 --- a/codec/types/interface_registry.go +++ b/codec/types/interface_registry.go @@ -1,6 +1,7 @@ package types import ( + "errors" "fmt" "reflect" @@ -12,6 +13,17 @@ import ( "cosmossdk.io/x/tx/signing" ) +var ( + + // MaxUnpackAnySubCalls extension point that defines the maximum number of sub-calls allowed during the unpacking + // process of protobuf Any messages. + MaxUnpackAnySubCalls = 100 + + // MaxUnpackAnyRecursionDepth extension point that defines the maximum allowed recursion depth during protobuf Any + // message unpacking. + MaxUnpackAnyRecursionDepth = 10 +) + // AnyUnpacker is an interface which allows safely unpacking types packed // in Any's against a whitelist of registered types type AnyUnpacker interface { @@ -270,6 +282,45 @@ func (registry *interfaceRegistry) ListImplementations(ifaceName string) []strin } func (registry *interfaceRegistry) UnpackAny(any *Any, iface interface{}) error { + unpacker := &statefulUnpacker{ + registry: registry, + maxDepth: MaxUnpackAnyRecursionDepth, + maxCalls: &sharedCounter{count: MaxUnpackAnySubCalls}, + } + return unpacker.UnpackAny(any, iface) +} + +// sharedCounter is a type that encapsulates a counter value +type sharedCounter struct { + count int +} + +// statefulUnpacker is a struct that helps in deserializing and unpacking +// protobuf Any messages while maintaining certain stateful constraints. +type statefulUnpacker struct { + registry *interfaceRegistry + maxDepth int + maxCalls *sharedCounter +} + +// cloneForRecursion returns a new statefulUnpacker instance with maxDepth reduced by one, preserving the registry and maxCalls. +func (r statefulUnpacker) cloneForRecursion() *statefulUnpacker { + return &statefulUnpacker{ + registry: r.registry, + maxDepth: r.maxDepth - 1, + maxCalls: r.maxCalls, + } +} + +// UnpackAny deserializes a protobuf Any message into the provided interface, ensuring the interface is a pointer. +// It applies stateful constraints such as max depth and call limits, and unpacks interfaces if required. +func (r *statefulUnpacker) UnpackAny(any *Any, iface interface{}) error { + if r.maxDepth == 0 { + return errors.New("max depth exceeded") + } + if r.maxCalls.count == 0 { + return errors.New("call limit exceeded") + } // here we gracefully handle the case in which `any` itself is `nil`, which may occur in message decoding if any == nil { return nil @@ -280,6 +331,8 @@ func (registry *interfaceRegistry) UnpackAny(any *Any, iface interface{}) error return nil } + r.maxCalls.count-- + rv := reflect.ValueOf(iface) if rv.Kind() != reflect.Ptr { return fmt.Errorf("UnpackAny expects a pointer") @@ -295,7 +348,7 @@ func (registry *interfaceRegistry) UnpackAny(any *Any, iface interface{}) error } } - imap, found := registry.interfaceImpls[rt] + imap, found := r.registry.interfaceImpls[rt] if !found { return fmt.Errorf("no registered implementations of type %+v", rt) } @@ -315,7 +368,7 @@ func (registry *interfaceRegistry) UnpackAny(any *Any, iface interface{}) error return err } - err = UnpackInterfaces(msg, registry) + err = UnpackInterfaces(msg, r.cloneForRecursion()) if err != nil { return err } diff --git a/codec/unknownproto/unknown_fields.go b/codec/unknownproto/unknown_fields.go index 9dda4e629b4b..4c08c4fe9a6f 100644 --- a/codec/unknownproto/unknown_fields.go +++ b/codec/unknownproto/unknown_fields.go @@ -40,9 +40,23 @@ func RejectUnknownFieldsStrict(bz []byte, msg proto.Message, resolver jsonpb.Any // This function traverses inside of messages nested via google.protobuf.Any. It does not do any deserialization of the proto.Message. // An AnyResolver must be provided for traversing inside google.protobuf.Any's. func RejectUnknownFields(bz []byte, msg proto.Message, allowUnknownNonCriticals bool, resolver jsonpb.AnyResolver) (hasUnknownNonCriticals bool, err error) { + // recursion limit with same default as https://github.com/protocolbuffers/protobuf-go/blob/v1.35.2/encoding/protowire/wire.go#L28 + return doRejectUnknownFields(bz, msg, allowUnknownNonCriticals, resolver, 10_000) +} + +func doRejectUnknownFields( + bz []byte, + msg proto.Message, + allowUnknownNonCriticals bool, + resolver jsonpb.AnyResolver, + recursionLimit int, +) (hasUnknownNonCriticals bool, err error) { if len(bz) == 0 { return hasUnknownNonCriticals, nil } + if recursionLimit == 0 { + return false, errors.New("recursion limit reached") + } desc, ok := msg.(descriptorIface) if !ok { @@ -130,7 +144,7 @@ func RejectUnknownFields(bz []byte, msg proto.Message, allowUnknownNonCriticals if protoMessageName == ".google.protobuf.Any" { // Firstly typecheck types.Any to ensure nothing snuck in. - hasUnknownNonCriticalsChild, err := RejectUnknownFields(fieldBytes, (*types.Any)(nil), allowUnknownNonCriticals, resolver) + hasUnknownNonCriticalsChild, err := doRejectUnknownFields(fieldBytes, (*types.Any)(nil), allowUnknownNonCriticals, resolver, recursionLimit-1) hasUnknownNonCriticals = hasUnknownNonCriticals || hasUnknownNonCriticalsChild if err != nil { return hasUnknownNonCriticals, err @@ -153,7 +167,7 @@ func RejectUnknownFields(bz []byte, msg proto.Message, allowUnknownNonCriticals } } - hasUnknownNonCriticalsChild, err := RejectUnknownFields(fieldBytes, msg, allowUnknownNonCriticals, resolver) + hasUnknownNonCriticalsChild, err := doRejectUnknownFields(fieldBytes, msg, allowUnknownNonCriticals, resolver, recursionLimit-1) hasUnknownNonCriticals = hasUnknownNonCriticals || hasUnknownNonCriticalsChild if err != nil { return hasUnknownNonCriticals, err diff --git a/server/mock/store.go b/server/mock/store.go index a73c57272f42..45ff433193a2 100644 --- a/server/mock/store.go +++ b/server/mock/store.go @@ -138,6 +138,10 @@ func (ms multiStore) SetIAVLDisableFastNode(disable bool) { panic("not implemented") } +func (ms multiStore) SetIAVLSyncPruning(syncPruning bool) { + panic("not implemented") +} + func (ms multiStore) SetInitialVersion(version int64) error { panic("not implemented") } diff --git a/server/start.go b/server/start.go index dbc575464669..57a25576ac67 100644 --- a/server/start.go +++ b/server/start.go @@ -74,6 +74,7 @@ const ( FlagMinRetainBlocks = "min-retain-blocks" FlagIAVLCacheSize = "iavl-cache-size" FlagDisableIAVLFastNode = "iavl-disable-fastnode" + FlagIAVLSyncPruning = "iavl-sync-pruning" FlagShutdownGrace = "shutdown-grace" // state sync-related flags diff --git a/server/util.go b/server/util.go index 521d13d8a508..09bb5c4a6c07 100644 --- a/server/util.go +++ b/server/util.go @@ -556,6 +556,7 @@ func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) { baseapp.SetSnapshot(snapshotStore, snapshotOptions), baseapp.SetIAVLCacheSize(cast.ToInt(appOpts.Get(FlagIAVLCacheSize))), baseapp.SetIAVLDisableFastNode(cast.ToBool(appOpts.Get(FlagDisableIAVLFastNode))), + baseapp.SetIAVLSyncPruning(cast.ToBool(appOpts.Get(FlagIAVLSyncPruning))), defaultMempool, baseapp.SetChainID(chainID), baseapp.SetQueryGasLimit(cast.ToUint64(appOpts.Get(FlagQueryGasLimit))), diff --git a/store/iavl/store.go b/store/iavl/store.go index 7066891cda19..a37117026fa4 100644 --- a/store/iavl/store.go +++ b/store/iavl/store.go @@ -50,7 +50,12 @@ func LoadStore(db dbm.DB, logger log.Logger, key types.StoreKey, id types.Commit // provided DB. An error is returned if the version fails to load, or if called with a positive // version on an empty tree. func LoadStoreWithInitialVersion(db dbm.DB, logger log.Logger, key types.StoreKey, id types.CommitID, initialVersion uint64, cacheSize int, disableFastNode bool, metrics metrics.StoreMetrics) (types.CommitKVStore, error) { - tree := iavl.NewMutableTree(wrapper.NewDBWrapper(db), cacheSize, disableFastNode, logger, iavl.InitialVersionOption(initialVersion), iavl.AsyncPruningOption(true)) + return LoadStoreWithOpts(db, logger, key, id, initialVersion, cacheSize, disableFastNode, metrics, iavl.AsyncPruningOption(true)) +} + +func LoadStoreWithOpts(db dbm.DB, logger log.Logger, key types.StoreKey, id types.CommitID, initialVersion uint64, cacheSize int, disableFastNode bool, metrics metrics.StoreMetrics, opts ...iavl.Option) (types.CommitKVStore, error) { + opts = append(opts, iavl.InitialVersionOption(initialVersion)) + tree := iavl.NewMutableTree(wrapper.NewDBWrapper(db), cacheSize, disableFastNode, logger, opts...) isUpgradeable, err := tree.IsUpgradeable() if err != nil { diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index 540814b51415..46ca30053897 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -62,6 +62,7 @@ type Store struct { pruningManager *pruning.Manager iavlCacheSize int iavlDisableFastNode bool + iavlSyncPruning bool storesParams map[types.StoreKey]storeParams // CommitStore is a common interface to unify generic CommitKVStore of different value types stores map[types.StoreKey]types.CommitStore @@ -133,6 +134,10 @@ func (rs *Store) SetIAVLDisableFastNode(disableFastNode bool) { rs.iavlDisableFastNode = disableFastNode } +func (rs *Store) SetIAVLSyncPruning(syncPruning bool) { + rs.iavlSyncPruning = syncPruning +} + // GetStoreType implements Store. func (rs *Store) GetStoreType() types.StoreType { return types.StoreTypeMulti @@ -627,6 +632,10 @@ func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStor if storeInfos[key.Name()] { return nil, err } + + // If the store donesn't exist at this version, create a dummy one to prevent + // nil pointer panic in newer query APIs. + cacheStore = dbadapter.Store{DB: dbm.NewMemDB()} } default: @@ -1047,15 +1056,7 @@ func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID panic("recursive MultiStores not yet supported") case types.StoreTypeIAVL: - var store types.CommitKVStore - var err error - - if params.initialVersion == 0 { - store, err = iavl.LoadStore(db, rs.logger, key, id, rs.iavlCacheSize, rs.iavlDisableFastNode, rs.metrics) - } else { - store, err = iavl.LoadStoreWithInitialVersion(db, rs.logger, key, id, params.initialVersion, rs.iavlCacheSize, rs.iavlDisableFastNode, rs.metrics) - } - + store, err := iavl.LoadStoreWithOpts(db, rs.logger, key, id, params.initialVersion, rs.iavlCacheSize, rs.iavlDisableFastNode, rs.metrics, iavltree.AsyncPruningOption(!rs.iavlSyncPruning)) if err != nil { return nil, err } diff --git a/store/rootmulti/store_test.go b/store/rootmulti/store_test.go index 23669c82a9a0..60f02ac5dd26 100644 --- a/store/rootmulti/store_test.go +++ b/store/rootmulti/store_test.go @@ -101,16 +101,19 @@ func TestCacheMultiStoreWithVersion(t *testing.T) { require.Equal(t, kvStore.Get(k), v) // add new module stores (store4 and store5) to multi stores and commit - ms.MountStoreWithDB(types.NewKVStoreKey("store4"), types.StoreTypeIAVL, nil) - ms.MountStoreWithDB(types.NewKVStoreKey("store5"), types.StoreTypeIAVL, nil) + key4, key5 := types.NewKVStoreKey("store4"), types.NewKVStoreKey("store5") + ms.MountStoreWithDB(key4, types.StoreTypeIAVL, nil) + ms.MountStoreWithDB(key5, types.StoreTypeIAVL, nil) err = ms.LoadLatestVersionAndUpgrade(&types.StoreUpgrades{Added: []string{"store4", "store5"}}) require.NoError(t, err) ms.Commit() // cache multistore of version before adding store4 should works - _, err = ms.CacheMultiStoreWithVersion(1) + cms2, err := ms.CacheMultiStoreWithVersion(1) require.NoError(t, err) + require.Empty(t, cms2.GetKVStore(key4).Get([]byte("key"))) + // require we cannot commit (write) to a cache-versioned multi-store require.Panics(t, func() { kvStore.Set(k, []byte("newValue")) diff --git a/store/types/store.go b/store/types/store.go index cd92fbd0ca07..23e54f253b3c 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -225,6 +225,9 @@ type CommitMultiStore interface { // SetIAVLDisableFastNode enables/disables fastnode feature on iavl. SetIAVLDisableFastNode(disable bool) + // SetIAVLSyncPruning set sync/async pruning on iavl. + SetIAVLSyncPruning(sync bool) + // RollbackToVersion rollback the db to specific version(height). RollbackToVersion(version int64) error diff --git a/x/tx/CHANGELOG.md b/x/tx/CHANGELOG.md index ba89544dd420..a2e461830117 100644 --- a/x/tx/CHANGELOG.md +++ b/x/tx/CHANGELOG.md @@ -37,7 +37,11 @@ Since v0.13.0, x/tx follows Cosmos SDK semver: https://github.com/cosmos/cosmos- * [#21850](https://github.com/cosmos/cosmos-sdk/pull/21850) Support bytes field as signer. -## [v0.13.6](https://github.com/cosmos/cosmos-sdk/releases/tag/x/tx/v0.13.6) - 2024-10-XX +### Bug Fixes + +* Fix [ABS-0043](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-8wcc-m6j2-qxvm) Limit recursion depth for unknown field detection + +## [v0.13.6](https://github.com/cosmos/cosmos-sdk/releases/tag/x/tx/v0.13.6) - 2024-12-12 ### Bug Fixes diff --git a/x/tx/decode/unknown.go b/x/tx/decode/unknown.go index fed2c1be8ff8..acc994f87a18 100644 --- a/x/tx/decode/unknown.go +++ b/x/tx/decode/unknown.go @@ -33,9 +33,23 @@ func RejectUnknownFieldsStrict(bz []byte, msg protoreflect.MessageDescriptor, re // This function traverses inside of messages nested via google.protobuf.Any. It does not do any deserialization of the proto.Message. // An AnyResolver must be provided for traversing inside google.protobuf.Any's. func RejectUnknownFields(bz []byte, desc protoreflect.MessageDescriptor, allowUnknownNonCriticals bool, resolver protodesc.Resolver) (hasUnknownNonCriticals bool, err error) { + // recursion limit with same default as https://github.com/protocolbuffers/protobuf-go/blob/v1.35.2/encoding/protowire/wire.go#L28 + return doRejectUnknownFields(bz, desc, allowUnknownNonCriticals, resolver, 10_000) +} + +func doRejectUnknownFields( + bz []byte, + desc protoreflect.MessageDescriptor, + allowUnknownNonCriticals bool, + resolver protodesc.Resolver, + recursionLimit int, +) (hasUnknownNonCriticals bool, err error) { if len(bz) == 0 { return hasUnknownNonCriticals, nil } + if recursionLimit == 0 { + return false, errors.New("recursion limit reached") + } fields := desc.Fields() @@ -100,7 +114,7 @@ func RejectUnknownFields(bz []byte, desc protoreflect.MessageDescriptor, allowUn if fieldMessage.FullName() == anyFullName { // Firstly typecheck types.Any to ensure nothing snuck in. - hasUnknownNonCriticalsChild, err := RejectUnknownFields(fieldBytes, anyDesc, allowUnknownNonCriticals, resolver) + hasUnknownNonCriticalsChild, err := doRejectUnknownFields(fieldBytes, anyDesc, allowUnknownNonCriticals, resolver, recursionLimit-1) hasUnknownNonCriticals = hasUnknownNonCriticals || hasUnknownNonCriticalsChild if err != nil { return hasUnknownNonCriticals, err @@ -120,7 +134,7 @@ func RejectUnknownFields(bz []byte, desc protoreflect.MessageDescriptor, allowUn fieldBytes = a.Value } - hasUnknownNonCriticalsChild, err := RejectUnknownFields(fieldBytes, fieldMessage, allowUnknownNonCriticals, resolver) + hasUnknownNonCriticalsChild, err := doRejectUnknownFields(fieldBytes, fieldMessage, allowUnknownNonCriticals, resolver, recursionLimit-1) hasUnknownNonCriticals = hasUnknownNonCriticals || hasUnknownNonCriticalsChild if err != nil { return hasUnknownNonCriticals, err