Skip to content

Commit 6a7d544

Browse files
committed
Merge branch 'conditionally-noreturn' into 'main'
Fix a bug that could cause false-negative fallthrough CFG edges Closes #634 See merge request rewriting/ddisasm!1255
2 parents 25853ad + 699d185 commit 6a7d544

12 files changed

Lines changed: 380 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Fix several issues that could result in missing symbolic expressions
55
- Improve resolution of TLS-related symbolic expression
66
* Fix bug that could lead to functional errors due to false-positive symbolic operands or data.
7+
* Fix bug that could drop fall-through edges from calls to conditionally no-return functions
78

89
# 1.9.2
910

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.PHONY: all clean check
2+
all: out.txt
3+
4+
out.txt: ex
5+
@./$^ > $@
6+
ex: ex_original.s
7+
gcc $^ -no-pie -o $@
8+
9+
clean:
10+
rm -f ex out.txt
11+
rm -fr ex.unstripped ex.s *.old* dl_files *.gtirb
12+
check:
13+
./ex > /tmp/res.txt
14+
@ diff out.txt /tmp/res.txt && echo TEST OK
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
2+
.intel_syntax noprefix
3+
.extern printf
4+
.extern exit
5+
6+
.section .rodata
7+
msg_main1:
8+
.string "main: calling conditional_sinkcall(5)\n"
9+
msg_main2:
10+
.string "main: calling conditional_sinkcall(2)\n"
11+
msg_main3:
12+
.string "main: calling conditional_sinkcall(0) -- should not return\n"
13+
msg_after:
14+
.string "main: AFTER conditional_sinkcall(0) (unreachable)\n"
15+
16+
msg_conditional_sinkcall_enter:
17+
.string "conditional_sinkcall: entered with x=%d\n"
18+
msg_conditional_sinkcall_ret:
19+
.string "conditional_sinkcall: returning normally\n"
20+
msg_fatal:
21+
.string "fatal_error: exiting program\n"
22+
23+
.section .text
24+
25+
# ---------------------------------------
26+
# void fatal_error()
27+
# prints message then exits (noreturn)
28+
# ---------------------------------------
29+
.type fatal_error,@function
30+
fatal_error:
31+
lea rdi, msg_fatal[rip]
32+
xor eax, eax
33+
call printf
34+
35+
xor edi, edi
36+
call exit
37+
38+
# unreachable safeguard
39+
hlt
40+
41+
42+
# ---------------------------------------
43+
# void conditional_sinkcall(int x)
44+
# argument in edi
45+
# ---------------------------------------
46+
.type conditional_sinkcall,@function
47+
conditional_sinkcall:
48+
push rbp
49+
mov rbp, rsp
50+
sub rsp, 16
51+
52+
mov [rbp-4], edi # save argument x
53+
54+
# log entry
55+
mov esi, edi
56+
lea rdi, msg_conditional_sinkcall_enter[rip]
57+
xor eax, eax
58+
call printf
59+
60+
mov edi, [rbp-4] # restore x
61+
test edi, edi
62+
je .error_path
63+
64+
lea rdi, msg_conditional_sinkcall_ret[rip]
65+
xor eax, eax
66+
call printf
67+
68+
leave
69+
ret
70+
71+
.error_path:
72+
leave
73+
call fatal_error # no return
74+
hlt
75+
76+
77+
# ---------------------------------------
78+
# main()
79+
# ---------------------------------------
80+
.global main
81+
.type main,@function
82+
main:
83+
push rbp
84+
mov rbp, rsp
85+
sub rsp, 16
86+
87+
lea rdi, msg_main1[rip]
88+
xor eax, eax
89+
call printf
90+
91+
call_1:
92+
mov edi, 5
93+
call conditional_sinkcall
94+
95+
lea rdi, msg_main2[rip]
96+
xor eax, eax
97+
call printf
98+
99+
call_2:
100+
mov edi, 2
101+
call conditional_sinkcall
102+
103+
nop
104+
nop
105+
nop
106+
nop
107+
108+
lea rdi, msg_main3[rip]
109+
xor eax, eax
110+
call printf
111+
112+
mov edi, 0
113+
# 0: sink call: does not return
114+
call conditional_sinkcall
115+
116+
nop
117+
nop
118+
nop
119+
120+
.type fall_through_func,@function
121+
fall_through_func:
122+
# unreachable if analysis works
123+
lea rdi, msg_after[rip]
124+
xor eax, eax
125+
call printf
126+
127+
xor edi, edi
128+
call exit

src/datalog/cfg.dl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ resolved_transfer_to_symbol(EA,Function, "branch"):-
237237
cfg_edge(Src,Dest,Conditional,"false","fallthrough"):-
238238
refined_block_control_instruction(Src,EA),
239239
may_fallthrough(EA,Dest),
240-
!no_return_call_propagated(EA),
240+
!no_return_call_propagated(EA,_),
241241
!nop_block(Src),
242242
code_in_refined_block(Dest,Dest),
243243
// Do not cross a section boundary

src/datalog/code_inference.dl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,26 @@ likely_fallthrough(From,To):-
179179
conditional_jump(From)
180180
).
181181

182+
/**
183+
A block `Block` falls through a NOP/padding sled to the next real block `NextBlock`.
184+
`From` is the source instruction of the edge leading to `NextBlock`.
185+
*/
186+
.decl fallthrough_over_padding(Block:address, From:address, NextBlock:address)
187+
188+
fallthrough_over_padding(Block, From, NextBlock):-
189+
block_last_instruction(Block, BlockLast),
190+
may_fallthrough(BlockLast, NextBlock),
191+
!candidate_block_is_padding(NextBlock),
192+
From = BlockLast.
193+
194+
fallthrough_over_padding(Block, From, NextBlock):-
195+
block_last_instruction(Block, Inst),
196+
may_fallthrough(Inst, PadBlock),
197+
candidate_block_is_padding(PadBlock),
198+
block_last_instruction(PadBlock, PadLast),
199+
may_fallthrough(PadLast, NextBlock),
200+
From = PadLast.
201+
182202
//////////////////////////////////////////////////////////////
183203
// This is a small refinement for discarding immediates as targets
184204
// in some obvious cases. This is specially useful for PIE code where

src/datalog/code_inference_postprocess.dl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,22 @@ padding_prefix_end(EA,Block):-
226226
!arch.is_nop(EA),
227227
next(PrevEA,EA),
228228
padding_prefix(PrevEA,Block).
229+
230+
/**
231+
A refined block `Block` contains at least one `return` instruction.
232+
*/
233+
.decl refined_block_contains_return(Block:address)
234+
235+
refined_block_contains_return(Block):-
236+
refined_block(Block),
237+
code_in_refined_block(EA,Block),
238+
arch.return(EA).
239+
240+
/**
241+
A function `Func` contains at least one `return` instruction.
242+
*/
243+
.decl refined_block_contains_return_in_func(Func:address)
244+
245+
refined_block_contains_return_in_func(Func) :-
246+
function_inference.in_function(Block, Func),
247+
refined_block_contains_return(Block).

src/datalog/noreturn.dl

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ segment_target_range(Beg,End,MaxTgt):-
9090
// propagate if it is a jump to a no-return proc.
9191
direct_or_pcrel_jump(BlockEnd,InterTarget),
9292
inter_procedural_edge(BlockEnd,_),
93-
no_return_block(InterTarget),
93+
no_return_block(InterTarget,_),
9494
MaxTgt = PrevMaxTgt
9595
),
9696
MaxTgt >= Beg.
@@ -112,43 +112,64 @@ that are not known to be no-return.
112112
The only situation where it would generate false-positive noreturns is if a
113113
"known noreturn" library function does, in fact, return.
114114
*/
115-
.decl no_return_block(EA:address)
115+
.decl no_return_block(EA:address,Reason:symbol)
116116

117117
// If we have a self-contained segment and the
118118
// last block does not fallthrough, all the blocks
119119
// in the segment cannot return
120120
// If the fallthrough in the last block turns
121121
// out to be a no-return call, the same applies
122-
no_return_block(Block):-
122+
no_return_block(Block,"no-fallthrough"):-
123123
self_contained_segment(Beg,End),
124124
block_boundaries(LastBlock,_,End),
125125
block_last_instruction(LastBlock,BlockEnd),
126126
(
127127
!may_fallthrough(BlockEnd,_);
128-
no_return_call_propagated(BlockEnd)
128+
no_return_call_propagated(BlockEnd,_)
129129
),
130130
block_boundaries(Block,Beg,EndBlock),
131131
EndBlock <= End.
132132

133-
// A function is called, and the call falls through interprocedurally.
134-
// The function is likely noreturn.
135-
// The edge after this call site should already be eliminated because it's
136-
// interprocedural, but this rule ensures other calls to the same function
137-
// don't introduce problematic edges.
138-
no_return_block(Func):-
133+
/**
134+
A function `Func` is called at instruction `Call` and the call may fall through
135+
to the next instruction interprocedurally.
136+
NOP or padding instructions may follow the call.
137+
*/
138+
.decl call_may_fallthrough_inter(Call:address,Func:address)
139+
140+
call_may_fallthrough_inter(Call,Func):-
139141
direct_call(Call,Func),
140-
may_fallthrough(Call,Fallthrough),
141-
(
142-
!candidate_block_is_padding(Fallthrough),
143-
Next = Fallthrough,
144-
From = Call
145-
;
146-
candidate_block_is_padding(Fallthrough),
147-
block_last_instruction(Fallthrough,From),
148-
may_fallthrough(From,Next)
149-
),
142+
block_last_instruction(CallBlock,Call),
143+
fallthrough_over_padding(CallBlock,From,Next),
150144
inter_procedural_edge(From,Next).
151145

146+
/**
147+
Represents the next initial function following `Func`
148+
(from `function_entry_initial`, before use-def and value analysis).
149+
`NextFunc` is the immediate successor of `Func`.
150+
*/
151+
.decl next_function_entry_initial(Func:address,NextFunc:address)
152+
153+
next_function_entry_initial(Func, NextFunc) :-
154+
function_inference.function_entry_initial(Func),
155+
NextFunc = min F : {
156+
function_inference.function_entry_initial(F),
157+
F > Func
158+
}.
159+
160+
/**
161+
An initial function (`function_entry_initial`, prior to use-def
162+
and value analysis) contains one or more `return` instructions.
163+
This is a heuristic and may generate false positives.
164+
*/
165+
.decl initial_function_containing_return(Func:address,ReturnEA:address)
166+
167+
initial_function_containing_return(Func,EA):-
168+
next_function_entry_initial(Func, NextFunc),
169+
EA >= Func,
170+
EA < NextFunc,
171+
arch.return(EA).
172+
152173
/**
153174
Calls to known no return functions or their PLT blocks.
154175
*/
@@ -164,19 +185,77 @@ no_return_call_refined(EA):-
164185
no_return_function(Pattern),
165186
match(Pattern,Function).
166187

167-
no_return_block(Block):-
188+
no_return_block(Block,"no_return_call_refined"):-
168189
no_return_call_refined(BlockEnd),
169190
block_last_instruction(Block,BlockEnd).
170191

192+
/**
193+
A call `Call` targets a function `CallTarget` that has another call-site
194+
which falls through interprocedurally.
195+
*/
196+
.decl call_target_has_other_fallthrough_inter(Call:address,CallTarget:address)
197+
198+
call_target_has_other_fallthrough_inter(Call,CallTarget):-
199+
call_may_fallthrough_inter(OtherCall,CallTarget),
200+
direct_call(Call,CallTarget),
201+
OtherCall != Call.
202+
171203
/**
172204
Calls to noreturn blocks.
173205
*/
174-
.decl no_return_call_propagated(EA:address)
206+
.decl no_return_call_propagated(EA:address,Reason:symbol)
175207

176-
no_return_call_propagated(EA):-
208+
no_return_call_propagated(EA,"no_return_call_refined"):-
177209
no_return_call_refined(EA).
178210

179-
no_return_call_propagated(EA):-
211+
no_return_call_propagated(EA,"direct_call to no_return_block"):-
180212
direct_call(EA,Block),
181-
no_return_block(Block),
213+
no_return_block(Block,_),
182214
!pc_load_call(EA,Block).
215+
216+
// A function is called, and the call falls through interprocedurally.
217+
// The function is likely noreturn.
218+
// The edge after this call site should already be eliminated because
219+
// it's interprocedural.
220+
no_return_call_propagated(Call,"interprocedural fallthrough"):-
221+
direct_call(Call,CallTarget),
222+
call_may_fallthrough_inter(Call,CallTarget),
223+
!no_return_call_refined(Call),
224+
!pc_load_call(Call,CallTarget).
225+
226+
// Any function target that has call-site falling through interprocedurally
227+
// is likely noreturn. This rule ensures other calls to the same function
228+
// don't introduce problematic edges.
229+
// However, a function can be conditionally noreturn (i.e., it may return along
230+
// some paths but not others).
231+
// To avoid incorrectly removing legitimate fallthrough CFG edges, this rule
232+
// conservatively checks whether `Next` is a `possible_target`, and if not,
233+
// we do not classify the function call as noreturn function call.
234+
no_return_call_propagated(
235+
Call,
236+
"call possiblly_no_return_func and fallthrough-next is a possible target"
237+
):-
238+
call_target_has_other_fallthrough_inter(Call,CallTarget),
239+
!call_may_fallthrough_inter(Call,CallTarget),
240+
!pc_load_call(Call,CallTarget),
241+
!no_return_call_refined(Call),
242+
block_last_instruction(CallBlock,Call),
243+
fallthrough_over_padding(CallBlock,_,Next),
244+
(
245+
// No return instruction appears in the target function (internal).
246+
!initial_function_containing_return(CallTarget,_), UNUSED(Next),
247+
!plt_block(CallTarget,_)
248+
;
249+
// If the target is external, there is not enough information to
250+
// determine whether it is conditionally non-returning.
251+
// As a heuristic, classify it as a no-return call only when
252+
// Next is a possible target.
253+
plt_block(CallTarget,_),
254+
possible_target(Next)
255+
;
256+
// The call target function may contain a return, which may
257+
// indicate that it is a conditionally non-returning function.
258+
// Further check whether Next is a possible target.
259+
initial_function_containing_return(CallTarget,_),
260+
possible_target(Next)
261+
).

0 commit comments

Comments
 (0)