@@ -155,3 +155,118 @@ func TestFailpoint_LackOfDiskSpace(t *testing.T) {
155155 require .Error (t , err )
156156 require .ErrorIs (t , err , errors .ErrTxClosed )
157157}
158+
159+ // TestIssue72 reproduces issue 72.
160+ //
161+ // When bbolt is processing a `Put` invocation, the key might be concurrently
162+ // updated by the application which calls the `Put` API (although it shouldn't).
163+ // It might lead to a situation that bbolt use an old key to find a proper
164+ // position to insert the key/value pair, but actually inserts a new key.
165+ // Eventually it might break the rule that all keys should be sorted. In a
166+ // worse case, it might cause page elements to point to already freed pages.
167+ //
168+ // REF: https://github.com/etcd-io/bbolt/issues/72
169+ func TestIssue72 (t * testing.T ) {
170+ db := btesting .MustCreateDBWithOption (t , & bolt.Options {PageSize : 4096 })
171+
172+ bucketName := []byte (t .Name ())
173+ err := db .Update (func (tx * bolt.Tx ) error {
174+ _ , txerr := tx .CreateBucket (bucketName )
175+ return txerr
176+ })
177+ require .NoError (t , err )
178+
179+ // The layout is like:
180+ //
181+ // +--+--+--+
182+ // +------+1 |3 |10+---+
183+ // | +-++--+--+ |
184+ // | | |
185+ // | | |
186+ // +v-+--+ +v-+--+ +-v+--+--+
187+ // |1 |2 | |3 |4 | |10|11|12|
188+ // +--+--+ +--+--+ +--+--+--+
189+ //
190+ err = db .Update (func (tx * bolt.Tx ) error {
191+ bk := tx .Bucket (bucketName )
192+
193+ for _ , id := range []int {1 , 2 , 3 , 4 , 10 , 11 , 12 } {
194+ if txerr := bk .Put (idToBytes (id ), make ([]byte , 1000 )); txerr != nil {
195+ return txerr
196+ }
197+ }
198+ return nil
199+ })
200+ require .NoError (t , err )
201+
202+ require .NoError (t , gofail .Enable ("beforeBucketPut" , `sleep(5000)` ))
203+
204+ // +--+--+--+
205+ // +------+1 |3 |1 +---+
206+ // | +-++--+--+ |
207+ // | | |
208+ // | | |
209+ // +v-+--+ +v-+--+ +-v+--+--+--+
210+ // |1 |2 | |3 |4 | |1 |10|11|12|
211+ // +--+--+ +--+--+ +--+--+--+--+
212+ //
213+ key := idToBytes (13 )
214+ updatedKey := idToBytes (1 )
215+ err = db .Update (func (tx * bolt.Tx ) error {
216+ bk := tx .Bucket (bucketName )
217+
218+ go func () {
219+ time .Sleep (3 * time .Second )
220+ copy (key , updatedKey )
221+ }()
222+ return bk .Put (key , make ([]byte , 100 ))
223+ })
224+ require .NoError (t , err )
225+
226+ require .NoError (t , gofail .Disable ("beforeBucketPut" ))
227+
228+ // bbolt inserts 100 into last branch page. Since there are two `1`
229+ // keys in branch, spill operation will update first `1` pointer and
230+ // then last one won't be updated and continues to point to freed page.
231+ //
232+ //
233+ // +--+--+--+
234+ // +---------------+1 |3 |1 +---------+
235+ // | +--++-+--+ |
236+ // | | |
237+ // | | |
238+ // | +--+--+ +v-+--+ +-----v-----+
239+ // | |1 |2 | |3 |4 | |freed page |
240+ // | +--+--+ +--+--+ +-----------+
241+ // |
242+ // +v-+--+--+--+---+
243+ // |1 |10|11|12|100|
244+ // +--+--+--+--+---+
245+ err = db .Update (func (tx * bolt.Tx ) error {
246+ return tx .Bucket (bucketName ).Put (idToBytes (100 ), make ([]byte , 100 ))
247+ })
248+ require .NoError (t , err )
249+
250+ defer func () {
251+ if r := recover (); r != nil {
252+ t .Logf ("panic info:\n %v" , r )
253+ }
254+ }()
255+
256+ // Add more keys to ensure branch node to spill.
257+ err = db .Update (func (tx * bolt.Tx ) error {
258+ bk := tx .Bucket (bucketName )
259+
260+ for _ , id := range []int {101 , 102 , 103 , 104 , 105 } {
261+ if txerr := bk .Put (idToBytes (id ), make ([]byte , 1000 )); txerr != nil {
262+ return txerr
263+ }
264+ }
265+ return nil
266+ })
267+ require .NoError (t , err )
268+ }
269+
270+ func idToBytes (id int ) []byte {
271+ return []byte (fmt .Sprintf ("%010d" , id ))
272+ }
0 commit comments