Skip to content

TaintTracking: copy inside range on map produces multiple (incomplete) path variants if struct or field are a pointer #9308

Open

Description

@gagliardetto

Summary

When a transfer of taint happens in a range loop, and the domain is a pointer to a struct and the field is a map, then the results will contain multiple paths.

Preamble code

package main

func main() {}

func source() interface{} { return nil }

func sink(v interface{})                   {}
func link(in interface{}, out interface{}) {}

type StructWithPointerField struct {
	Params *map[string]string
}
type StructWithValueField struct {
	Params map[string]string
}

Cases with domain = a field of a struct (spoiler: most produce unexpected results)

func range_2A() {
	src := source().(StructWithValueField)

	{
		data := make(map[string]interface{})
		for key, value := range src.Params {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_2B() {
	src := source().(*StructWithValueField)

	{
		data := make(map[string]interface{})
		for key, value := range src.Params {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (multiple paths, which is UNexpected):

image


func range_2C() {
	src := source().(StructWithPointerField)

	{
		data := make(map[string]interface{})
		for key, value := range *src.Params {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (multiple paths, which is UNexpected):

image


func range_2D() {
	src := source().(*StructWithPointerField)

	{
		data := make(map[string]interface{})
		for key, value := range *src.Params {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (multiple paths, which is UNexpected):

image

Cases with domain = map (spoiler: all satisfy expectations)

func range_1A() {
	src := source().(map[string]string)

	{
		data := make(map[string]interface{})
		for key, value := range src {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1B() {
	src := source().(*map[string]string)

	{
		data := make(map[string]interface{})
		for key, value := range *src {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1C() {
	src := source().(*map[*string]string)

	{
		data := make(map[string]interface{})
		for key, value := range *src {
			data[*key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1D() {
	src := source().(*map[string]*string)

	{
		data := make(map[string]interface{})
		for key, value := range *src {
			data[key] = *value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1E() {
	src := source().(*map[*string]*string)

	{
		data := make(map[string]interface{})
		for key, value := range *src {
			data[*key] = *value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1F() {
	src := source().(string)

	var (
		Map map[string]string
	)
	link([]byte(src), &Map)

	{
		data := make(map[string]interface{})
		for key, value := range Map {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1G() {
	src := source().(string)

	Map := new(map[string]string)
	link([]byte(src), Map)

	{
		data := make(map[string]interface{})
		for key, value := range *Map {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image


func range_1H() {
	src := source().(string)

	Map := make(map[string]string)
	link([]byte(src), &Map)

	{
		data := make(map[string]interface{})
		for key, value := range Map {
			data[key] = value
		}
		sink(data)
	}
}

nets the following result (one path, which is expected):

image

Activity

  1. gagliardetto commented on Mar 24, 2021

    @gagliardetto
    ContributorAuthor

    Go source code:

    package main
    
    func main() {}
    
    func source() interface{} { return nil }
    
    func sink(v interface{})                   {}
    func link(in interface{}, out interface{}) {}
    
    type StructWithPointerField struct {
    	Params *map[string]string
    }
    type StructWithValueField struct {
    	Params map[string]string
    }
    
    func range_1A() {
    	src := source().(map[string]string)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range src {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_1B() {
    	src := source().(*map[string]string)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_1C() {
    	src := source().(*map[*string]string)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src {
    			data[*key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_1D() {
    	src := source().(*map[string]*string)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src {
    			data[key] = *value
    		}
    		sink(data)
    	}
    }
    
    func range_1E() {
    	src := source().(*map[*string]*string)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src {
    			data[*key] = *value
    		}
    		sink(data)
    	}
    }
    
    func range_1F() {
    	src := source().(string)
    
    	var (
    		Map map[string]string
    	)
    	link([]byte(src), &Map)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range Map {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_1G() {
    	src := source().(string)
    
    	Map := new(map[string]string)
    	link([]byte(src), Map)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *Map {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_1H() {
    	src := source().(string)
    
    	Map := make(map[string]string)
    	link([]byte(src), &Map)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range Map {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    /////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////
    
    func range_2A() {
    	src := source().(StructWithValueField)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range src.Params {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_2B() {
    	src := source().(*StructWithValueField)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range src.Params {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_2C() {
    	src := source().(StructWithPointerField)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src.Params {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
    
    func range_2D() {
    	src := source().(*StructWithPointerField)
    
    	{
    		data := make(map[string]interface{})
    		for key, value := range *src.Params {
    			data[key] = value
    		}
    		sink(data)
    	}
    }
  2. gagliardetto commented on Mar 24, 2021

    @gagliardetto
    ContributorAuthor

    Codeql query code:

    /**
     * @kind path-problem
     */
    
    import go
    import DataFlow::PathGraph
    
    /* A special helper function used inside the test code */
    class Link extends TaintTracking::FunctionModel {
      Link() { hasQualifiedName(_, "link") }
    
      override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) {
        inp.isParameter(0) and outp.isParameter(1)
      }
    }
    
    class FlowConf extends TaintTracking::Configuration {
      FlowConf() { this = "FlowConf" }
    
      predicate isSource(DataFlow::Node source, DataFlow::CallNode call) {
        exists(Function fn | fn.hasQualifiedName(_, "source") |
          call = fn.getACall() and source = call.getResult()
        )
      }
    
      predicate isSink(DataFlow::Node sink, DataFlow::CallNode call) {
        exists(Function fn | fn.hasQualifiedName(_, "sink") |
          call = fn.getACall() and sink = call.getArgument(0)
        )
      }
    
      override predicate isSource(DataFlow::Node source) { isSource(source, _) }
    
      override predicate isSink(DataFlow::Node sink) { isSink(sink, _) }
    }
    
    from FlowConf cfg, DataFlow::PathNode source, DataFlow::PathNode sink
    where cfg.hasFlowPath(source, sink)
    select sink.getNode(), source, sink,
      source.getNode().getRoot().(Decl).(FuncDecl).getName() + ": Flow from $@ to $@.",
      source.getNode(), "source", sink.getNode(), "sink"
  3. gagliardetto commented on Mar 24, 2021

    @gagliardetto
    ContributorAuthor

    The RangeCopy.expected file:

    edges
    | RangeCopy.go:18:9:18:16 | call to source : interface type | RangeCopy.go:25:8:25:11 | data |
    | RangeCopy.go:30:9:30:16 | call to source : interface type | RangeCopy.go:37:8:37:11 | data |
    | RangeCopy.go:42:9:42:16 | call to source : interface type | RangeCopy.go:49:8:49:11 | data |
    | RangeCopy.go:54:9:54:16 | call to source : interface type | RangeCopy.go:61:8:61:11 | data |
    | RangeCopy.go:66:9:66:16 | call to source : interface type | RangeCopy.go:73:8:73:11 | data |
    | RangeCopy.go:78:9:78:16 | call to source : interface type | RangeCopy.go:83:7:83:17 | type conversion : interface type |
    | RangeCopy.go:83:7:83:17 | type conversion : interface type | RangeCopy.go:90:8:90:11 | data |
    | RangeCopy.go:95:9:95:16 | call to source : interface type | RangeCopy.go:98:7:98:17 | type conversion : interface type |
    | RangeCopy.go:98:7:98:17 | type conversion : interface type | RangeCopy.go:105:8:105:11 | data |
    | RangeCopy.go:110:9:110:16 | call to source : interface type | RangeCopy.go:113:7:113:17 | type conversion : interface type |
    | RangeCopy.go:113:7:113:17 | type conversion : interface type | RangeCopy.go:120:8:120:11 | data |
    | RangeCopy.go:129:9:129:16 | call to source : interface type | RangeCopy.go:136:8:136:11 | data |
    | RangeCopy.go:141:9:141:16 | call to source : interface type | RangeCopy.go:145:27:145:29 | implicit dereference : StructWithValueField |
    | RangeCopy.go:141:9:141:16 | call to source : interface type | RangeCopy.go:148:8:148:11 | data |
    | RangeCopy.go:145:27:145:29 | implicit dereference : StructWithValueField | RangeCopy.go:145:27:145:29 | implicit dereference : StructWithValueField |
    | RangeCopy.go:145:27:145:29 | implicit dereference : StructWithValueField | RangeCopy.go:148:8:148:11 | data |
    | RangeCopy.go:153:9:153:16 | call to source : interface type | RangeCopy.go:157:28:157:37 | selection of Params : pointer type |
    | RangeCopy.go:153:9:153:16 | call to source : interface type | RangeCopy.go:160:8:160:11 | data |
    | RangeCopy.go:157:28:157:37 | selection of Params : pointer type | RangeCopy.go:157:28:157:37 | selection of Params : pointer type |
    | RangeCopy.go:157:28:157:37 | selection of Params : pointer type | RangeCopy.go:160:8:160:11 | data |
    | RangeCopy.go:165:9:165:16 | call to source : interface type | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField |
    | RangeCopy.go:165:9:165:16 | call to source : interface type | RangeCopy.go:169:28:169:37 | selection of Params : pointer type |
    | RangeCopy.go:165:9:165:16 | call to source : interface type | RangeCopy.go:172:8:172:11 | data |
    | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField |
    | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField | RangeCopy.go:169:28:169:37 | selection of Params : pointer type |
    | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField | RangeCopy.go:172:8:172:11 | data |
    | RangeCopy.go:169:28:169:37 | selection of Params : pointer type | RangeCopy.go:169:28:169:37 | selection of Params : pointer type |
    | RangeCopy.go:169:28:169:37 | selection of Params : pointer type | RangeCopy.go:172:8:172:11 | data |
    nodes
    | RangeCopy.go:18:9:18:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:25:8:25:11 | data | semmle.label | data |
    | RangeCopy.go:30:9:30:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:37:8:37:11 | data | semmle.label | data |
    | RangeCopy.go:42:9:42:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:49:8:49:11 | data | semmle.label | data |
    | RangeCopy.go:54:9:54:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:61:8:61:11 | data | semmle.label | data |
    | RangeCopy.go:66:9:66:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:73:8:73:11 | data | semmle.label | data |
    | RangeCopy.go:78:9:78:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:83:7:83:17 | type conversion : interface type | semmle.label | type conversion : interface type |
    | RangeCopy.go:90:8:90:11 | data | semmle.label | data |
    | RangeCopy.go:95:9:95:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:98:7:98:17 | type conversion : interface type | semmle.label | type conversion : interface type |
    | RangeCopy.go:105:8:105:11 | data | semmle.label | data |
    | RangeCopy.go:110:9:110:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:113:7:113:17 | type conversion : interface type | semmle.label | type conversion : interface type |
    | RangeCopy.go:120:8:120:11 | data | semmle.label | data |
    | RangeCopy.go:129:9:129:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:136:8:136:11 | data | semmle.label | data |
    | RangeCopy.go:141:9:141:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:145:27:145:29 | implicit dereference : StructWithValueField | semmle.label | implicit dereference : StructWithValueField |
    | RangeCopy.go:148:8:148:11 | data | semmle.label | data |
    | RangeCopy.go:153:9:153:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:157:28:157:37 | selection of Params : pointer type | semmle.label | selection of Params : pointer type |
    | RangeCopy.go:160:8:160:11 | data | semmle.label | data |
    | RangeCopy.go:165:9:165:16 | call to source : interface type | semmle.label | call to source : interface type |
    | RangeCopy.go:169:28:169:30 | implicit dereference : StructWithPointerField | semmle.label | implicit dereference : StructWithPointerField |
    | RangeCopy.go:169:28:169:37 | selection of Params : pointer type | semmle.label | selection of Params : pointer type |
    | RangeCopy.go:172:8:172:11 | data | semmle.label | data |
    #select
    | RangeCopy.go:25:8:25:11 | data | RangeCopy.go:18:9:18:16 | call to source : interface type | RangeCopy.go:25:8:25:11 | data | range_1A: Flow from $@ to $@. | RangeCopy.go:18:9:18:16 | call to source | source | RangeCopy.go:25:8:25:11 | data | sink |
    | RangeCopy.go:37:8:37:11 | data | RangeCopy.go:30:9:30:16 | call to source : interface type | RangeCopy.go:37:8:37:11 | data | range_1B: Flow from $@ to $@. | RangeCopy.go:30:9:30:16 | call to source | source | RangeCopy.go:37:8:37:11 | data | sink |
    | RangeCopy.go:49:8:49:11 | data | RangeCopy.go:42:9:42:16 | call to source : interface type | RangeCopy.go:49:8:49:11 | data | range_1C: Flow from $@ to $@. | RangeCopy.go:42:9:42:16 | call to source | source | RangeCopy.go:49:8:49:11 | data | sink |
    | RangeCopy.go:61:8:61:11 | data | RangeCopy.go:54:9:54:16 | call to source : interface type | RangeCopy.go:61:8:61:11 | data | range_1D: Flow from $@ to $@. | RangeCopy.go:54:9:54:16 | call to source | source | RangeCopy.go:61:8:61:11 | data | sink |
    | RangeCopy.go:73:8:73:11 | data | RangeCopy.go:66:9:66:16 | call to source : interface type | RangeCopy.go:73:8:73:11 | data | range_1E: Flow from $@ to $@. | RangeCopy.go:66:9:66:16 | call to source | source | RangeCopy.go:73:8:73:11 | data | sink |
    | RangeCopy.go:90:8:90:11 | data | RangeCopy.go:78:9:78:16 | call to source : interface type | RangeCopy.go:90:8:90:11 | data | range_1F: Flow from $@ to $@. | RangeCopy.go:78:9:78:16 | call to source | source | RangeCopy.go:90:8:90:11 | data | sink |
    | RangeCopy.go:105:8:105:11 | data | RangeCopy.go:95:9:95:16 | call to source : interface type | RangeCopy.go:105:8:105:11 | data | range_1G: Flow from $@ to $@. | RangeCopy.go:95:9:95:16 | call to source | source | RangeCopy.go:105:8:105:11 | data | sink |
    | RangeCopy.go:120:8:120:11 | data | RangeCopy.go:110:9:110:16 | call to source : interface type | RangeCopy.go:120:8:120:11 | data | range_1H: Flow from $@ to $@. | RangeCopy.go:110:9:110:16 | call to source | source | RangeCopy.go:120:8:120:11 | data | sink |
    | RangeCopy.go:136:8:136:11 | data | RangeCopy.go:129:9:129:16 | call to source : interface type | RangeCopy.go:136:8:136:11 | data | range_2A: Flow from $@ to $@. | RangeCopy.go:129:9:129:16 | call to source | source | RangeCopy.go:136:8:136:11 | data | sink |
    | RangeCopy.go:148:8:148:11 | data | RangeCopy.go:141:9:141:16 | call to source : interface type | RangeCopy.go:148:8:148:11 | data | range_2B: Flow from $@ to $@. | RangeCopy.go:141:9:141:16 | call to source | source | RangeCopy.go:148:8:148:11 | data | sink |
    | RangeCopy.go:160:8:160:11 | data | RangeCopy.go:153:9:153:16 | call to source : interface type | RangeCopy.go:160:8:160:11 | data | range_2C: Flow from $@ to $@. | RangeCopy.go:153:9:153:16 | call to source | source | RangeCopy.go:160:8:160:11 | data | sink |
    | RangeCopy.go:172:8:172:11 | data | RangeCopy.go:165:9:165:16 | call to source : interface type | RangeCopy.go:172:8:172:11 | data | range_2D: Flow from $@ to $@. | RangeCopy.go:165:9:165:16 | call to source | source | RangeCopy.go:172:8:172:11 | data | sink |
    
  4. gagliardetto commented on Mar 25, 2021

    @gagliardetto
    ContributorAuthor

    @smowton As per our discussion, I think this is the cause of result inconsistency.

  5. gagliardetto commented on Mar 25, 2021

    @gagliardetto
    ContributorAuthor

    Another separate issue is that there's no mention of the range loop step in the tain flow steps' list.

  6. smowton commented on Mar 25, 2021

    @smowton
    Contributor

    The root cause of all this is our algorithm for exposing steps in a path explanation. The actual paths are basically fine, as you can see using something like

    query predicate edge(DataFlow::Node pred, DataFlow::Node succ) {
      TaintTracking::localTaintStep(pred, succ) and succ.getRoot().(FuncDef).getName() = "range_2D"
    }
    

    If you follow definitions from PathGraph::edges, you'll eventually get to localFlowBigStep, which is drawing a maximal subsequence of small steps (here exposed by localTaintStep), plus requiring the endpoint to satisfy localFlowExit, which is true in particular if the step at either end is a read or store step.

    In the simple cases there is only one maximal subsequence to find, straight from the source to the sink. However when derefs get involved we find extra paths because we have general taint rules x -taint-> *x and *x -taint-> x, which of course introduces a trivial loop and a new candidate for localFlowBigStep, because the end of a loop constitutes a maximal subsequence and a deref operation can be either a read or a store step. We want to retain both these taint rules, but ideally we would refrain from showing a confusing explanation like this -- I've asked the core team whether that's likely to be possible in the future.

    Some but not all of these taint-flow loops are attributable to Go's imprecise model of dataflow out of an update operation -- in particular its PostUpdateNodes, which represent a particular expression after a side-effect has taken place -- are not as precisely connected to only control-flow-successive reads as they are in other languages.

    To summarise, all the problems observed here are issues with how dataflow is displayed rather than the actual results being incorrect. The possible improvements that might make this better are:

    1. Improving the fidelity with which our Go analysis connects PostUpdateNodes to future reads of the same expression. Possible, and we may well do it, but it's quite a big project.
    2. Improving our display heuristics to avoid showing paths like this even when the language's taint rules say they might exist.
    3. Exposing more information to at least explain to the user why the path was shown.
  7. gagliardetto commented on Mar 25, 2021

    @gagliardetto
    ContributorAuthor

    @smowton Thanks for the in-depth response!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions