VYPR
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.

PackageAffected versionsPatched versions
go.etcd.io/etcdGo
< 0.5.0-alpha.5.0.20200423152442-f4b650b51dc40.5.0-alpha.5.0.20200423152442-f4b650b51dc4

Affected products

1

Patches

2
f4b650b51dc4

Merge pull request #11793 from gyuho/fix

https://github.com/etcd-io/etcdSahdev ZalaApr 23, 2020via ghsa
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
    
4571e528f496

wal: check out of range slice in "ReadAll", "decoder"

https://github.com/etcd-io/etcdGyuho LeeApr 16, 2020via ghsa
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

News mentions

0

No linked articles in our index yet.