Low severityNVD Advisory· Published Aug 5, 2020· Updated Aug 4, 2024
Improper Input Validation in etcd
CVE-2020-15106
Description
In etcd before versions 3.3.23 and 3.4.10, a large slice causes panic in decodeRecord method. The size of a record is stored in the length field of a WAL file and no additional validation is done on this data. Therefore, it is possible to forge an extremely large frame size that can unintentionally panic at the expense of any RAFT participant trying to decode the WAL.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
go.etcd.io/etcdGo | < 0.5.0-alpha.5.0.20200423152442-f4b650b51dc4 | 0.5.0-alpha.5.0.20200423152442-f4b650b51dc4 |
Affected products
1Patches
2f4b650b51dc4Merge 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
4571e528f496wal: check out of range slice in "ReadAll", "decoder"
3 files changed · +54 −8
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+17 −8 modified@@ -56,13 +56,15 @@ var ( plog = capnslog.NewPackageLogger("go.etcd.io/etcd", "wal") - 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") - ErrDecoderNotFound = errors.New("wal: decoder 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") + ErrDecoderNotFound = errors.New("wal: decoder not found") + crcTable = crc32.MakeTable(crc32.Castagnoli) ) // WAL is a logical representation of the stable storage. @@ -447,8 +449,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-p4g4-wgrh-qrg2ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/L6B6R43Y7M3DCHWK3L3UVGE2K6WWECMP/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2020-15106ghsaADVISORY
- github.com/etcd-io/etcd/blob/master/security/SECURITY_AUDIT.pdfghsaWEB
- github.com/etcd-io/etcd/commit/4571e528f49625d3de3170f219a45c3b3d38c675ghsaWEB
- github.com/etcd-io/etcd/commit/f4b650b51dc4a53a8700700dc12e1242ac56ba07ghsaWEB
- github.com/etcd-io/etcd/pull/11793ghsaWEB
- github.com/etcd-io/etcd/security/advisories/GHSA-p4g4-wgrh-qrg2ghsax_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.