Moderate severityNVD Advisory· Published Aug 5, 2020· Updated Aug 4, 2024
Improper Input Validation in etcd
CVE-2020-15112
Description
In etcd before versions 3.3.23 and 3.4.10, it is possible to have an entry index greater then the number of entries in the ReadAll method in wal/wal.go. This could cause issues when WAL entries are being read during consensus as an arbitrary etcd consensus participant could go down from a runtime panic when reading the entry.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
go.etcd.io/etcd/v3Go | < 3.3.23 | 3.3.23 |
go.etcd.io/etcd/v3Go | >= 3.4.0, < 3.4.10 | 3.4.10 |
Affected products
1Patches
27d1cf640497cwal: fix panic when decoder not set
2 files changed · +28 −1
wal/wal.go+7 −1 modified@@ -61,6 +61,7 @@ var ( ErrCRCMismatch = errors.New("wal: crc mismatch") ErrSnapshotMismatch = errors.New("wal: snapshot mismatch") ErrSnapshotNotFound = errors.New("wal: snapshot not found") + ErrDecoderNotFound = errors.New("wal: decoder not found") crcTable = crc32.MakeTable(crc32.Castagnoli) ) @@ -95,7 +96,8 @@ type WAL struct { } // Create creates a WAL ready for appending records. The given metadata is -// recorded at the head of each WAL file, and can be retrieved with ReadAll. +// recorded at the head of each WAL file, and can be retrieved with ReadAll +// after the file is Open. func Create(lg *zap.Logger, dirpath string, metadata []byte) (*WAL, error) { if Exist(dirpath) { return nil, os.ErrExist @@ -434,6 +436,10 @@ func (w *WAL) ReadAll() (metadata []byte, state raftpb.HardState, ents []raftpb. defer w.mu.Unlock() rec := &walpb.Record{} + + if w.decoder == nil { + return nil, state, nil, ErrDecoderNotFound + } decoder := w.decoder var match bool
wal/wal_test.go+21 −0 modified@@ -1059,3 +1059,24 @@ func TestValidSnapshotEntriesAfterPurgeWal(t *testing.T) { t.Fatal(err) } } + +// TestReadAllFail ensure ReadAll error if used without opening the WAL +func TestReadAllFail(t *testing.T) { + dir, err := ioutil.TempDir(os.TempDir(), "waltest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + // create initial WAL + f, err := Create(zap.NewExample(), dir, []byte("metadata")) + if err != nil { + t.Fatal(err) + } + f.Close() + // try to read without opening the WAL + _, _, _, err = f.ReadAll() + if err == nil || err != ErrDecoderNotFound { + t.Fatalf("err = %v, want ErrDecoderNotFound", err) + } +}
f4b650b51dc4Merge pull request #11793 from gyuho/fix
4 files changed · +54 −8
CHANGELOG-3.5.md+1 −1 modified@@ -105,7 +105,6 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change. - Remove [redundant storage restore operation to shorten the startup time](https://github.com/etcd-io/etcd/pull/11779). - With 40 million key test data,it can shorten the startup time from 5 min to 2.5 min. - ### Package `embed` - Remove [`embed.Config.Debug`](https://github.com/etcd-io/etcd/pull/10947). @@ -133,6 +132,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change. ### Package `wal` - Add [`etcd_wal_write_bytes_total`](https://github.com/etcd-io/etcd/pull/11738). +- Handle [out-of-range slice bound in `ReadAll` and entry limit in `decodeRecord`](https://github.com/etcd-io/etcd/pull/11793). ### etcdctl v3
wal/decoder.go+8 −0 modified@@ -59,6 +59,11 @@ func (d *decoder) decode(rec *walpb.Record) error { return d.decodeRecord(rec) } +// raft max message size is set to 1 MB in etcd server +// assume projects set reasonable message size limit, +// thus entry size should never exceed 10 MB +const maxWALEntrySizeLimit = int64(10 * 1024 * 1024) + func (d *decoder) decodeRecord(rec *walpb.Record) error { if len(d.brs) == 0 { return io.EOF @@ -79,6 +84,9 @@ func (d *decoder) decodeRecord(rec *walpb.Record) error { } recBytes, padBytes := decodeFrameSize(l) + if recBytes >= maxWALEntrySizeLimit-padBytes { + return ErrMaxWALEntrySizeLimitExceeded + } data := make([]byte, recBytes+padBytes) if _, err = io.ReadFull(d.brs[0], data); err != nil {
wal/wal.go+16 −7 modified@@ -53,12 +53,14 @@ var ( // so that tests can set a different segment size. SegmentSizeBytes int64 = 64 * 1000 * 1000 // 64MB - ErrMetadataConflict = errors.New("wal: conflicting metadata found") - ErrFileNotFound = errors.New("wal: file not found") - ErrCRCMismatch = errors.New("wal: crc mismatch") - ErrSnapshotMismatch = errors.New("wal: snapshot mismatch") - ErrSnapshotNotFound = errors.New("wal: snapshot not found") - crcTable = crc32.MakeTable(crc32.Castagnoli) + ErrMetadataConflict = errors.New("wal: conflicting metadata found") + ErrFileNotFound = errors.New("wal: file not found") + ErrCRCMismatch = errors.New("wal: crc mismatch") + ErrSnapshotMismatch = errors.New("wal: snapshot mismatch") + ErrSnapshotNotFound = errors.New("wal: snapshot not found") + ErrSliceOutOfRange = errors.New("wal: slice bounds out of range") + ErrMaxWALEntrySizeLimitExceeded = errors.New("wal: max entry size limit exceeded") + crcTable = crc32.MakeTable(crc32.Castagnoli) ) // WAL is a logical representation of the stable storage. @@ -411,8 +413,15 @@ func (w *WAL) ReadAll() (metadata []byte, state raftpb.HardState, ents []raftpb. switch rec.Type { case entryType: e := mustUnmarshalEntry(rec.Data) + // 0 <= e.Index-w.start.Index - 1 < len(ents) if e.Index > w.start.Index { - ents = append(ents[:e.Index-w.start.Index-1], e) + // prevent "panic: runtime error: slice bounds out of range [:13038096702221461992] with capacity 0" + up := e.Index - w.start.Index - 1 + if up > uint64(len(ents)) { + // return error before append call causes runtime panic + return nil, state, nil, ErrSliceOutOfRange + } + ents = append(ents[:up], e) } w.enti = e.Index
wal/wal_test.go+29 −0 modified@@ -645,6 +645,35 @@ func TestOpenForRead(t *testing.T) { } } +func TestOpenWithMaxIndex(t *testing.T) { + p, err := ioutil.TempDir(os.TempDir(), "waltest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(p) + // create WAL + w, err := Create(zap.NewExample(), p, nil) + if err != nil { + t.Fatal(err) + } + defer w.Close() + + es := []raftpb.Entry{{Index: uint64(math.MaxInt64)}} + if err = w.Save(raftpb.HardState{}, es); err != nil { + t.Fatal(err) + } + w.Close() + + w, err = Open(zap.NewExample(), p, walpb.Snapshot{}) + if err != nil { + t.Fatal(err) + } + _, _, _, err = w.ReadAll() + if err == nil || err != ErrSliceOutOfRange { + t.Fatalf("err = %v, want ErrSliceOutOfRange", err) + } +} + func TestSaveEmpty(t *testing.T) { var buf bytes.Buffer var est raftpb.HardState
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-m332-53r6-2w93ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/L6B6R43Y7M3DCHWK3L3UVGE2K6WWECMP/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2020-15112ghsaADVISORY
- github.com/etcd-io/etcd/blob/master/security/SECURITY_AUDIT.pdfghsaWEB
- github.com/etcd-io/etcd/commit/7d1cf640497cbcdfb932e619b13624112c7e3865ghsaWEB
- github.com/etcd-io/etcd/commit/f4b650b51dc4a53a8700700dc12e1242ac56ba07ghsaWEB
- github.com/etcd-io/etcd/pull/11793ghsaWEB
- github.com/etcd-io/etcd/security/advisories/GHSA-m332-53r6-2w93ghsax_refsource_CONFIRMWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/L6B6R43Y7M3DCHWK3L3UVGE2K6WWECMPghsaWEB
- pkg.go.dev/vuln/GO-2020-0005ghsaWEB
News mentions
0No linked articles in our index yet.