flaw_detector 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,179 @@
1
+ module FlawDetector
2
+ module Detector
3
+ class NilFalsePathFlow
4
+ NIL_CHECK_METHODS = [:nil?]
5
+ VAR_BRANCH_KLASS = {:klass => [NilClass, FalseClass], :message => {:branchif => :last, :branchunless => :first}}
6
+ # if methods return true, RCEV type is decided
7
+ RCEV_KLASS_OF_METHODS = {
8
+ :nil? =>
9
+ {:klass => [NilClass],
10
+ :message => {:branchif => :first, :branchunless => :last}}
11
+ }
12
+ REVERSE_EDGE = {:first => :last, :last => :first}
13
+ NIL_METHODS = nil.methods
14
+ FALSE_CLASS_METHODS = FalseClass.methods
15
+ CALC_CODE = {:opt_plus => :+, :opt_minus => :-, :opt_mult => :*, :opt_div => :/, :opt_mod => :%,
16
+ :opt_eq => :==, :opt_neq => :!=, :opt_lt => :<, :opt_le => :<=, :opt_gt => :>, :opt_ge => :>=,
17
+ :opt_ltlt => :<<}
18
+ def initialize
19
+
20
+ end
21
+
22
+ # == Arguments & Return
23
+ # _dom_ ::
24
+ # *Return* :: [Array] array of hash. each hash key must be [:msgid,:file,:line,:short_desc,:long_desc,:details]
25
+ def analyze(dom)
26
+ result = []
27
+
28
+ search_frames = dom.select_methods + [dom.top] + dom.select_classes
29
+ search_frames.each do |frame|
30
+ klass_specified_variable_list = []
31
+ # *search
32
+ frame.insns.each_with_index do |insn, index|
33
+ next unless [:getlocal, :getdynamic].include?(insn[0])
34
+ varframe, varname = frame.insn2variable(insn)
35
+ obj_index,argnum = frame.find_use_insn_index_of_ret(index, insn)
36
+ unless obj_index
37
+ # @todo support using variable at multiple branchif ;ex) case-when syntax
38
+ next
39
+ end
40
+ obj_insn = frame.raw_block_data[:body][:insns][obj_index]
41
+ p_cfg = frame.find_cfg_node_by_insn_index(obj_index)
42
+ klass_specified_edge = nil
43
+ case obj_insn[0]
44
+ when :send
45
+ if argnum == 0 && RCEV_KLASS_OF_METHODS.has_key?(obj_insn[1]) #varnum is the receiver and method is a nil check
46
+ next_obj_index,next_argnum = frame.find_use_insn_index_of_ret(obj_index, obj_insn)
47
+ next_obj_insn = frame.raw_block_data[:body][:insns][next_obj_index]
48
+ case next_obj_insn[0]
49
+ when :branchif, :branchunless
50
+ klass_specified_edge = RCEV_KLASS_OF_METHODS[obj_insn[1]][:message][next_obj_insn[0]]
51
+ klass = RCEV_KLASS_OF_METHODS[obj_insn[1]][:klass]
52
+ end
53
+ end
54
+ when :branchif, :branchunless
55
+ klass_specified_edge = VAR_BRANCH_KLASS[:message][obj_insn[0]]
56
+ klass = VAR_BRANCH_KLASS[:klass]
57
+ end
58
+
59
+ next unless klass_specified_edge
60
+ ks_variable = {:specified_edge => klass_specified_edge, :branch_node => p_cfg, :variable => {:frame => varframe, :name => varname, :klass => klass}, :already_checked => false}
61
+ ks_variable[:complement_edge] = REVERSE_EDGE[ks_variable[:specified_edge]]
62
+ nn = ks_variable[:branch_node].next_nodes
63
+ tmperase = []
64
+ tmperase << :complement_edge if nn.send(ks_variable[:specified_edge]).is_dominance_frontier_of?(nn.send(ks_variable[:complement_edge]))
65
+ tmperase << :specified_edge if nn.send(ks_variable[:complement_edge]).is_dominance_frontier_of?(nn.send(ks_variable[:specified_edge]))
66
+ tmperase.each{|k| ks_variable.delete(k)}
67
+
68
+ klass_specified_variable_list << ks_variable
69
+ end
70
+
71
+ # *detection
72
+ klass_specified_variable_list.each do |ks_variable|
73
+ next if ks_variable[:already_checked]
74
+ if ks_variable[:specified_edge]
75
+ cfg = ks_variable[:branch_node].next_nodes.send(ks_variable[:specified_edge])
76
+ cfg.visit_specified_dominators do |node|
77
+ check_end, ng_list = check_nil_var_ref(node, ks_variable[:variable])
78
+ ng_list.each do |elem|
79
+ line = elem[:frame].index2line(elem[:index])
80
+ result << FlawDetector::make_info(:file => dom.filepath, :line => line, :msgid => "NP_ALWAYS_FALSE", :params => [elem[:variable][:name]])
81
+ end
82
+ if check_end
83
+ next :none
84
+ else
85
+ redundant = find_redundant_node(klass_specified_variable_list, node, ks_variable)
86
+ if redundant
87
+ redundant[:already_checked] = true
88
+ line = redundant[:branch_node].last_line
89
+ result << FlawDetector::make_info(:file => dom.filepath, :line => line, :msgid => "RCN_REDUNDANT_FALSECHECK_OF_FALSE_VALUE", :params => [redundant[:variable][:name], ks_variable[:branch_node].last_line])
90
+ next redundant[:specified_edge]
91
+ else
92
+ next :all
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ if ks_variable[:complement_edge]
99
+ cfg = ks_variable[:branch_node].next_nodes.send(ks_variable[:complement_edge])
100
+ cfg.visit_specified_dominators do |node|
101
+ redundant = find_redundant_node(klass_specified_variable_list, node, ks_variable)
102
+ if redundant
103
+ redundant[:already_checked] = true
104
+ line = redundant[:branch_node].last_line
105
+ result << FlawDetector::make_info(:file => dom.filepath, :line => line, :msgid => "RCN_REDUNDANT_FALSECHECK_OF_TRUE_VALUE", :params => [redundant[:variable][:name], ks_variable[:branch_node].last_line])
106
+ next redundant[:complement_edge]
107
+ else
108
+ next :all
109
+ end
110
+ end
111
+ end
112
+ # [p_cfg's dominators - s_cfg's dominators] check send method except nil methods
113
+ #TODO: add warning list if get localvar and nil checked. # deadcode
114
+ #TODO: implements
115
+ end
116
+ end
117
+
118
+ #* remove duplicate result
119
+ #NOTE: it be caused that input code to analyzer has deadcode.
120
+ return result
121
+ end
122
+
123
+ # _variable_ :: nil variable on node
124
+ # return: check_end?, detected ngs
125
+ def check_nil_var_ref(node, variable)
126
+ ng_list = [] # each element has keys: {:variable, :index, :frame}
127
+ unless node.prev_nodes.first == node.prev_nodes.last
128
+ # check setlocal opcode at dominance frontier
129
+ pfpl = node.prev_nodes.first.dominators - node.prev_nodes.last.dominators
130
+ plpf = node.prev_nodes.last.dominators - node.prev_nodes.first.dominators
131
+ #TODO: return if pfpl/plpf have set localvar
132
+ end
133
+
134
+ frame = node.insns_frame
135
+
136
+ node.bb[0].each do |index|
137
+ insn = frame.insns[index]
138
+ case insn[0]
139
+ when :setlocal, :setdynamic
140
+ varframe,varname = frame.insn2variable(insn)
141
+ # return if insn of index set localvar
142
+ return true, ng_list if variable[:name] == varname && variable[:frame] == varframe
143
+ when :getlocal, :getdynamic
144
+ varframe,varname = frame.insn2variable(insn)
145
+ if variable[:name] == varname && variable[:frame] == varframe
146
+ use_index, use_argnum = frame.find_use_insn_index_of_ret(index, insn)
147
+ use_insn = frame.insns[use_index]
148
+ case use_insn[0]
149
+ when :send
150
+ use_insn[1]
151
+ if use_argnum == 0 && !NIL_METHODS.include?(use_insn[1])
152
+ ng_list << {:variable => variable, :index => use_index, :frame => frame}
153
+ end
154
+ when *(CALC_CODE.keys)
155
+ # ASSERT: nil not operand method
156
+ unless NIL_METHODS.include?(CALC_CODE[use_insn[0]])
157
+ ng_list << {:variable => variable, :index => use_index, :frame => frame}
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ return false, ng_list
164
+ end
165
+
166
+ def find_redundant_node(klass_specified_variable_list, node, ks_variable)
167
+ redundant = klass_specified_variable_list.find do |other_info| # @todo sort by tree parent first
168
+ next if other_info[:branch_node] == ks_variable[:branch_node]
169
+ next unless other_info[:variable][:name] == ks_variable[:variable][:name] &&
170
+ other_info[:variable][:frame] == ks_variable[:variable][:frame]
171
+ next unless other_info[:branch_node] == node
172
+ next unless (ks_variable[:variable][:klass] - other_info[:variable][:klass]).empty?
173
+ true
174
+ end
175
+ return redundant
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,24 @@
1
+ require 'csv'
2
+
3
+ module FlawDetector
4
+ module Formatter
5
+ class CsvFormatter
6
+ def initialize(io = STDOUT)
7
+ @io = io
8
+ end
9
+
10
+ # == Arguments & Return
11
+ # _result_ :: [Array] array of hash. each hash key must be [:msgid,:file,:line,:summary,:description]
12
+ # *Return* :: [String]
13
+ def render(result)
14
+ headers = [:msgid,:file,:line,:short_desc,:long_desc,:details]
15
+ data = CSV.generate("", :row_sep => "\r\n", :headers => headers, :write_headers => true) do |csv|
16
+ result.each do |row|
17
+ csv << row.values_at(*headers)
18
+ end
19
+ end
20
+ @io << data
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module FlawDetector
2
+ MESSAGE_IDS = {
3
+ "RCN_REDUNDANT_FALSECHECK_WOULD_HAVE_BEEN_A_NPE" => { # @todo rewrite it to meaningful message
4
+ :short_desc => "Falsecheck of value previously dereferenced",
5
+ :long_desc => "Falsecheck of %{0} at %{1} of value previously dereferenced in %{2}",
6
+ :details => "A value is checked here to see whether it is false, but this value can't be false because it was previously dereferenced and if it were false a false pointer exception would have occurred at the earlier dereference. Essentially, this code and the previous dereference disagree as to whether this value is allowed to be false. Either the check is redundant or the previous dereference is erroneous."
7
+ },
8
+ "RCN_REDUNDANT_FALSECHECK_OF_FALSE_VALUE" => {
9
+ :short_desc => "Redundant falsecheck of value known to be false",
10
+ :long_desc => "Redundant falsecheck of %{0} which is known to be false in LINE:%{1}",
11
+ :details => "This method contains a redundant check of a known false value against the constant false."
12
+ },
13
+ "RCN_REDUNDANT_FALSECHECK_OF_TRUE_VALUE" => {
14
+ :short_desc => "Redundant falsecheck of value known to be false",
15
+ :long_desc => "Redundant falsecheck of %{0} which is known to be false in LINE:%{1}",
16
+ :details => "This method contains a redundant check of a known false value against the constant false."
17
+ },
18
+ "NP_ALWAYS_FALSE" => {
19
+ :short_desc => "False value missing method received",
20
+ :long_desc => "False value missing method received in %{0}",
21
+ :details => "A false value, which is NilClass or FalseClass, is received missing method here. This will lead to a NoMethodError when the code is executed."
22
+ },
23
+ "NP_FALSE_ON_SOME_PATH" =>{ # @todo rewrite it to meaningful message
24
+ :short_desc => "Possible false pointer dereference",
25
+ :long_desc => "Possible false pointer dereference in %{0}",
26
+ :details => "There is a branch of statement that, if executed, guarantees that a false value will be dereferenced, which would generate a RuntimeError when the code is executed. Of course, the problem might be that the branch or statement is infeasible and that the false pointer exception can't ever be executed; deciding that is beyond the ability of FlawDetector."
27
+ }
28
+ }
29
+
30
+ def make_info(params={})
31
+ info = {}
32
+ info[:msgid] = params[:msgid]
33
+ info[:file] = params[:file]
34
+ info[:line] = params[:line]
35
+ hash = {}
36
+ params[:params].each_with_index do |elem, index|
37
+ hash["%{#{index}}"] = elem
38
+ end
39
+ pattern = Regexp.new(hash.keys.map{|n| Regexp.escape(n)}.join("|"))
40
+ info[:short_desc] = MESSAGE_IDS[info[:msgid]][:short_desc].gsub(pattern, hash)
41
+ info[:long_desc] = MESSAGE_IDS[info[:msgid]][:long_desc].gsub(pattern, hash)
42
+ info[:details] = MESSAGE_IDS[info[:msgid]][:details].gsub(pattern, hash)
43
+ return info
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module FlawDetector
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,169 @@
1
+ require "flaw_detector/version"
2
+ require "flaw_detector/controller"
3
+ require "flaw_detector/message"
4
+ require "flaw_detector/formatter/csv_formatter"
5
+ require "flaw_detector/detector/nil_false_path_flow"
6
+ require "flaw_detector/code_model/code_document"
7
+ require "flaw_detector/code_model/insns_frame"
8
+ require "flaw_detector/code_model/cfg_node"
9
+ require File.expand_path("../../ext/insns_ext/insn_ext.rb", __FILE__)
10
+
11
+ module FlawDetector
12
+ OPERAND_0 = 1
13
+ OPERAND_1 = 2
14
+ OPERAND_2 = 3
15
+ OPERAND_3 = 4
16
+
17
+ module ISeqHeader
18
+ TYPE_TOP = :top
19
+ TYPE_CLASS = :class
20
+ TYPE_METHOD = :method
21
+ TYPE_BLOCK = :block
22
+ end
23
+
24
+ include CodeModel
25
+
26
+ # (key,val) = (opcode, operand number from 0)
27
+ INSNS_HAVE_ISEQ_IN_OPERAND = {"putiseq" => OPERAND_0, "defineclass" => OPERAND_1, "send" => OPERAND_2, "invokesuper" => OPERAND_1}
28
+ # ASSERT: operand number 0 is offset
29
+ INSNS_OF_BRANCH = ["branchif", "branchunless"]
30
+
31
+ def parse(code_str, filepath="<compiled>")
32
+ isqns = RubyVM::InstructionSequence.new(code_str)
33
+ data = iseq_parse(isqns.to_a)
34
+ CodeDocument.create(data, filepath)
35
+ end
36
+
37
+ def parse_file(file)
38
+ case file
39
+ when String
40
+ code_str = File.open(file, "r"){|f| f.read}
41
+ filepath = file
42
+ when File
43
+ code_str = file.read
44
+ filepath = file.path
45
+ when IO
46
+ code_str = file.read
47
+ filepath = "<compiled>"
48
+ else
49
+ raise "not support class#{file.class}"
50
+ end
51
+ parse(code_str, filepath)
52
+ end
53
+
54
+ def iseq_parse(iseq)
55
+ return nil unless iseq
56
+
57
+ header = {}
58
+ header[:magic] = iseq[0]
59
+ header[:major] = iseq[1]
60
+ header[:minor] = iseq[2]
61
+ header[:format_type] = iseq[3]
62
+ header[:misc] = iseq[4]
63
+ header[:name] = iseq[5]
64
+ header[:filename] = iseq[6]
65
+ header[:filepath] = iseq[7]
66
+ header[:line_no] = iseq[8]
67
+ header[:type] = iseq[9]
68
+ header[:locals] = iseq[10]
69
+ header[:args] = iseq[11]
70
+ header[:exceptions] = iseq[12]
71
+
72
+ insns = []
73
+ insns_pos_to_lineno = []
74
+ label_pos = {}
75
+ lineno_and_insns_pos = [nil,nil]
76
+ insns_pos_to_operand_iseq = {}
77
+ branch_info = [] # element = {:pos =>, :label =>}
78
+ basic_block = [] # element type is (Range, Array). It is guaranteed that each element Range doesn't overlap
79
+ tmp_bb_start_pos = 0
80
+
81
+ iseq[13].each do |mixed|
82
+ case mixed
83
+ when Array # instruction
84
+ insns << mixed
85
+ opcode = mixed.first.to_s
86
+ if INSNS_HAVE_ISEQ_IN_OPERAND.keys.include?(opcode)
87
+ operand_num = INSNS_HAVE_ISEQ_IN_OPERAND[opcode]
88
+ iseq_in_operand = iseq_parse(mixed[operand_num])
89
+ insn_pos = insns.count-1
90
+ insns_pos_to_operand_iseq[insn_pos] = iseq_in_operand
91
+
92
+ #replace to link
93
+ mixed[operand_num] = insn_pos
94
+
95
+ if insns_pos_to_operand_iseq[insn_pos] &&
96
+ insns_pos_to_operand_iseq[insn_pos][:header][:type] == ISeqHeader::TYPE_BLOCK
97
+ # NOTE: bb is changed becase iseq of block type may have break, raise, or etc...
98
+ if tmp_bb_start_pos < insn_pos
99
+ basic_block << [tmp_bb_start_pos...insn_pos, insn_pos]
100
+ end
101
+ basic_block << [insn_pos...(insn_pos+1), insn_pos+1, :exception]
102
+ tmp_bb_start_pos = insns.count
103
+ end
104
+ elsif INSNS_OF_BRANCH.include?(opcode)
105
+ basic_block << [tmp_bb_start_pos...insns.count, mixed[OPERAND_0], insns.count]
106
+ tmp_bb_start_pos = insns.count
107
+ elsif opcode == "jump"
108
+ basic_block << [tmp_bb_start_pos...insns.count, mixed[OPERAND_0]]
109
+ tmp_bb_start_pos = insns.count
110
+ elsif opcode == "leave"
111
+ basic_block << [tmp_bb_start_pos...insns.count, :leave]
112
+ tmp_bb_start_pos = insns.count
113
+ end
114
+ when Fixnum # lineno
115
+ if lineno_and_insns_pos[0]
116
+ range = lineno_and_insns_pos[1]...(insns.count)
117
+ insns_pos_to_lineno << [range, lineno_and_insns_pos[0]]
118
+ end
119
+ lineno_and_insns_pos[0] = mixed
120
+ lineno_and_insns_pos[1] = insns.count
121
+ when Symbol # label
122
+ label_pos[mixed] = insns.count
123
+ basic_block << [tmp_bb_start_pos...insns.count, insns.count]
124
+ tmp_bb_start_pos = insns.count
125
+ end
126
+ end
127
+ if lineno_and_insns_pos[0]
128
+ if insns_pos_to_lineno.empty? || !insns_pos_to_lineno.last.include?(insns.count-1)
129
+ range = lineno_and_insns_pos[1]...(insns.count)
130
+ insns_pos_to_lineno << [range, lineno_and_insns_pos[0]]
131
+ end
132
+ end
133
+ # remove redundant bb
134
+ basic_block.reject! {|bb| bb[0].to_a.size == 0}
135
+ basic_block.each do |bb|
136
+ bb.map! do |elem|
137
+ if elem.to_s =~ /label_.+/
138
+ label_pos[elem]
139
+ else
140
+ elem
141
+ end
142
+ end
143
+ end
144
+ #TODO: add remove redundant bb code
145
+
146
+ #set name
147
+ cnt = 0
148
+ bs = {}
149
+ basic_block.each do |bb|
150
+ sym = "bb_#{bs.count}".to_sym
151
+ bs[sym] = bb
152
+ end
153
+ bs[:entry] = [0...0,0]
154
+ bs[:leave] = [0...0]
155
+
156
+ body = {}
157
+ body[:insns] = insns
158
+
159
+ extra = {}
160
+ extra[:insns_pos_to_lineno] = insns_pos_to_lineno
161
+ extra[:label_pos] = label_pos
162
+ extra[:insns_pos_to_operand_iseq] = insns_pos_to_operand_iseq
163
+ extra[:basic_blocks] = bs
164
+
165
+ return {:header => header, :body => body, :extra => extra}
166
+ end
167
+
168
+ module_function :iseq_parse, :parse, :make_info, :parse_file
169
+ end
@@ -0,0 +1,9 @@
1
+ def flaw(a)
2
+ if a
3
+ rl = a + 1
4
+ elsif a #**RCN_REDUNDANT_FALSECHECK_OF_FALSE_VALUE,a,2**
5
+ rl = 1 + a
6
+ else
7
+ rl = a + 1 #**NP_ALWAYS_FALSE,a**
8
+ end
9
+ end