blockchain: Implement stricter bounds checking.

This implements stricter bounds checking during transaction spend
journal decoding.
This commit is contained in:
Aaron Campbell 2019-08-14 07:57:26 -04:00 committed by Dave Collins
parent 77a14a9ead
commit b9b863f5a7
3 changed files with 93 additions and 49 deletions

View File

@ -130,27 +130,48 @@ func deserializeToMinimalOutputs(serialized []byte) ([]*stake.MinimalOutput, int
}
// readDeserializeSizeOfMinimalOutputs reads the size of the stored set of
// minimal outputs without allocating memory for the structs themselves. It
// will panic if the function reads outside of memory bounds.
func readDeserializeSizeOfMinimalOutputs(serialized []byte) int {
// minimal outputs without allocating memory for the structs themselves.
func readDeserializeSizeOfMinimalOutputs(serialized []byte) (int, error) {
numOutputs, offset := deserializeVLQ(serialized)
if offset == 0 {
return offset, errDeserialize("unexpected end of " +
"data during decoding (num outputs)")
}
for i := 0; i < int(numOutputs); i++ {
// Amount
_, bytesRead := deserializeVLQ(serialized[offset:])
if bytesRead == 0 {
return offset, errDeserialize("unexpected end of " +
"data during decoding (output amount)")
}
offset += bytesRead
// Script version
_, bytesRead = deserializeVLQ(serialized[offset:])
if bytesRead == 0 {
return offset, errDeserialize("unexpected end of " +
"data during decoding (output script version)")
}
offset += bytesRead
// Script
var scriptSize uint64
scriptSize, bytesRead = deserializeVLQ(serialized[offset:])
if bytesRead == 0 {
return offset, errDeserialize("unexpected end of " +
"data during decoding (output script size)")
}
offset += bytesRead
if uint64(len(serialized[offset:])) < scriptSize {
return offset, errDeserialize("unexpected end of " +
"data during decoding (output script)")
}
offset += int(scriptSize)
}
return offset
return offset, nil
}
// ConvertUtxosToMinimalOutputs converts the contents of a UTX to a series of
@ -565,27 +586,11 @@ func putSpentTxOut(target []byte, stxo *spentTxOut) int {
// An error will be returned if the version is not serialized as a part of the
// stxo and is also not provided to the function.
func decodeSpentTxOut(serialized []byte, stxo *spentTxOut, amount int64, height uint32, index uint32) (int, error) {
// Ensure there are bytes to decode.
if len(serialized) == 0 {
return 0, errDeserialize("no serialized bytes")
}
// Deserialize the header code.
// Deserialize the flags.
flags, offset := deserializeVLQ(serialized)
if offset >= len(serialized) {
return offset, errDeserialize("unexpected end of data after " +
"spent tx out flags")
}
// Decode the flags. If the flags are non-zero, it means that the
// transaction was fully spent at this spend.
if decodeFlagsFullySpent(byte(flags)) {
isCoinBase, hasExpiry, txType, _ := decodeFlags(byte(flags))
stxo.isCoinBase = isCoinBase
stxo.hasExpiry = hasExpiry
stxo.txType = txType
stxo.txFullySpent = true
if offset == 0 {
return 0, errDeserialize("unexpected end of data during " +
"decoding (flags)")
}
// Decode the compressed txout. We pass false for the amount flag,
@ -609,22 +614,28 @@ func decodeSpentTxOut(serialized []byte, stxo *spentTxOut, amount int64, height
// Deserialize the containing transaction if the flags indicate that
// the transaction has been fully spent.
if decodeFlagsFullySpent(byte(flags)) {
isCoinBase, hasExpiry, txType, _ := decodeFlags(byte(flags))
stxo.isCoinBase = isCoinBase
stxo.hasExpiry = hasExpiry
stxo.txType = txType
stxo.txFullySpent = true
txVersion, bytesRead := deserializeVLQ(serialized[offset:])
offset += bytesRead
if offset == 0 || offset > len(serialized) {
return offset, errDeserialize("unexpected end of data " +
"after version")
if bytesRead == 0 {
return offset, errDeserialize("unexpected end of " +
"data during decoding (tx version)")
}
offset += bytesRead
stxo.txVersion = uint16(txVersion)
if stxo.txType == stake.TxTypeSStx {
sz := readDeserializeSizeOfMinimalOutputs(serialized[offset:])
if sz == 0 || sz > len(serialized[offset:]) {
return offset, errDeserialize("corrupt data for ticket " +
"fully spent stxo stakeextra")
sz, err := readDeserializeSizeOfMinimalOutputs(serialized[offset:])
if err != nil {
return offset + sz, errDeserialize(fmt.Sprintf("unable to decode "+
"ticket outputs: %v", err))
}
stakeExtra := make([]byte, sz)
copy(stakeExtra, serialized[offset:offset+sz])
stxo.stakeExtra = stakeExtra

View File

@ -493,53 +493,81 @@ func TestStxoDecodeErrors(t *testing.T) {
tests := []struct {
name string
stxo spentTxOut
txVersion int32 // When the txout is not fully spent.
serialized []byte
bytesRead int // Expected number of bytes read.
errType error
bytesRead int // Expected number of bytes read.
}{
{
name: "nothing serialized",
// [EOF]
name: "nothing serialized (no flags)",
stxo: spentTxOut{},
serialized: hexToBytes(""),
errType: errDeserialize(""),
bytesRead: 0,
},
{
name: "no data after flags w/o version",
// [<flags 00> EOF]
name: "no compressed txout script version",
stxo: spentTxOut{},
serialized: hexToBytes("00"),
errType: errDeserialize(""),
bytesRead: 1,
},
{
name: "no data after flags code",
// [<flags 10> <script version 00> EOF]
name: "no tx version data after empty script for a fully spent regular stxo",
stxo: spentTxOut{},
serialized: hexToBytes("14"),
serialized: hexToBytes("1000"),
errType: errDeserialize(""),
bytesRead: 1,
bytesRead: 2,
},
{
name: "no tx version data after script",
// [<flags 10> <script version 00> <compressed pk script 01 6e ...> EOF]
name: "no tx version data after a pay-to-script-hash script for a fully spent regular stxo",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
serialized: hexToBytes("1000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
errType: errDeserialize(""),
bytesRead: 23,
},
{
name: "no stakeextra data after script for ticket",
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> EOF]
name: "no stakeextra data after script for a fully spent ticket stxo",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
errType: errDeserialize(""),
bytesRead: 24,
},
{
name: "incomplete compressed txout",
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs only)",
stxo: spentTxOut{},
txVersion: 1,
serialized: hexToBytes("1432"),
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e860101"),
errType: errDeserialize(""),
bytesRead: 2,
bytesRead: 25,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs and amount only)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f"),
errType: errDeserialize(""),
bytesRead: 26,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (num outputs, amount, and script version only)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f00"),
errType: errDeserialize(""),
bytesRead: 27,
},
{
// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00} {script size 1a} {25 bytes of script instead of 26}> EOF]
name: "truncated stakeextra data after script for a fully spent ticket stxo (script size specified as 0x1a, but only 0x19 bytes provided)",
stxo: spentTxOut{},
serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588"),
errType: errDeserialize(""),
bytesRead: 28,
},
}

View File

@ -653,9 +653,9 @@ func decodeCompressedTxOut(serialized []byte, compressionVersion uint32,
// remaining for the compressed script.
var compressedAmount uint64
compressedAmount, bytesRead = deserializeVLQ(serialized)
if bytesRead >= len(serialized) {
if bytesRead == 0 {
return 0, 0, nil, bytesRead, errDeserialize("unexpected end of " +
"data after compressed amount")
"data during decoding (compressed amount)")
}
amount = int64(decompressTxOutAmount(compressedAmount))
offset += bytesRead
@ -664,12 +664,17 @@ func decodeCompressedTxOut(serialized []byte, compressionVersion uint32,
// Decode the script version.
var scriptVersion uint64
scriptVersion, bytesRead = deserializeVLQ(serialized[offset:])
if bytesRead == 0 {
return 0, 0, nil, offset, errDeserialize("unexpected end of " +
"data during decoding (script version)")
}
offset += bytesRead
// Decode the compressed script size and ensure there are enough bytes
// left in the slice for it.
scriptSize := decodeCompressedScriptSize(serialized[offset:],
compressionVersion)
// Note: scriptSize == 0 is OK (an empty compressed script is valid)
if scriptSize < 0 {
return 0, 0, nil, offset, errDeserialize("negative script size")
}