Skip to content

Commit 16b0d0b

Browse files
authored
Add a memory checker (#420)
Detects leaks, double-frees, and use-after-frees of manually memory managed LLVM objects
1 parent 7284514 commit 16b0d0b

37 files changed

+286
-141
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ jobs:
8888
if: runner.os == 'Windows'
8989

9090
- name: Run tests
91-
run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=`-g2`)'
91+
run: |
92+
julia -e 'open("LocalPreferences.toml", "a") do io
93+
println(io, "typecheck = \"true\"")
94+
println(io, "memcheck = \"true\"")
95+
end'
96+
julia --project -e 'using Pkg; Pkg.test(; coverage=true)'
9297
env:
9398
JULIA_LLVM_ARGS: ${{ matrix.llvm_args }}
9499
- uses: julia-actions/julia-processcoverage@v1
@@ -151,7 +156,12 @@ jobs:
151156
run: julia --project=deps deps/build_ci.jl
152157

153158
- name: Run tests
154-
run: julia --project -e 'using Pkg; Pkg.test(; coverage=true, julia_args=`-g2`)'
159+
run: |
160+
julia -e 'open("LocalPreferences.toml", "a") do io
161+
println(io, "typecheck = \"true\"")
162+
println(io, "memcheck = \"true\"")
163+
end'
164+
julia --project -e 'using Pkg; Pkg.test(; coverage=true)'
155165
env:
156166
JULIA_LLVM_ARGS: ${{ matrix.llvm_args }}
157167
- uses: julia-actions/julia-processcoverage@v1

LocalPreferences.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@
33
# but if you are using a custom version of LLVM you will need to provide your own,
44
# e.g., by running `deps/build_local.jl`.
55
#libLLVMExtra = "/path/to/libLLVMExtra.so"
6+
7+
# whether to enable additional object type checking
8+
#typecheck = "false"
9+
10+
# whether to enable object memory checking. these are expensive checks that keep
11+
# track of allocated objects, whether they are freed correctly, etc.
12+
#memcheck = "false"

examples/constrained.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ meta(::Type{FPExceptStrict}) = "fpexcept.strict"
4444
intrinsic = Intrinsic("llvm.experimental.constrained.$(func(F))")
4545
intrinsic_fun = LLVM.Function(mod, intrinsic, [typ])
4646
ftype = LLVM.FunctionType(intrinsic,[typ])
47+
4748
# generate IR
4849
@dispose builder=IRBuilder() begin
4950
entry = BasicBlock(llvm_f, "entry")

src/LLVM.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module LLVM
22

3+
using Preferences
34
using Unicode
45
using Printf
56
using Libdl
@@ -59,6 +60,9 @@ has_oldpm() = LLVM.version() < v"17"
5960
has_newpm() = LLVM.version() >= v"15"
6061
has_julia_ojit() = VERSION >= v"1.10.0-DEV.1395"
6162

63+
# helpers
64+
include("debug.jl")
65+
6266
# LLVM API wrappers
6367
include("support.jl")
6468
if LLVM.version() < v"17"
@@ -133,6 +137,7 @@ function __init__()
133137

134138
_install_handlers()
135139
_install_handlers(GlobalContext())
140+
atexit(report_leaks)
136141
end
137142

138143
end

src/analysis.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ export DomTree, dominates
2929
ref::API.LLVMDominatorTreeRef
3030
end
3131

32-
Base.unsafe_convert(::Type{API.LLVMDominatorTreeRef}, domtree::DomTree) = domtree.ref
32+
Base.unsafe_convert(::Type{API.LLVMDominatorTreeRef}, domtree::DomTree) =
33+
mark_use(domtree).ref
3334

34-
DomTree(f::Function) = DomTree(API.LLVMCreateDominatorTree(f))
35-
dispose(domtree::DomTree) = API.LLVMDisposeDominatorTree(domtree)
35+
DomTree(f::Function) = mark_alloc(DomTree(API.LLVMCreateDominatorTree(f)))
36+
dispose(domtree::DomTree) = mark_dispose(API.LLVMDisposeDominatorTree, domtree)
3637

3738
function dominates(domtree::DomTree, A::Instruction, B::Instruction)
3839
API.LLVMDominatorTreeInstructionDominates(domtree, A, B) |> Bool
@@ -48,10 +49,11 @@ export PostDomTree, dominates
4849
end
4950

5051
Base.unsafe_convert(::Type{API.LLVMPostDominatorTreeRef}, postdomtree::PostDomTree) =
51-
postdomtree.ref
52+
mark_use(postdomtree).ref
5253

53-
PostDomTree(f::Function) = PostDomTree(API.LLVMCreatePostDominatorTree(f))
54-
dispose(postdomtree::PostDomTree) = API.LLVMDisposePostDominatorTree(postdomtree)
54+
PostDomTree(f::Function) = mark_alloc(PostDomTree(API.LLVMCreatePostDominatorTree(f)))
55+
dispose(postdomtree::PostDomTree) =
56+
mark_dispose(API.LLVMDisposePostDominatorTree, postdomtree)
5557

5658
function dominates(postdomtree::PostDomTree, A::Instruction, B::Instruction)
5759
API.LLVMPostDominatorTreeInstructionDominates(postdomtree, A, B) |> Bool

src/bitcode.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ function Base.parse(::Type{Module}, membuf::MemoryBuffer)
99
Module(out_ref[])
1010
end
1111

12-
Base.parse(::Type{Module}, data::Vector) = parse(Module, MemoryBuffer(data, "", false))
12+
function Base.parse(::Type{Module}, data::Vector)
13+
@dispose membuf = MemoryBuffer(data, "", false) begin
14+
parse(Module, membuf)
15+
end
16+
end
1317

1418

1519
## writer

src/buffer.jl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ export MemoryBuffer, MemoryBufferFile, dispose
44
ref::API.LLVMMemoryBufferRef
55
end
66

7-
Base.unsafe_convert(::Type{API.LLVMMemoryBufferRef}, membuf::MemoryBuffer) = membuf.ref
7+
Base.unsafe_convert(::Type{API.LLVMMemoryBufferRef}, membuf::MemoryBuffer) =
8+
mark_use(membuf).ref
89

910
function MemoryBuffer(data::Vector{T}, name::String="", copy::Bool=true) where T<:Union{UInt8,Int8}
1011
ptr = pointer(data)
1112
len = Csize_t(length(data))
12-
if copy
13-
return MemoryBuffer(API.LLVMCreateMemoryBufferWithMemoryRangeCopy(ptr, len, name))
13+
membuf = if copy
14+
MemoryBuffer(API.LLVMCreateMemoryBufferWithMemoryRangeCopy(ptr, len, name))
1415
else
15-
return MemoryBuffer(API.LLVMCreateMemoryBufferWithMemoryRange(ptr, len, name, false))
16+
MemoryBuffer(API.LLVMCreateMemoryBufferWithMemoryRange(ptr, len, name, false))
1617
end
18+
mark_alloc(membuf)
1719
end
1820

1921
function MemoryBuffer(f::Core.Function, args...; kwargs...)
@@ -48,7 +50,7 @@ function MemoryBufferFile(f::Core.Function, args...; kwargs...)
4850
end
4951
end
5052

51-
dispose(membuf::MemoryBuffer) = API.LLVMDisposeMemoryBuffer(membuf)
53+
dispose(membuf::MemoryBuffer) = mark_dispose(API.LLVMDisposeMemoryBuffer, membuf)
5254

5355
Base.length(membuf::MemoryBuffer) = API.LLVMGetBufferSize(membuf)
5456

src/core/context.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ export Context, dispose, GlobalContext
66
ref::API.LLVMContextRef
77
end
88

9-
Base.unsafe_convert(::Type{API.LLVMContextRef}, ctx::Context) = ctx.ref
9+
Base.unsafe_convert(::Type{API.LLVMContextRef}, ctx::Context) = mark_use(ctx).ref
1010

1111
function Context(; opaque_pointers=nothing)
12-
ctx = Context(API.LLVMContextCreate())
12+
ctx = mark_alloc(Context(API.LLVMContextCreate()))
1313
if opaque_pointers !== nothing
1414
opaque_pointers!(ctx, opaque_pointers)
1515
end
@@ -20,7 +20,7 @@ end
2020

2121
function dispose(ctx::Context)
2222
deactivate(ctx)
23-
API.LLVMContextDispose(ctx)
23+
mark_dispose(API.LLVMContextDispose, ctx)
2424
end
2525

2626
function Context(f::Core.Function; kwargs...)

src/core/instructions.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ end
1818

1919
function refcheck(::Type{T}, ref::API.LLVMValueRef) where T<:Instruction
2020
ref==C_NULL && throw(UndefRefError())
21-
if Base.JLOptions().debug_level >= 2
21+
if typecheck_enabled
2222
T′ = identify(Instruction, ref)
2323
if T != T′
2424
error("invalid conversion of $T′ instruction reference to $T")

src/core/metadata.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ end
2121

2222
function refcheck(::Type{T}, ref::API.LLVMMetadataRef) where T<:Metadata
2323
ref==C_NULL && throw(UndefRefError())
24-
if Base.JLOptions().debug_level >= 2
24+
if typecheck_enabled
2525
T′ = identify(Metadata, ref)
2626
if T != T′
2727
error("invalid conversion of $T′ metadata reference to $T")

src/core/module.jl

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@ export dispose, context,
99

1010
# forward definition of Module in src/core/value/constant.jl
1111

12-
function Base.unsafe_convert(::Type{API.LLVMModuleRef}, mod::Module)
13-
# modules can get destroyed, so be sure to check for validity
14-
mod.ref == C_NULL && throw(UndefRefError())
15-
mod.ref
16-
end
12+
Base.unsafe_convert(::Type{API.LLVMModuleRef}, mod::Module) = mark_use(mod).ref
1713

1814
Base.:(==)(x::Module, y::Module) = (x.ref === y.ref)
1915

@@ -23,12 +19,12 @@ Base.:(==)(x::Module, y::Module) = (x.ref === y.ref)
2319
end
2420

2521
Module(name::String) =
26-
Module(API.LLVMModuleCreateWithNameInContext(name, context()))
22+
mark_alloc(Module(API.LLVMModuleCreateWithNameInContext(name, context())))
2723

28-
Module(mod::Module) = Module(API.LLVMCloneModule(mod))
24+
Module(mod::Module) = mark_alloc(Module(API.LLVMCloneModule(mod)))
2925
Base.copy(mod::Module) = Module(mod)
3026

31-
dispose(mod::Module) = API.LLVMDisposeModule(mod)
27+
dispose(mod::Module) = mark_dispose(API.LLVMDisposeModule, mod)
3228

3329
function Module(f::Core.Function, args...; kwargs...)
3430
mod = Module(args...; kwargs...)

src/core/type.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ end
2323

2424
function refcheck(::Type{T}, ref::API.LLVMTypeRef) where T<:LLVMType
2525
ref==C_NULL && throw(UndefRefError())
26-
if Base.JLOptions().debug_level >= 2
26+
if typecheck_enabled
2727
T′ = identify(LLVMType, ref)
2828
if T != T′
2929
error("invalid conversion of $T′ type reference to $T")

src/core/value.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ end
2121

2222
function refcheck(::Type{T}, ref::API.LLVMValueRef) where T<:Value
2323
ref==C_NULL && throw(UndefRefError())
24-
if Base.JLOptions().debug_level >= 2
24+
if typecheck_enabled
2525
T′ = identify(Value, ref)
2626
if T != T′
2727
error("invalid conversion of $T′ value reference to $T")

src/core/value/constant.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ abstract type Constant <: User end
1212
unsafe_destroy!(constant::Constant) = API.LLVMDestroyConstant(constant)
1313

1414
# forward declarations
15-
@checked mutable struct Module
15+
@checked struct Module
1616
ref::API.LLVMModuleRef
1717
end
1818
abstract type Instruction <: User end

src/datalayout.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ export DataLayout, dispose,
88

99
# forward definition of DataLayout in src/module.jl
1010

11-
Base.unsafe_convert(::Type{API.LLVMTargetDataRef}, dl::DataLayout) = dl.ref
11+
Base.unsafe_convert(::Type{API.LLVMTargetDataRef}, dl::DataLayout) = mark_use(dl).ref
1212

13-
DataLayout(rep::String) = DataLayout(API.LLVMCreateTargetData(rep))
13+
DataLayout(rep::String) = mark_alloc(DataLayout(API.LLVMCreateTargetData(rep)))
1414

15-
DataLayout(tm::TargetMachine) = DataLayout(API.LLVMCreateTargetDataLayout(tm))
15+
DataLayout(tm::TargetMachine) = mark_alloc(DataLayout(API.LLVMCreateTargetDataLayout(tm)))
1616

1717
function DataLayout(f::Core.Function, args...; kwargs...)
1818
data = DataLayout(args...; kwargs...)
@@ -23,7 +23,7 @@ function DataLayout(f::Core.Function, args...; kwargs...)
2323
end
2424
end
2525

26-
dispose(data::DataLayout) = API.LLVMDisposeTargetData(data)
26+
dispose(data::DataLayout) = mark_dispose(API.LLVMDisposeTargetData, data)
2727

2828
Base.string(data::DataLayout) =
2929
unsafe_message(API.LLVMCopyStringRepOfTargetData(data))

src/debug.jl

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
## typecheck: ensuring that the types of objects is as expected
2+
3+
const typecheck_enabled = parse(Bool, @load_preference("typecheck", "false"))
4+
5+
6+
## memcheck: keeping track when objects are valid
7+
8+
const memcheck_enabled = parse(Bool, @load_preference("memcheck", "false"))
9+
10+
const tracked_objects = Dict{Any,Any}()
11+
12+
function mark_alloc(obj::Any)
13+
@static if memcheck_enabled
14+
io = Core.stdout
15+
new_alloc_bt = backtrace()[2:end]
16+
17+
if haskey(tracked_objects, obj)
18+
old_alloc_bt, dispose_bt = tracked_objects[obj]
19+
if dispose_bt == nothing
20+
print("\nWARNING: An instance of $(typeof(obj)) was not properly disposed of, and a new allocation will overwrite it.")
21+
print("\nThe original allocation was at:")
22+
Base.show_backtrace(io, old_alloc_bt)
23+
print("\nThe new allocation is at:")
24+
Base.show_backtrace(io, new_alloc_bt)
25+
println(io)
26+
end
27+
end
28+
29+
tracked_objects[obj] = (new_alloc_bt, nothing)
30+
end
31+
return obj
32+
end
33+
34+
function mark_use(obj::Any)
35+
@static if memcheck_enabled
36+
io = Core.stdout
37+
38+
if !haskey(tracked_objects, obj)
39+
# we have to ignore unknown objects, as they may originate externally.
40+
# for example, a Julia-created Type we call `context` on.
41+
return obj
42+
end
43+
44+
alloc_bt, dispose_bt = tracked_objects[obj]
45+
if dispose_bt !== nothing
46+
print("\nWARNING: An instance of $(typeof(obj)) is being used after it was disposed.")
47+
print("\nThe object was allocated at:")
48+
Base.show_backtrace(io, alloc_bt)
49+
print("\nThe object was disposed at:")
50+
Base.show_backtrace(io, dispose_bt)
51+
print("\nThe object is being used at:")
52+
Base.show_backtrace(io, backtrace()[2:end])
53+
println(io)
54+
end
55+
end
56+
return obj
57+
end
58+
59+
function mark_dispose(obj)
60+
@static if memcheck_enabled
61+
io = Core.stdout
62+
new_dispose_bt = backtrace()[2:end]
63+
64+
if !haskey(tracked_objects, obj)
65+
print(io, "\nWARNING: An unknown instance of $(typeof(obj)) is being disposed of.")
66+
Base.show_backtrace(io, new_dispose_bt)
67+
return
68+
end
69+
70+
alloc_bt, old_dispose_bt = tracked_objects[obj]
71+
if old_dispose_bt !== nothing
72+
print("\nWARNING: An instance of $(typeof(obj)) is being disposed twice.")
73+
print("\nThe object was allocated at:")
74+
Base.show_backtrace(io, alloc_bt)
75+
print("\nThe object was already disposed at:")
76+
Base.show_backtrace(io, old_dispose_bt)
77+
print("\nThe object is being disposed again at:")
78+
Base.show_backtrace(io, new_dispose_bt)
79+
println(io)
80+
end
81+
82+
tracked_objects[obj] = (alloc_bt, new_dispose_bt)
83+
end
84+
return
85+
end
86+
87+
# helper for single-line disposal without a use-after-free warning
88+
function mark_dispose(f, obj)
89+
ret = f(obj)
90+
mark_dispose(obj)
91+
return ret
92+
end
93+
94+
function report_leaks(code=0)
95+
# if we errored, we can't trust the memory state
96+
if code != 0
97+
return
98+
end
99+
100+
@static if memcheck_enabled
101+
io = Core.stdout
102+
for (obj, (alloc_bt, dispose_bt)) in tracked_objects
103+
if dispose_bt === nothing
104+
print(io, "\nWARNING: An instance of $(typeof(obj)) was not properly disposed of.")
105+
print("\nThe object was allocated at:")
106+
Base.show_backtrace(io, alloc_bt)
107+
println(io)
108+
end
109+
end
110+
end
111+
end

0 commit comments

Comments
 (0)