mirror of
https://github.com/securego/gosec.git
synced 2026-06-20 00:15:59 +02:00
* G115: Enhance RangeAnalyzer with constant propagation and chained arithmetic support * Fix G115 overflow detection for negated values and robustify RangeAnalyzer propagation
1470 lines
42 KiB
Go
1470 lines
42 KiB
Go
// (c) Copyright gosec's authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package analyzers
|
|
|
|
import (
|
|
"cmp"
|
|
"go/constant"
|
|
"go/token"
|
|
"go/types"
|
|
"math/bits"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/tools/go/ssa"
|
|
)
|
|
|
|
// ByteRange represents a range [Low, High)
|
|
type ByteRange struct {
|
|
Low int64
|
|
High int64
|
|
}
|
|
|
|
// RangeAction represents a read/write action on a byte range.
|
|
type RangeAction struct {
|
|
Instr ssa.Instruction
|
|
Range ByteRange
|
|
IsSafe bool // true = Read (Dynamic), false = Write/Alloc (Hardcoded)
|
|
}
|
|
|
|
type rangeCacheKey struct {
|
|
block *ssa.BasicBlock
|
|
val ssa.Value
|
|
}
|
|
|
|
type rangeResult struct {
|
|
minValue uint64
|
|
maxValue uint64
|
|
minValueSet bool
|
|
maxValueSet bool
|
|
explicitPositiveVals []uint
|
|
explicitNegativeVals []int
|
|
isRangeCheck bool
|
|
shared bool // If true, do not release to pool
|
|
}
|
|
|
|
type RangeAnalyzer struct {
|
|
RangeCache map[rangeCacheKey]*rangeResult
|
|
ResultPool []*rangeResult
|
|
Depth int
|
|
BlockMap map[*ssa.BasicBlock]bool
|
|
ValueMap map[ssa.Value]bool
|
|
ByteRangeCache map[ssa.Value]ByteRange
|
|
BufferLenCache map[ssa.Value]int64
|
|
reachStack []*ssa.BasicBlock
|
|
}
|
|
|
|
var rangeAnalyzerPool = sync.Pool{
|
|
New: func() any {
|
|
return &RangeAnalyzer{
|
|
RangeCache: make(map[rangeCacheKey]*rangeResult),
|
|
ResultPool: make([]*rangeResult, 0, 32),
|
|
BlockMap: make(map[*ssa.BasicBlock]bool),
|
|
ValueMap: make(map[ssa.Value]bool),
|
|
ByteRangeCache: make(map[ssa.Value]ByteRange),
|
|
BufferLenCache: make(map[ssa.Value]int64),
|
|
reachStack: make([]*ssa.BasicBlock, 0, 32),
|
|
}
|
|
},
|
|
}
|
|
|
|
func (res *rangeResult) Reset() {
|
|
res.minValue = toUint64(minInt64)
|
|
res.maxValue = maxUint64
|
|
res.minValueSet = false
|
|
res.maxValueSet = false
|
|
res.explicitPositiveVals = res.explicitPositiveVals[:0]
|
|
res.explicitNegativeVals = res.explicitNegativeVals[:0]
|
|
res.isRangeCheck = false
|
|
res.shared = false
|
|
}
|
|
|
|
func (res *rangeResult) CopyFrom(other *rangeResult) {
|
|
res.minValue = other.minValue
|
|
res.maxValue = other.maxValue
|
|
res.minValueSet = other.minValueSet
|
|
res.maxValueSet = other.maxValueSet
|
|
res.explicitPositiveVals = append(res.explicitPositiveVals[:0], other.explicitPositiveVals...)
|
|
res.explicitNegativeVals = append(res.explicitNegativeVals[:0], other.explicitNegativeVals...)
|
|
res.isRangeCheck = other.isRangeCheck
|
|
}
|
|
|
|
// NewRangeAnalyzer acquires a RangeAnalyzer from the pool.
|
|
func NewRangeAnalyzer() *RangeAnalyzer {
|
|
return rangeAnalyzerPool.Get().(*RangeAnalyzer)
|
|
}
|
|
|
|
// Release returns the RangeAnalyzer to the pool after clearing its caches.
|
|
func (ra *RangeAnalyzer) Release() {
|
|
ra.ResetCache()
|
|
rangeAnalyzerPool.Put(ra)
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) ResetCache() {
|
|
for _, res := range ra.RangeCache {
|
|
res.shared = false
|
|
ra.releaseResult(res)
|
|
}
|
|
clear(ra.RangeCache)
|
|
clear(ra.BlockMap)
|
|
clear(ra.ValueMap)
|
|
clear(ra.ByteRangeCache)
|
|
clear(ra.BufferLenCache)
|
|
ra.reachStack = ra.reachStack[:0]
|
|
ra.Depth = 0
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) acquireResult() *rangeResult {
|
|
if len(ra.ResultPool) > 0 {
|
|
idx := len(ra.ResultPool) - 1
|
|
res := ra.ResultPool[idx]
|
|
ra.ResultPool = ra.ResultPool[:idx]
|
|
res.Reset()
|
|
return res
|
|
}
|
|
res := &rangeResult{}
|
|
res.Reset()
|
|
return res
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) releaseResult(res *rangeResult) {
|
|
if res != nil && !res.shared {
|
|
ra.ResultPool = append(ra.ResultPool, res)
|
|
}
|
|
}
|
|
|
|
// ResolveRange combines definition-based range analysis (computeRange) with dominator-based constraints (If blocks) to determine the full range of a value.
|
|
func (ra *RangeAnalyzer) ResolveRange(v ssa.Value, block *ssa.BasicBlock) *rangeResult {
|
|
key := rangeCacheKey{block: block, val: v}
|
|
if res, ok := ra.RangeCache[key]; ok {
|
|
return res
|
|
}
|
|
|
|
isSrcUnsigned := isUint(v)
|
|
result := ra.acquireResult()
|
|
// result is initialized to wide range (MinInt64, MaxUint64) by acquireResult/Reset
|
|
if isSrcUnsigned {
|
|
result.minValue = 0
|
|
} else {
|
|
result.maxValue = maxInt64
|
|
}
|
|
|
|
// Check for explicit range checks.
|
|
if vIndex, ok := v.(*ssa.IndexAddr); ok {
|
|
res := ra.ResolveRange(vIndex.Index, vIndex.Block())
|
|
if res.isRangeCheck && res.minValueSet && res.maxValueSet {
|
|
// If the index itself has a known range, apply it.
|
|
result.minValue = maxBounds(result.minValue, result.minValueSet, res.minValue, res.minValueSet, isSrcUnsigned)
|
|
result.maxValue = minBounds(result.maxValue, result.maxValueSet, res.maxValue, res.maxValueSet, isSrcUnsigned)
|
|
result.minValueSet = true
|
|
result.maxValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
ra.releaseResult(res)
|
|
}
|
|
|
|
if ra.Depth > MaxDepth {
|
|
result.shared = true
|
|
ra.RangeCache[key] = result
|
|
return result
|
|
}
|
|
|
|
ra.Depth++
|
|
defer func() { ra.Depth-- }()
|
|
|
|
// Basic properties
|
|
isNonNeg := ra.IsNonNegative(v)
|
|
if isNonNeg {
|
|
result.minValue = 0
|
|
result.minValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
|
|
// Range from definition
|
|
defRange := ra.ComputeRange(v, block)
|
|
if defRange.isRangeCheck || defRange.minValueSet || defRange.maxValueSet {
|
|
result.isRangeCheck = true
|
|
if defRange.minValueSet {
|
|
result.minValue = maxBounds(result.minValue, result.minValueSet, defRange.minValue, defRange.minValueSet, isSrcUnsigned)
|
|
result.minValueSet = true
|
|
}
|
|
if defRange.maxValueSet {
|
|
result.maxValue = minBounds(result.maxValue, result.maxValueSet, defRange.maxValue, defRange.maxValueSet, isSrcUnsigned)
|
|
result.maxValueSet = true
|
|
}
|
|
}
|
|
// ComputeRange returns a temporary result, release it
|
|
ra.releaseResult(defRange)
|
|
|
|
// Range from control flow constraints
|
|
currDom := block.Idom()
|
|
for currDom != nil {
|
|
if vIf, ok := currDom.Instrs[len(currDom.Instrs)-1].(*ssa.If); ok {
|
|
var finalResIf *rangeResult
|
|
matchCount := 0
|
|
for i, succ := range currDom.Succs {
|
|
reach := ra.IsReachable(succ, block)
|
|
if reach {
|
|
matchCount++
|
|
if resIf := ra.getResultRangeForIfEdge(vIf, i == 0, v); resIf != nil {
|
|
if matchCount == 1 {
|
|
finalResIf = resIf
|
|
} else {
|
|
ra.releaseResult(resIf)
|
|
if finalResIf != nil {
|
|
ra.releaseResult(finalResIf)
|
|
finalResIf = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if matchCount == 1 && finalResIf != nil {
|
|
if finalResIf.minValueSet {
|
|
result.minValue = maxBounds(result.minValue, result.minValueSet, finalResIf.minValue, finalResIf.minValueSet, isSrcUnsigned)
|
|
result.minValueSet = true
|
|
}
|
|
if finalResIf.maxValueSet {
|
|
result.maxValue = minBounds(result.maxValue, result.maxValueSet, finalResIf.maxValue, finalResIf.maxValueSet, isSrcUnsigned)
|
|
result.maxValueSet = true
|
|
}
|
|
if finalResIf.isRangeCheck {
|
|
result.isRangeCheck = true
|
|
}
|
|
ra.releaseResult(finalResIf)
|
|
}
|
|
}
|
|
currDom = currDom.Idom()
|
|
}
|
|
|
|
// Persist in cache
|
|
result.shared = true
|
|
ra.RangeCache[key] = result
|
|
return result
|
|
}
|
|
|
|
// IsReachable returns true if there is a path from the start block to the target block in the CFG.
|
|
// It uses iterative stack-based traversal and the RangeAnalyzer's BlockMap to avoid allocations.
|
|
func (ra *RangeAnalyzer) IsReachable(start, target *ssa.BasicBlock) bool {
|
|
if start == target {
|
|
return true
|
|
}
|
|
clear(ra.BlockMap)
|
|
ra.reachStack = ra.reachStack[:0]
|
|
ra.reachStack = append(ra.reachStack, start)
|
|
|
|
for len(ra.reachStack) > 0 {
|
|
curr := ra.reachStack[len(ra.reachStack)-1]
|
|
ra.reachStack = ra.reachStack[:len(ra.reachStack)-1]
|
|
|
|
if curr == target {
|
|
return true
|
|
}
|
|
if ra.BlockMap[curr] {
|
|
continue
|
|
}
|
|
ra.BlockMap[curr] = true
|
|
|
|
for _, succ := range curr.Succs {
|
|
if !ra.BlockMap[succ] {
|
|
ra.reachStack = append(ra.reachStack, succ)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) getResultRangeForIfEdge(vIf *ssa.If, isTrue bool, v ssa.Value) *rangeResult {
|
|
res := ra.acquireResult()
|
|
binOp, _ := vIf.Cond.(*ssa.BinOp)
|
|
if binOp != nil && IsRangeCheck(vIf.Cond, v) {
|
|
ra.updateResultFromBinOpForValue(res, binOp, v, isTrue)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) updateResultFromBinOpForValue(result *rangeResult, binOp *ssa.BinOp, v ssa.Value, successPathConvert bool) {
|
|
operandsFlipped := false
|
|
compareVal, op := getRealValueFromOperation(v)
|
|
if fieldAddr, ok := compareVal.(*ssa.FieldAddr); ok {
|
|
compareVal = fieldAddr
|
|
}
|
|
|
|
var matchSide ssa.Value
|
|
var inverseOp operationInfo
|
|
if isEquivalent(binOp.X, v) {
|
|
matchSide = binOp.Y
|
|
op = operationInfo{}
|
|
} else if isEquivalent(binOp.Y, v) {
|
|
matchSide = binOp.X
|
|
operandsFlipped = true
|
|
op = operationInfo{}
|
|
} else if isSameOrRelated(binOp.X, compareVal) {
|
|
matchSide = binOp.Y
|
|
// check if binOp.X has an operation relative to compareVal
|
|
if rVal, rOp := getRealValueFromOperation(binOp.X); rVal == compareVal {
|
|
inverseOp = rOp
|
|
}
|
|
} else if rVal, rOp := getRealValueFromOperation(binOp.X); rVal == compareVal {
|
|
matchSide = binOp.Y
|
|
inverseOp = rOp
|
|
} else if isSameOrRelated(binOp.Y, compareVal) {
|
|
matchSide = binOp.X
|
|
operandsFlipped = true
|
|
// check if binOp.Y has an operation relative to compareVal
|
|
if rVal, rOp := getRealValueFromOperation(binOp.Y); rVal == compareVal {
|
|
inverseOp = rOp
|
|
}
|
|
} else if rVal, rOp := getRealValueFromOperation(binOp.Y); rVal == compareVal {
|
|
matchSide = binOp.X
|
|
operandsFlipped = true
|
|
inverseOp = rOp
|
|
} else {
|
|
return
|
|
}
|
|
|
|
val, ok := GetConstantInt64(matchSide)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Apply inverse operations to the limit 'val' before updating min/max
|
|
if inverseOp.op != "" {
|
|
switch inverseOp.op {
|
|
case "<<":
|
|
if vShift, ok := GetConstantInt64(inverseOp.extra); ok && vShift >= 0 {
|
|
val = val >> uint(vShift)
|
|
}
|
|
case "+":
|
|
if vAdd, ok := GetConstantInt64(inverseOp.extra); ok {
|
|
val -= vAdd
|
|
}
|
|
case "-":
|
|
if vSub, ok := GetConstantInt64(inverseOp.extra); ok {
|
|
if inverseOp.flipped { // val = extra - x => x = extra - val
|
|
val = vSub - val
|
|
operandsFlipped = !operandsFlipped
|
|
} else { // val = x - extra => x = val + extra
|
|
val += vSub
|
|
}
|
|
}
|
|
case "neg":
|
|
val = -val
|
|
operandsFlipped = !operandsFlipped
|
|
case ">>":
|
|
if vShift, ok := GetConstantInt64(inverseOp.extra); ok && vShift >= 0 {
|
|
val = val << uint(vShift)
|
|
}
|
|
case "*":
|
|
if vMul, ok := GetConstantUint64(inverseOp.extra); ok && vMul > 0 {
|
|
val = toInt64(toUint64(val) / vMul)
|
|
}
|
|
case "/":
|
|
if vQuo, ok := GetConstantUint64(inverseOp.extra); ok && vQuo > 0 {
|
|
if inverseOp.flipped { // val = extra / x => x = extra / val
|
|
if val != 0 {
|
|
val = toInt64(vQuo / toUint64(val))
|
|
}
|
|
operandsFlipped = !operandsFlipped
|
|
} else { // val = x / extra => x = val * vQuo
|
|
val = toInt64(toUint64(val) * vQuo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply forward operations from 'op' to the limit 'val'
|
|
if op.op != "" {
|
|
switch op.op {
|
|
case "<<":
|
|
if vShift, ok := GetConstantInt64(op.extra); ok && vShift >= 0 {
|
|
val = val << uint(vShift)
|
|
}
|
|
case "+":
|
|
if vAdd, ok := GetConstantInt64(op.extra); ok {
|
|
val += vAdd
|
|
}
|
|
case "-":
|
|
if vSub, ok := GetConstantInt64(op.extra); ok {
|
|
if op.flipped { // v = extra - x. x < val => v > extra - val
|
|
val = vSub - val
|
|
operandsFlipped = !operandsFlipped
|
|
} else { // v = x - extra. x < val => v < val - extra
|
|
val -= vSub
|
|
}
|
|
}
|
|
case ">>":
|
|
if vShift, ok := GetConstantInt64(op.extra); ok && vShift >= 0 {
|
|
val = val >> uint(vShift)
|
|
}
|
|
case "*":
|
|
isSrcUnsigned := isUint(v)
|
|
if isSrcUnsigned {
|
|
if vMul, ok := GetConstantUint64(op.extra); ok && vMul != 0 {
|
|
hi, lo := bits.Mul64(toUint64(val), vMul)
|
|
if hi != 0 {
|
|
return
|
|
}
|
|
val = toInt64(lo)
|
|
}
|
|
} else {
|
|
if vMul, ok := GetConstantInt64(op.extra); ok && vMul != 0 {
|
|
if vMul > 0 {
|
|
if val >= 0 {
|
|
hi, lo := bits.Mul64(toUint64(val), toUint64(vMul))
|
|
if hi != 0 {
|
|
return
|
|
}
|
|
val = toInt64(lo)
|
|
} else {
|
|
if val < minInt64/vMul {
|
|
return
|
|
}
|
|
val = val * vMul
|
|
}
|
|
} else {
|
|
val = val * vMul
|
|
operandsFlipped = !operandsFlipped
|
|
}
|
|
}
|
|
}
|
|
case "/":
|
|
if vQuo, ok := GetConstantInt64(op.extra); ok && vQuo > 0 {
|
|
if op.flipped { // v = extra / x. x < val => v > extra / val
|
|
if val != 0 {
|
|
val = vQuo / val
|
|
}
|
|
operandsFlipped = !operandsFlipped
|
|
} else { // v = x / extra. x < val => v < val / vQuo
|
|
val = val / vQuo
|
|
}
|
|
}
|
|
case "neg":
|
|
val = -val
|
|
operandsFlipped = !operandsFlipped
|
|
}
|
|
}
|
|
|
|
switch binOp.Op {
|
|
case token.LEQ, token.LSS:
|
|
updateMinMaxForLessOrEqual(result, val, binOp.Op, operandsFlipped, successPathConvert)
|
|
case token.GEQ, token.GTR:
|
|
updateMinMaxForGreaterOrEqual(result, val, binOp.Op, operandsFlipped, successPathConvert)
|
|
case token.EQL:
|
|
if successPathConvert {
|
|
updateExplicitValues(result, val)
|
|
}
|
|
case token.NEQ:
|
|
if !successPathConvert {
|
|
updateExplicitValues(result, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) IsNonNegative(v ssa.Value) bool {
|
|
clear(ra.ValueMap)
|
|
return ra.isNonNegativeRecursive(v)
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) isNonNegativeRecursive(v ssa.Value) bool {
|
|
if ra.ValueMap[v] {
|
|
return true // Assume non-negative to break cycles
|
|
}
|
|
ra.ValueMap[v] = true
|
|
|
|
if isUint(v) {
|
|
return true
|
|
}
|
|
|
|
v, info := getRealValueFromOperation(v)
|
|
if info.op == "neg" || info.op == "-" {
|
|
return false
|
|
}
|
|
switch v := v.(type) {
|
|
case *ssa.Extract:
|
|
if _, ok := v.Tuple.(*ssa.Next); ok {
|
|
return true
|
|
}
|
|
case *ssa.Call:
|
|
if fn, ok := v.Call.Value.(*ssa.Builtin); ok {
|
|
switch fn.Name() {
|
|
case "len", "cap":
|
|
return true
|
|
case "min":
|
|
for _, arg := range v.Call.Args {
|
|
if !ra.isNonNegativeRecursive(arg) {
|
|
return false
|
|
}
|
|
}
|
|
return len(v.Call.Args) > 0
|
|
case "max":
|
|
for _, arg := range v.Call.Args {
|
|
if ra.isNonNegativeRecursive(arg) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
if callee := v.Call.StaticCallee(); callee != nil {
|
|
name := callee.String()
|
|
if strings.Contains(name, "UnixMilli") || strings.Contains(name, "UnixMicro") || strings.Contains(name, "UnixNano") {
|
|
return true
|
|
}
|
|
}
|
|
case *ssa.BinOp:
|
|
switch v.Op {
|
|
case token.ADD, token.MUL, token.QUO:
|
|
return ra.isNonNegativeRecursive(v.X) && ra.isNonNegativeRecursive(v.Y)
|
|
case token.REM, token.AND, token.SHR:
|
|
return ra.isNonNegativeRecursive(v.X)
|
|
}
|
|
case *ssa.Const:
|
|
if val, ok := GetConstantInt64(v); ok && val >= 0 {
|
|
return true
|
|
}
|
|
case *ssa.Phi:
|
|
allNonNeg := true
|
|
for _, edge := range v.Edges {
|
|
if !ra.isNonNegativeRecursive(edge) {
|
|
if constVal, ok := edge.(*ssa.Const); ok {
|
|
if val, ok := GetConstantInt64(constVal); ok && val == -1 {
|
|
continue
|
|
}
|
|
}
|
|
allNonNeg = false
|
|
break
|
|
}
|
|
}
|
|
return allNonNeg
|
|
case *ssa.Convert:
|
|
if isUint(v.X) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) ComputeRange(v ssa.Value, block *ssa.BasicBlock) *rangeResult {
|
|
res := ra.acquireResult()
|
|
isSrcUnsigned := isUint(v)
|
|
|
|
switch v := v.(type) {
|
|
case *ssa.BinOp:
|
|
switch v.Op {
|
|
case token.ADD:
|
|
if val, ok := GetConstantInt64(v.Y); ok {
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if subRes.isRangeCheck {
|
|
if subRes.minValueSet {
|
|
res.minValue = toUint64(toInt64(subRes.minValue) + val)
|
|
res.minValueSet = true
|
|
}
|
|
if subRes.maxValueSet {
|
|
res.maxValue = toUint64(toInt64(subRes.maxValue) + val)
|
|
res.maxValueSet = true
|
|
}
|
|
if res.minValueSet || res.maxValueSet {
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
} else if val, ok := GetConstantInt64(v.X); ok {
|
|
subRes := ra.ResolveRange(v.Y, block)
|
|
if subRes.isRangeCheck {
|
|
if subRes.minValueSet {
|
|
res.minValue = toUint64(val + toInt64(subRes.minValue))
|
|
res.minValueSet = true
|
|
}
|
|
if subRes.maxValueSet {
|
|
res.maxValue = toUint64(val + toInt64(subRes.maxValue))
|
|
res.maxValueSet = true
|
|
}
|
|
if res.minValueSet || res.maxValueSet {
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
} else {
|
|
subResX := ra.ResolveRange(v.X, block)
|
|
subResY := ra.ResolveRange(v.Y, block)
|
|
if subResX.isRangeCheck || subResY.isRangeCheck {
|
|
if subResX.minValueSet && subResY.minValueSet {
|
|
constrainRange(res, toUint64(toInt64(subResX.minValue)+toInt64(subResY.minValue)), true, isSrcUnsigned)
|
|
}
|
|
if subResX.maxValueSet && subResY.maxValueSet {
|
|
constrainRange(res, toUint64(toInt64(subResX.maxValue)+toInt64(subResY.maxValue)), false, isSrcUnsigned)
|
|
}
|
|
// Ensure we set isRangeCheck if we computed valid bounds, even if inputs were not "range checks"
|
|
// per se but just constant propagations.
|
|
if res.minValueSet || res.maxValueSet {
|
|
res.isRangeCheck = true
|
|
}
|
|
} else if subResX.minValueSet && subResX.maxValueSet && subResY.minValueSet && subResY.maxValueSet {
|
|
// Constant folding case: inputs might be plain constants.
|
|
constrainRange(res, toUint64(toInt64(subResX.minValue)+toInt64(subResY.minValue)), true, isSrcUnsigned)
|
|
constrainRange(res, toUint64(toInt64(subResX.maxValue)+toInt64(subResY.maxValue)), false, isSrcUnsigned)
|
|
res.isRangeCheck = true
|
|
}
|
|
ra.releaseResult(subResX)
|
|
ra.releaseResult(subResY)
|
|
}
|
|
case token.SUB:
|
|
if val, ok := GetConstantInt64(v.Y); ok {
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if subRes.isRangeCheck {
|
|
if subRes.minValueSet {
|
|
constrainRange(res, toUint64(toInt64(subRes.minValue)-val), true, isSrcUnsigned)
|
|
}
|
|
if subRes.maxValueSet {
|
|
constrainRange(res, toUint64(toInt64(subRes.maxValue)-val), false, isSrcUnsigned)
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
} else if val, ok := GetConstantInt64(v.X); ok {
|
|
subRes := ra.ResolveRange(v.Y, block)
|
|
if subRes.isRangeCheck {
|
|
if subRes.maxValueSet {
|
|
// res = val - subRes.maxValue (this is the new min if subtract max)
|
|
constrainRange(res, toUint64(val-toInt64(subRes.maxValue)), true, isSrcUnsigned)
|
|
}
|
|
if subRes.minValueSet {
|
|
// res = val - subRes.minValue (this is the new max if subtract min)
|
|
constrainRange(res, toUint64(val-toInt64(subRes.minValue)), false, isSrcUnsigned)
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
} else {
|
|
subResX := ra.ResolveRange(v.X, block)
|
|
subResY := ra.ResolveRange(v.Y, block)
|
|
if subResX.isRangeCheck || subResY.isRangeCheck {
|
|
if subResX.minValueSet && subResY.maxValueSet {
|
|
// Min = MinX - MaxY
|
|
constrainRange(res, toUint64(toInt64(subResX.minValue)-toInt64(subResY.maxValue)), true, isSrcUnsigned)
|
|
}
|
|
if subResX.maxValueSet && subResY.minValueSet {
|
|
// Max = MaxX - MinY
|
|
constrainRange(res, toUint64(toInt64(subResX.maxValue)-toInt64(subResY.minValue)), false, isSrcUnsigned)
|
|
}
|
|
if res.minValueSet || res.maxValueSet {
|
|
res.isRangeCheck = true
|
|
}
|
|
} else if subResX.minValueSet && subResX.maxValueSet && subResY.minValueSet && subResY.maxValueSet {
|
|
// Constant folding case for SUB
|
|
constrainRange(res, toUint64(toInt64(subResX.minValue)-toInt64(subResY.maxValue)), true, isSrcUnsigned)
|
|
constrainRange(res, toUint64(toInt64(subResX.maxValue)-toInt64(subResY.minValue)), false, isSrcUnsigned)
|
|
res.isRangeCheck = true
|
|
}
|
|
ra.releaseResult(subResX)
|
|
ra.releaseResult(subResY)
|
|
}
|
|
case token.MUL:
|
|
val, ok := GetConstantInt64(v.Y)
|
|
if !ok {
|
|
val, ok = GetConstantInt64(v.X)
|
|
}
|
|
if ok && val != 0 {
|
|
var subRes *rangeResult
|
|
if _, isConst := v.Y.(*ssa.Const); isConst {
|
|
subRes = ra.ResolveRange(v.X, block)
|
|
} else {
|
|
subRes = ra.ResolveRange(v.Y, block)
|
|
}
|
|
|
|
if subRes.isRangeCheck || subRes.minValueSet || subRes.maxValueSet {
|
|
srcInt, _ := GetIntTypeInfo(v.X.Type())
|
|
if srcInt.Signed {
|
|
// Signed multiplication
|
|
if subRes.minValueSet && subRes.maxValueSet {
|
|
v1 := toInt64(subRes.minValue) * val
|
|
v2 := toInt64(subRes.maxValue) * val
|
|
vMin, vMax := v1, v2
|
|
if vMin > vMax {
|
|
vMin, vMax = vMax, vMin
|
|
}
|
|
if (val > 0 && v1/val == toInt64(subRes.minValue)) || (val < 0 && v1/val == toInt64(subRes.minValue)) {
|
|
constrainRange(res, toUint64(vMin), true, false)
|
|
constrainRange(res, toUint64(vMax), false, false)
|
|
res.isRangeCheck = subRes.isRangeCheck
|
|
}
|
|
}
|
|
} else {
|
|
// Unsigned multiplication
|
|
uVal := toUint64(val)
|
|
if subRes.maxValueSet {
|
|
hi, _ := bits.Mul64(subRes.maxValue, uVal)
|
|
if hi == 0 {
|
|
if subRes.minValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, subRes.minValue*uVal, true, true)
|
|
}
|
|
if subRes.maxValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, subRes.maxValue*uVal, false, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case token.SHL:
|
|
if val, ok := GetConstantInt64(v.Y); ok && val >= 0 {
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if subRes.minValueSet {
|
|
newMin := subRes.minValue << uint(val) // #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
// #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
if newMin>>uint(val) == subRes.minValue {
|
|
constrainRange(res, newMin, true, isSrcUnsigned)
|
|
}
|
|
}
|
|
if subRes.maxValueSet {
|
|
newMax := subRes.maxValue << uint(val) // #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
// #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
if newMax>>uint(val) == subRes.maxValue {
|
|
constrainRange(res, newMax, false, isSrcUnsigned)
|
|
}
|
|
}
|
|
}
|
|
case token.SHR:
|
|
if val, ok := GetConstantInt64(v.Y); ok && val >= 0 {
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if subRes.minValueSet {
|
|
constrainRange(res, subRes.minValue>>uint(val), true, isSrcUnsigned) // #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
}
|
|
if subRes.maxValueSet {
|
|
constrainRange(res, subRes.maxValue>>uint(val), false, isSrcUnsigned) // #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
} else {
|
|
// Even if we don't have a max value set, we know the upper bound from the type.
|
|
srcInt, _ := GetIntTypeInfo(v.X.Type())
|
|
res.maxValue = srcInt.Max >> uint(val) // #nosec G115 - WORKAROUND for old golangci-lint, remove when updated
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
case token.QUO:
|
|
if val, ok := GetConstantInt64(v.Y); ok && val != 0 {
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if val > 0 {
|
|
if subRes.minValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, toUint64(toInt64(subRes.minValue)/val), true, isSrcUnsigned)
|
|
}
|
|
if subRes.maxValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, toUint64(toInt64(subRes.maxValue)/val), false, isSrcUnsigned)
|
|
}
|
|
} else {
|
|
if subRes.maxValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, toUint64(toInt64(subRes.maxValue)/val), true, isSrcUnsigned)
|
|
}
|
|
if subRes.minValueSet && subRes.isRangeCheck {
|
|
constrainRange(res, toUint64(toInt64(subRes.minValue)/val), false, isSrcUnsigned)
|
|
}
|
|
}
|
|
}
|
|
case token.REM:
|
|
if val, ok := GetConstantInt64(v.Y); ok && val > 0 {
|
|
res.minValue = toUint64(-(val - 1))
|
|
res.maxValue = toUint64(val - 1)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
// If we know x >= 0, we can refine to [0, val-1]
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if (subRes.minValueSet && toInt64(subRes.minValue) >= 0) || ra.IsNonNegative(v.X) {
|
|
res.minValue = 0
|
|
}
|
|
ra.releaseResult(subRes)
|
|
}
|
|
case token.AND:
|
|
if val, ok := GetConstantInt64(v.Y); ok && val >= 0 {
|
|
res.minValue = 0
|
|
res.maxValue = uint64(val)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
} else if val, ok := GetConstantInt64(v.X); ok && val >= 0 {
|
|
res.minValue = 0
|
|
res.maxValue = uint64(val)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
case *ssa.UnOp:
|
|
switch v.Op {
|
|
case token.MUL:
|
|
// Dereference (Load)
|
|
if alloc, ok := v.X.(*ssa.Alloc); ok {
|
|
return ra.resolveAllocRange(alloc, block, v)
|
|
}
|
|
// Just recurse
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
res.CopyFrom(subRes)
|
|
ra.releaseResult(subRes)
|
|
case token.SUB:
|
|
// Negation (-X)
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
|
|
// If X in [min, max], then -X in [-max, -min]
|
|
// We need to work with int64 views for negation
|
|
srcBuff, _ := GetIntTypeInfo(v.X.Type())
|
|
if srcBuff.Signed {
|
|
// Negation only meaningful for signed integers.
|
|
if subRes.minValueSet && subRes.maxValueSet {
|
|
// If X in [min, max], then -X in [-max, -min].
|
|
// Internal uint64 representation handles -MinInt overflow correctly.
|
|
|
|
oldMin := toInt64(subRes.minValue)
|
|
oldMax := toInt64(subRes.maxValue)
|
|
|
|
res.minValue = toUint64(-oldMax)
|
|
res.maxValue = toUint64(-oldMin)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = subRes.isRangeCheck
|
|
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = subRes.isRangeCheck
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
}
|
|
case *ssa.Convert:
|
|
subRes := ra.ResolveRange(v.X, block)
|
|
if subRes.minValueSet && subRes.maxValueSet {
|
|
srcInt, err := GetIntTypeInfo(v.X.Type())
|
|
if err != nil {
|
|
return res
|
|
}
|
|
dstInt, err := GetIntTypeInfo(v.Type())
|
|
if err != nil {
|
|
return res
|
|
}
|
|
|
|
// Helper to convert/truncate a value to destination size
|
|
convertBound := func(val uint64) uint64 {
|
|
// Truncate/Mask to destination size
|
|
var newVal uint64
|
|
switch dstInt.Size {
|
|
case 8:
|
|
newVal = val & 0xFF
|
|
if dstInt.Signed {
|
|
// Sign extend 8->64
|
|
if newVal&0x80 != 0 {
|
|
newVal |= 0xFFFFFFFFFFFFFF00
|
|
}
|
|
}
|
|
case 16:
|
|
newVal = val & 0xFFFF
|
|
if dstInt.Signed {
|
|
// Sign extend 16->64
|
|
if newVal&0x8000 != 0 {
|
|
newVal |= 0xFFFFFFFFFFFF0000
|
|
}
|
|
}
|
|
case 32:
|
|
newVal = val & 0xFFFFFFFF
|
|
if dstInt.Signed {
|
|
// Sign extend 32->64
|
|
if newVal&0x80000000 != 0 {
|
|
newVal |= 0xFFFFFFFF00000000
|
|
}
|
|
}
|
|
default: // 64 or ptr
|
|
newVal = val
|
|
}
|
|
return newVal
|
|
}
|
|
|
|
newMin := convertBound(subRes.minValue)
|
|
newMax := convertBound(subRes.maxValue)
|
|
|
|
valid := false
|
|
if dstInt.Signed {
|
|
if toInt64(newMin) <= toInt64(newMax) {
|
|
// Check if old min/max are "safe" for the new type
|
|
// This heuristic ensures we don't accidentally wrap disjoint ranges into a safe interval.
|
|
// We only propagate if the source values fit into destination type OR
|
|
// if they were safe before and remain safe (e.g. extension).
|
|
|
|
// Checking if source values fit in destination domain is key for safety.
|
|
// If they fit, then min <= max holds and range is contiguous.
|
|
|
|
fits := func(v uint64) bool {
|
|
var v64 int64
|
|
if srcInt.Signed {
|
|
v64 = toInt64(v)
|
|
return v64 >= dstInt.Min && (dstInt.Size == 64 || v64 <= toInt64(dstInt.Max))
|
|
}
|
|
// Unsigned src
|
|
return v <= dstInt.Max
|
|
}
|
|
|
|
if fits(subRes.minValue) && fits(subRes.maxValue) {
|
|
valid = true
|
|
}
|
|
}
|
|
} else {
|
|
// Destination Unsigned
|
|
if newMin <= newMax {
|
|
fits := func(v uint64) bool {
|
|
var v64 int64
|
|
if srcInt.Signed {
|
|
v64 = toInt64(v)
|
|
return v64 >= 0 && uint64(v64) <= dstInt.Max
|
|
}
|
|
return v <= dstInt.Max
|
|
}
|
|
if fits(subRes.minValue) && fits(subRes.maxValue) {
|
|
valid = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if valid {
|
|
res.minValue = newMin
|
|
res.maxValue = newMax
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
ra.releaseResult(subRes)
|
|
case *ssa.Call:
|
|
if fn, ok := v.Call.Value.(*ssa.Builtin); ok {
|
|
switch fn.Name() {
|
|
case "min":
|
|
if len(v.Call.Args) > 0 {
|
|
for i, arg := range v.Call.Args {
|
|
argRes := ra.ResolveRange(arg, block)
|
|
if i == 0 {
|
|
res.CopyFrom(argRes)
|
|
} else {
|
|
res.minValue = minBounds(res.minValue, res.minValueSet, argRes.minValue, argRes.minValueSet, isSrcUnsigned)
|
|
res.minValueSet = res.minValueSet && argRes.minValueSet
|
|
res.maxValue = minBounds(res.maxValue, res.maxValueSet, argRes.maxValue, argRes.maxValueSet, isSrcUnsigned)
|
|
res.maxValueSet = res.maxValueSet && argRes.maxValueSet
|
|
}
|
|
ra.releaseResult(argRes)
|
|
}
|
|
res.isRangeCheck = true
|
|
}
|
|
case "max":
|
|
if len(v.Call.Args) > 0 {
|
|
for i, arg := range v.Call.Args {
|
|
argRes := ra.ResolveRange(arg, block)
|
|
if i == 0 {
|
|
res.CopyFrom(argRes)
|
|
} else {
|
|
res.minValue = maxBounds(res.minValue, res.minValueSet, argRes.minValue, argRes.minValueSet, isSrcUnsigned)
|
|
res.minValueSet = res.minValueSet && argRes.minValueSet
|
|
res.maxValue = maxBounds(res.maxValue, res.maxValueSet, argRes.maxValue, argRes.maxValueSet, isSrcUnsigned)
|
|
res.maxValueSet = res.maxValueSet && argRes.maxValueSet
|
|
}
|
|
ra.releaseResult(argRes)
|
|
}
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
}
|
|
case *ssa.Phi:
|
|
isSrcUnsigned := isUint(v)
|
|
for _, edge := range v.Edges {
|
|
subRes := ra.ResolveRange(edge, block)
|
|
if subRes.minValueSet {
|
|
expandRange(res, subRes.minValue, true, isSrcUnsigned)
|
|
}
|
|
if subRes.maxValueSet {
|
|
expandRange(res, subRes.maxValue, false, isSrcUnsigned)
|
|
}
|
|
ra.releaseResult(subRes)
|
|
}
|
|
case *ssa.Extract:
|
|
if v.Index == 0 {
|
|
if call, ok := v.Tuple.(*ssa.Call); ok {
|
|
if callee := call.Call.StaticCallee(); callee != nil {
|
|
switch callee.Name() {
|
|
case "ParseInt":
|
|
if len(call.Call.Args) == 3 {
|
|
if bitSizeVal, ok := GetConstantInt64(call.Call.Args[2]); ok {
|
|
shift := int(bitSizeVal) - 1
|
|
if shift >= 0 && shift < 64 {
|
|
res.minValue = toUint64(-1 << shift)
|
|
res.maxValue = toUint64((1 << shift) - 1)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
}
|
|
case "ParseUint":
|
|
if len(call.Call.Args) == 3 {
|
|
if bitSizeVal, ok := GetConstantInt64(call.Call.Args[2]); ok {
|
|
if bitSizeVal == 64 {
|
|
res.maxValue = maxUint64
|
|
} else if bitSizeVal > 0 && bitSizeVal < 64 {
|
|
res.maxValue = (1 << bitSizeVal) - 1
|
|
}
|
|
res.minValue = 0
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case *ssa.Const:
|
|
if val, ok := GetConstantInt64(v); ok {
|
|
res.minValue = toUint64(val)
|
|
res.maxValue = toUint64(val)
|
|
res.minValueSet = true
|
|
res.maxValueSet = true
|
|
res.isRangeCheck = true
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// ResolveByteRange determines the absolute byte range of 'val' relative to its
|
|
// underlying root allocation by recursively resolving slice offsets and indices.
|
|
func (ra *RangeAnalyzer) ResolveByteRange(val ssa.Value) (ByteRange, bool) {
|
|
if r, ok := ra.ByteRangeCache[val]; ok {
|
|
return r, true
|
|
}
|
|
|
|
if ra.Depth > MaxDepth {
|
|
return ByteRange{}, false
|
|
}
|
|
ra.Depth++
|
|
defer func() { ra.Depth-- }()
|
|
|
|
res, ok := ra.recursiveByteRange(val)
|
|
if ok {
|
|
ra.ByteRangeCache[val] = res
|
|
}
|
|
return res, ok
|
|
}
|
|
|
|
// recursiveByteRange is a helper for ResolveByteRange that traverses up the SSA value chain
|
|
// (handling Slice, IndexAddr, Convert, etc.) to compute the range.
|
|
func (ra *RangeAnalyzer) recursiveByteRange(val ssa.Value) (ByteRange, bool) {
|
|
switch v := val.(type) {
|
|
case *ssa.Alloc:
|
|
l := ra.BufferedLen(v)
|
|
if l <= 0 {
|
|
// If it is a local variable slot, try to find what was stored in it
|
|
if refs := v.Referrers(); refs != nil {
|
|
for _, ref := range *refs {
|
|
if st, ok := ref.(*ssa.Store); ok && st.Addr == v {
|
|
return ra.recursiveByteRange(st.Val)
|
|
}
|
|
}
|
|
}
|
|
return ByteRange{}, false
|
|
}
|
|
return ByteRange{0, l}, true
|
|
case *ssa.MakeSlice:
|
|
if l, ok := GetConstantInt64(v.Len); ok && l > 0 {
|
|
return ByteRange{0, l}, true
|
|
}
|
|
return ByteRange{}, false
|
|
case *ssa.Convert:
|
|
if c, ok := v.X.(*ssa.Const); ok && c.Value.Kind() == constant.String {
|
|
l := int64(len(constant.StringVal(c.Value)))
|
|
if l > 0 {
|
|
return ByteRange{0, l}, true
|
|
}
|
|
}
|
|
return ByteRange{}, false
|
|
case *ssa.Slice:
|
|
parentRange, ok := ra.recursiveByteRange(v.X)
|
|
if !ok {
|
|
return ByteRange{}, false
|
|
}
|
|
|
|
var low int64
|
|
if v.Low != nil {
|
|
l, ok := GetConstantInt64(v.Low)
|
|
if !ok {
|
|
res := ra.ResolveRange(v.Low, v.Block())
|
|
if res.isRangeCheck && res.maxValueSet {
|
|
l = toInt64(res.maxValue)
|
|
} else {
|
|
return ByteRange{}, false
|
|
}
|
|
ra.releaseResult(res)
|
|
}
|
|
low = l
|
|
}
|
|
|
|
var high int64
|
|
if v.High == nil {
|
|
high = parentRange.High
|
|
} else {
|
|
h, ok := GetConstantInt64(v.High)
|
|
if !ok {
|
|
res := ra.ResolveRange(v.High, v.Block())
|
|
if res.isRangeCheck && res.maxValueSet {
|
|
h = toInt64(res.maxValue)
|
|
} else {
|
|
return ByteRange{}, false
|
|
}
|
|
ra.releaseResult(res)
|
|
}
|
|
high = parentRange.Low + h
|
|
}
|
|
|
|
newLow := parentRange.Low + low
|
|
newHigh := min(high, parentRange.High)
|
|
if newLow >= newHigh {
|
|
return ByteRange{newLow, newLow}, true // Handle empty slices consistently
|
|
}
|
|
return ByteRange{newLow, newHigh}, true
|
|
case *ssa.IndexAddr:
|
|
parentRange, ok := ra.recursiveByteRange(v.X)
|
|
if !ok {
|
|
return ByteRange{}, false
|
|
}
|
|
if c, ok := GetConstantInt64(v.Index); ok {
|
|
start := parentRange.Low + c
|
|
return ByteRange{start, start + 1}, true
|
|
}
|
|
// Check for explicit range checks.
|
|
res := ra.ResolveRange(v.Index, v.Block())
|
|
if res.isRangeCheck && res.minValueSet && res.maxValueSet {
|
|
minVal := toInt64(res.minValue)
|
|
maxVal := toInt64(res.maxValue)
|
|
if minVal > maxVal {
|
|
// Contradictory range.
|
|
return ByteRange{parentRange.Low, parentRange.High}, true
|
|
}
|
|
start := parentRange.Low + minVal
|
|
end := parentRange.Low + maxVal + 1
|
|
ra.releaseResult(res)
|
|
return ByteRange{start, end}, true
|
|
}
|
|
ra.releaseResult(res)
|
|
return ByteRange{}, false
|
|
case *ssa.UnOp:
|
|
if v.Op == token.MUL {
|
|
return ra.recursiveByteRange(v.X)
|
|
}
|
|
}
|
|
return ByteRange{}, false
|
|
}
|
|
|
|
// BufferedLen attempts to find the constant length of a buffer/slice/array, using cache if available.
|
|
func (ra *RangeAnalyzer) BufferedLen(val ssa.Value) int64 {
|
|
if res, ok := ra.BufferLenCache[val]; ok {
|
|
return res
|
|
}
|
|
length := GetBufferLen(val)
|
|
ra.BufferLenCache[val] = length
|
|
return length
|
|
}
|
|
|
|
// Precedes returns true if instruction a is executed before instruction b.
|
|
// It assumes both instructions belong to the same function.
|
|
func (ra *RangeAnalyzer) Precedes(a, b ssa.Instruction) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
if a.Block() != b.Block() {
|
|
return ra.IsReachable(a.Block(), b.Block())
|
|
}
|
|
// Same block: check order in Instrs
|
|
for _, instr := range a.Block().Instrs {
|
|
if instr == a {
|
|
return true
|
|
}
|
|
if instr == b {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsRangeCheck determines if an instruction is part of a range check for a value.
|
|
func IsRangeCheck(v ssa.Value, x ssa.Value) bool {
|
|
compareVal, _ := getRealValueFromOperation(x)
|
|
switch op := v.(type) {
|
|
case *ssa.BinOp:
|
|
switch op.Op {
|
|
case token.LSS, token.LEQ, token.GTR, token.GEQ, token.EQL, token.NEQ:
|
|
leftMatch := isSameOrRelated(op.X, x) || isSameOrRelated(op.X, compareVal)
|
|
if !leftMatch {
|
|
if rVal, _ := getRealValueFromOperation(op.X); rVal == x || (compareVal != nil && rVal == compareVal) {
|
|
leftMatch = true
|
|
}
|
|
}
|
|
rightMatch := isSameOrRelated(op.Y, x) || isSameOrRelated(op.Y, compareVal)
|
|
if !rightMatch {
|
|
if rVal, _ := getRealValueFromOperation(op.Y); rVal == x || (compareVal != nil && rVal == compareVal) {
|
|
rightMatch = true
|
|
}
|
|
}
|
|
return leftMatch || rightMatch
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func updateExplicitValues(result *rangeResult, val int64) {
|
|
if val < 0 {
|
|
result.explicitNegativeVals = append(result.explicitNegativeVals, int(val))
|
|
} else {
|
|
result.explicitPositiveVals = append(result.explicitPositiveVals, uint(val))
|
|
}
|
|
result.minValue = toUint64(val)
|
|
result.maxValue = toUint64(val)
|
|
result.minValueSet = true
|
|
result.maxValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
|
|
func updateMinMaxForLessOrEqual(result *rangeResult, val int64, op token.Token, operandsFlipped bool, successPathConvert bool) {
|
|
if successPathConvert != operandsFlipped {
|
|
result.maxValue = toUint64(val)
|
|
if (op == token.LSS && successPathConvert) || (op == token.LEQ && !successPathConvert) {
|
|
result.maxValue--
|
|
}
|
|
result.maxValueSet = true
|
|
result.isRangeCheck = true
|
|
} else {
|
|
// Path where x >= val
|
|
result.minValue = toUint64(val)
|
|
if (op == token.LEQ && !successPathConvert) || (op == token.LSS && successPathConvert) {
|
|
result.minValue++ // !(x <= val) -> x > val
|
|
}
|
|
result.minValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
}
|
|
|
|
func updateMinMaxForGreaterOrEqual(result *rangeResult, val int64, op token.Token, operandsFlipped bool, successPathConvert bool) {
|
|
if successPathConvert != operandsFlipped {
|
|
result.minValue = toUint64(val)
|
|
if (op == token.GTR && successPathConvert) || (op == token.GEQ && !successPathConvert) {
|
|
result.minValue++
|
|
}
|
|
result.minValueSet = true
|
|
result.isRangeCheck = true
|
|
} else {
|
|
// Path where x < val
|
|
result.maxValue = toUint64(val)
|
|
if (op == token.GEQ && !successPathConvert) || (op == token.GTR && successPathConvert) {
|
|
result.maxValue-- // !(x >= val) -> x < val
|
|
}
|
|
result.maxValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
}
|
|
|
|
// constrainRange updates the min or max value of the result range if the new value is tighter (intersection).
|
|
func constrainRange(result *rangeResult, newVal uint64, isMin bool, isSrcUnsigned bool) {
|
|
if isMin {
|
|
if !result.minValueSet || (isSrcUnsigned && newVal > result.minValue) || (!isSrcUnsigned && toInt64(newVal) > toInt64(result.minValue)) {
|
|
result.minValue = newVal
|
|
result.minValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
} else {
|
|
if !result.maxValueSet || (isSrcUnsigned && newVal < result.maxValue) || (!isSrcUnsigned && toInt64(newVal) < toInt64(result.maxValue)) {
|
|
result.maxValue = newVal
|
|
result.maxValueSet = true
|
|
result.isRangeCheck = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// mergeRanges takes a list of ByteRanges and merges overlapping or contiguous ranges.
|
|
// It modifies the input slice in-place to reduce allocations and returns a slice of disjoint ranges.
|
|
func mergeRanges(ranges []ByteRange) []ByteRange {
|
|
if len(ranges) <= 1 {
|
|
return ranges
|
|
}
|
|
slices.SortFunc(ranges, func(a, b ByteRange) int {
|
|
return cmp.Compare(a.Low, b.Low)
|
|
})
|
|
|
|
// In-place merge
|
|
// 'idx' points to the position of the 'current' merged range being built.
|
|
idx := 0
|
|
for _, r := range ranges[1:] {
|
|
if r.Low <= ranges[idx].High {
|
|
ranges[idx].High = max(ranges[idx].High, r.High)
|
|
} else {
|
|
idx++
|
|
ranges[idx] = r
|
|
}
|
|
}
|
|
return ranges[:idx+1]
|
|
}
|
|
|
|
// subtractRange removes 'taint' range from the list of 'safe' ranges, potentially
|
|
// splitting existing safe ranges into two separate fragments. The results are appended to 'dest'.
|
|
func subtractRange(safe []ByteRange, taint ByteRange, dest *[]ByteRange) {
|
|
*dest = (*dest)[:0]
|
|
for _, r := range safe {
|
|
// No overlap
|
|
if r.High <= taint.Low || r.Low >= taint.High {
|
|
*dest = append(*dest, r)
|
|
continue
|
|
}
|
|
|
|
if r.Low < taint.Low {
|
|
*dest = append(*dest, ByteRange{r.Low, taint.Low})
|
|
}
|
|
if r.High > taint.High {
|
|
*dest = append(*dest, ByteRange{taint.High, r.High})
|
|
}
|
|
}
|
|
}
|
|
|
|
// expandRange updates the min or max value of the result range if the new value expands the range (union).
|
|
func expandRange(result *rangeResult, newVal uint64, isMin bool, isSrcUnsigned bool) {
|
|
if isMin {
|
|
if !result.minValueSet {
|
|
result.minValue = newVal
|
|
result.minValueSet = true
|
|
} else {
|
|
if isSrcUnsigned {
|
|
if newVal < result.minValue {
|
|
result.minValue = newVal
|
|
}
|
|
} else {
|
|
if toInt64(newVal) < toInt64(result.minValue) {
|
|
result.minValue = newVal
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if !result.maxValueSet {
|
|
result.maxValue = newVal
|
|
result.maxValueSet = true
|
|
} else {
|
|
if isSrcUnsigned {
|
|
if newVal > result.maxValue {
|
|
result.maxValue = newVal
|
|
}
|
|
} else {
|
|
if toInt64(newVal) > toInt64(result.maxValue) {
|
|
result.maxValue = newVal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ra *RangeAnalyzer) resolveAllocRange(alloc *ssa.Alloc, block *ssa.BasicBlock, loadInstr ssa.Instruction) *rangeResult {
|
|
res := ra.acquireResult()
|
|
|
|
// 1. Same-block reaching definition check.
|
|
if loadInstr != nil && loadInstr.Block() == block {
|
|
// Traverse backwards from loadInstr
|
|
found := false
|
|
var nearestStore *ssa.Store
|
|
|
|
// Scan backwards
|
|
instrs := block.Instrs
|
|
startIndex := -1
|
|
|
|
// Find the index of the load instruction to start scanning backwards from it.
|
|
for i := len(instrs) - 1; i >= 0; i-- {
|
|
if instrs[i] == loadInstr {
|
|
startIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if startIndex != -1 {
|
|
for i := startIndex - 1; i >= 0; i-- {
|
|
if store, ok := instrs[i].(*ssa.Store); ok && store.Addr == alloc {
|
|
nearestStore = store
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if found {
|
|
storeRes := ra.ResolveRange(nearestStore.Val, block)
|
|
res.CopyFrom(storeRes)
|
|
res.isRangeCheck = storeRes.isRangeCheck // Inherit properties
|
|
ra.releaseResult(storeRes)
|
|
return res
|
|
}
|
|
}
|
|
|
|
// 2. Fallback: Union of all stores.
|
|
first := true
|
|
|
|
refs := alloc.Referrers()
|
|
if refs == nil {
|
|
return res // No refs, unknown
|
|
}
|
|
|
|
for _, ref := range *refs {
|
|
if store, ok := ref.(*ssa.Store); ok && store.Addr == alloc {
|
|
storeRes := ra.ResolveRange(store.Val, block)
|
|
|
|
if first {
|
|
res.CopyFrom(storeRes)
|
|
if storeRes.minValueSet || storeRes.maxValueSet {
|
|
first = false
|
|
}
|
|
} else {
|
|
// Merge: broaden the range
|
|
// Union:
|
|
// Min = Min(currentMin, newMin)
|
|
// Max = Max(currentMax, newMax)
|
|
|
|
// Handling signed/unsigned mix is tricky. Assuming types match generally for the alloc.
|
|
elemType := alloc.Type().(*types.Pointer).Elem()
|
|
basic, ok := elemType.Underlying().(*types.Basic)
|
|
isUnsignedElem := ok && (basic.Info()&types.IsUnsigned != 0)
|
|
|
|
if storeRes.minValueSet {
|
|
expandRange(res, storeRes.minValue, true, isUnsignedElem)
|
|
} else {
|
|
res.minValueSet = false // If one path has unknown min, union is unknown
|
|
}
|
|
|
|
if storeRes.maxValueSet {
|
|
expandRange(res, storeRes.maxValue, false, isUnsignedElem)
|
|
} else {
|
|
res.maxValueSet = false
|
|
}
|
|
|
|
// Propagate isRangeCheck if any of the sources have it.
|
|
res.isRangeCheck = res.isRangeCheck || storeRes.isRangeCheck
|
|
}
|
|
ra.releaseResult(storeRes)
|
|
}
|
|
}
|
|
|
|
// If no stores were found, assume default/zero value.
|
|
if first {
|
|
// Default 0.
|
|
res.minValue = 0
|
|
res.maxValue = 0
|
|
res.maxValueSet = true
|
|
}
|
|
|
|
return res
|
|
}
|