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

PackageAffected versionsPatched versions
go.etcd.io/etcd/v3Go
< 3.3.233.3.23
go.etcd.io/etcd/v3Go
>= 3.4.0, < 3.4.103.4.10

Affected products

1

Patches

2
7d1cf640497c

wal: fix panic when decoder not set

https://github.com/etcd-io/etcdSahdev P. ZalaApr 23, 2020via ghsa
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)
    +	}
    +}
    
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
    

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.