Verifier Pass doesn't catch calls with different number of parameters than the definition

Compiling two files where one has a function declaration and a call to it with some number of parameters and in the other file there is a definition of that function with a different number of parameters. For example this compiles and runs:

test.c

int foo(int);

int main() {
    return foo(8);
}

foo.c

int foo(int a, int b) {
    return a + b;
}

In the pass Verifier.cpp there seems to be a check for this but it always passes. I tried running IR that might be generated after the linker for this and this passes(This type of IR is generated when compiling with llvm-link as the linker):

define i32 @main() {
  %res = call i32 @foo(i32 8)
  ret i32 %res
}

define i32 @foo(i32 %a, i32 %b) {
  %res = add i32 %a, %b
  ret i32 %res
}

When debugging opt running the verifier pass it seems that the FunctionType that the verifier gets in visitCallBase from the call instruction isn’t generated from the definition of the function but from the call itself making the check there never fail.

The IR verifier does not report an error, because the IR is well-formed, even though actually executing the code would result in undefined behavior.

If you want to catch cases of fishy IR that will likely cause UB, you can use the Lint passes instead. opt -passes=lint prints:

Undefined behavior: Call argument count mismatches callee argument count
  %res = call i32 @foo(i32 8)

Things like this cannot be malformed IR because the code might occur in dynamically dead code, resulting in a problem that is still well-defined overall.

1 Like

This is a regression from the opaque pointer transition I believe: Verifier regression for caller/callee after enabling opaque pointers · Issue #63484 · llvm/llvm-project · GitHub

I don’t think I agree with this statement, otherwise how were we able to reject this in the verifier before LLVM 15?

Let me make my statement more precise: IR needs to have a well-formed (but not necessarily well-defined) way to call a function with a signature that differs from its declaration.

That’s the requirement, but the precise syntax to do so is of course up for debate. The syntax to do this was different prior to the opaque pointer transition, and the use of the old syntax was of course enforced by the IR parser.

Are you saying that the old “syntax” isn’t something that could be built in memory and caught by the IR verifier? It seemed to me that before it was the IR construct that was different, not the “textual syntax”. And I thought it was caught by the IR verifier (hence the IR was invalid), which is why I’m confused why you bring up “syntax” here?

What I mainly wanted to highlight in my last post is just that there has to be some way to represent this – and yes, that does include both textual IR and in-memory representation.

This used to be call i32 bitcast (i32(i32,i32)* @foo to i32(i32)*)(i32 8), and now it is just call i32 @foo(i32 8).

With opaque pointers, from a perspective of in-memory IR, I believe that the current representation is the only sensible one. But for textual IR, there is leeway in the choice of syntax, which is why I phrased things in terms of syntax.

For example, we could have call i32 @foo(i32 8) ; signature mismatch if we want to just make this easier to spot in IR dumps, or call signature_mismatch i32 @foo(i32 8) or so if we wanted to make this explicit in IR inputs as well.

1 Like