C function pointers : trick questions

Hello guys. Today I come to you with (what I think are) unusual C function pointer behaviors. I’ve tried looking for answers online but it seems I’ve crossed a threshold and entered “voodoo programming” territory. And it feels a bit like this :

Some context first. I’m working on a tiny library to create simple menu-based user interfaces for Linux console programs. I just needed this for a larger job and since it’s kind of a recurring need I decided to spend a few days hacking a generic menu library together.

As you might guess, this is a curses-based project. The gist of it is, it lets you build menus dynamically at runtime because it stores menus as linked lists of individual entries; each entry being a structure that contains (mainly) a label string, a pointer to the next menu entry in the linked list, and a pointer to the function that will be called when the entry is selected. I’m not trying to innovate, here.

I was kind of in a hurry and I didn’t remember the exact syntax for function pointers, so I ended-up using void pointers instead (when passing a function’s address to my library) and then calling those void pointers like functions. It works, gcc doesn’t even throw a warning. Eventually, as I added more code, I toyed with passing arguments to those functions.

That is where the weird questions come.

Normally, when you want a function to take a function pointer as argument, you write something like this :

int menuAddEntry (menuItem* m, char* l, void (*f)());

The last argument specifies argument “f” must be a function that doesn’t return a value (void) and takes no argument. Instead I wrote this :

int menuAddEntry (menuItem* m, char* l, void* f);

You can call this function just like the first one, however it will not cause the compiler to check for the type of the return value and the argument list on the function(s) you pass as argument. It will take any function.

On its own, that’s kinda cool, but then I thought : “wait a sec, how do I call this function ? The compiler won’t know if I’m providing the right arguments to it.”

Turns out, that’s not a problem. You can call it without argument, or with ten arguments, and it works : gcc just lets it slide, no warning. Daym !

image

Better yet, it runs, apparently without a problem.

So that made me think harder : what happens if I call a pointer to a function that takes some arguments, but call it without any ? For example, suppose this function :

int func (int a, int b, int c, int d);

I assign “func” to a void pointer, and then call it like so :

void *fptr = func;
fptr();

This compiles without warning and runs without problem.

Now you’re probably be wondering, what are the values of a, b, c, d ?

Well, it appears that “a” is consistently zero but any argument past the first one is some “random” number. On occasion, it’s the value of a local variable in the scope of the function that called the pointer.

I don’t have a K&R close-by, it’s probably already packed with my compiler design books as I’m moving soon. Do you guys know exactly what’s happening here ? Specifically, how unsafe is it to use void pointers as function pointers ? Because it does seem rather flexible and useful… if it’s deterministic.

A few things I’m wondering :

1/ When I call a void pointer like a function, what does the compiler really do ? It’s obviously going to create a new stack frame for that call and transfer execution to the function. Somewhere along the way, it’s going to push the arguments from my function call onto the stack, so the function can get them.

If I declared my function pointer normally, the compiler would be able to check my calls and see if I’m passing too few or too many arguments, and if the types don’t match. But with a call to a void pointer, it appears gcc just pushes everything onto the stack without doing any checks.

That sounds suspiciously like a vulnerability.

2/ When my function executes, if its argument list is bigger than what I provided when calling it through a void pointer, the compiler still creates them as local variables and pops their values from the stack. Except now it’s popping more values than I actually gave, which means some of them are actually data from a different stack frame.

This also sounds suspiciously like a vulnerability.

Now, I’m been around the block quite a few times. My first use of C dates back to the mid-90’s and it’s the language I use the most. Yet I don’t consider myself a guru or a hacker. Just an above-average programmer who even manages to make a living out of it. I’m surprised I’ve never noticed those behaviors before, but at the same time, I feel pretty sure this is something actual gurus have to be well aware of already.

Or is it ?

One thing I haven’t tried yet is get a return value from a function that doesn’t return one, by calling it through a void pointer. I’ll keep you posted. Also, if this is something I should have been aware of since school, feel free to pelt me with virtual rotten tomatoes, but right now I’m feeling like Neo :

image

3 Likes

One of the official languages at the office is C++, not really C … and I was never a big fan of either C or C++; but I can’t help but wonder that there should be some warnings against implicit casting of void* that maybe a compiler can emit with -Wall or similar. The fact that the language allows you to cast void* into anything explicitly doesn’t mean it should blindly let folks do so, IMHO.

Mixing data and function pointers is explicitly forbidden in the C spec. It depends entirely on the architecture and ABI in use, whether it works or not.

Since on most modern/common architectures function and code addresses are identical on a hardware level, it works as long as the ABI is upheld.

I am guessing the compiler will do the exact same thing, as if you call a function that was never declared: implicit function declaration. it will assume argument count and types from what you give it for the call and do the same, as if that function was actually declared.
What happens here should be the same, as when you give the compiler mismatched declarations and implementations statically, without any function pointers involved.

If the register and stack contents are compatible to the function actually being called it works without a crash.

Due to the high likelyhood of mismatched arguments and non-portability one should not do this though.

Example 1: Argument is declared as int32, but you call with int16. If declared, compiler will silently extend the value, if not, it will assume int16 is expected by the caller. Problematic if the representation of an int16 and int32 argument is not identical in the ABI anyway. On a 32bit system, that hands most arguments over in registers, this will not make a difference. But on a 16bit system where 32bit values occupy 2 registers, it will make a huge difference.

Example 2: The actual function expects a certain number of arguments. The ABI defines where those arguments are located. Possibly on the stack or in registers.
If the caller does not define those arguments correctly, the callee does not know and will still access the same locations, which most likely contain local variables / state from the callers frame.

Depending on how a frame is dismantled, stack might also break on exit of that function. For example if the callee expected 4 arguments on stack and per ABI it would be the callees job to clean those from the stack upon return.
If the caller only populated 2 values however, the stack will misalign and the callee will overwrite / destroy the callers frame. This gets particularly important if no frame pointer is employed, as most compilers do on higher optimization levels.

Just a few examples of how things can go wrong here on an assembly level.

4 Likes

I am now watching this thread because this seems uber interesting. Also, this is why I kinda like that my career has turned me into a .NET dev; I no longer have to think about these kind of things :slight_smile:

Indeed. Instead you’re now dependent on Microsoft not to f*ck-up…

.NET is comfortable, but to me it’s the same kind of comfort born from ignorance of the danger that people enjoy right up until their car crashes into a tree because of a design error that should have caused a mass recall several years prior :sweat_smile:

I code in many, many different languages, but I’m mostly an aero / space guy and work on mission-critical systems. We like C for very good, life-saving reasons. I can’t help but laugh whenever someone writes an article about C# entering the world of embedded systems :scream:

Still, I agree with you that my little “problem” is very interesting. Spectators are welcome :wink:

image

@Nefastor Just curious but you ever use Haskell? Haskell has a safer type system and I understand that it can call C functions if you need to open a trap door for that kind of control. I haven’t used that feature but I am guessing you would wrap the C function in some sort of Monad.

It sounds like you shouldn’t use void* … unless you’re writing drivers (working with hardware directly is … basically you’re your
own safety) or working on code where you wish you had templates, but don’t have them because you’re working in C … so your options are limited and you have to resort to void* .

I wonder if some came up with a function attribute like __void_star_is_ok or some such thing to help static analysis and linters and stuff.

I’ve heard about it, but I’ve never had the opportunity. I’m more into assembler and C for microcontrollers / critical systems, some C++ for application programming (though I’m gradually replacing it with C# and .NET on Windows), various scripting languages (good old PERL but also Python now that’ve started dabbling in ML), and then some specialized engineering languages, mostly MATLAB. And then VHDL and Verilog but that’s not exactly programming, it’s hardware design (though it blurs the line). Also did some R, some Analog Device DSP stuff, some industrial automation… I feel older just writing that list. I started as a kid with BASIC, of course, and then all flavors of HP RPL (orthodox and system) plus Casio and TI calculator languages. Basically, if something’s got a processor inside, there’s a good chance I’ve coded on it :slight_smile:

Once in a while I look into new languages in case I might find some new useful tool. Maybe I’ll get around to trying Haskell someday, but right now I’m having fun with MicroPython and next on my list is Qiskit (though that may not become actually useful in our lifetimes)

Not recommended. Function pointers are tricky, the syntax is terrible, and getting the compiler to check stuff is a very good idea.

Undefined behaviour (not sure if that’s the C terminology these days), anything might happen.

Note also that void func(); declares that func takes an unspecified number of arguments. Use void func(void); to say that it takes no arguments. StackOverflow tells me that “The use of function declarators with empty parentheses is an obsolescent feature”.

3 Likes

I haven’t heard anything about this; I thought Rust was the expected successor to C?

Also, alot of the issues I see with .Net in production systems is due to the fact that the ease of use design has caused a plethora of newer devs that never bother to actually question or understand what is happening under the hood of all those fancy abstractions.

But yeah, I’d be highly skeptical to use it in critical things like what you work in.

It’s amazing for quickly developing and deploying simple applications like a REST API endpoint or console app though.

What MCU platforms do you use?

Any reason you couldn’t use C++ , which would provide you with a more expressive and safer type system such that you’d end up not having to do void* ever.

e.g. the FreeRTOS on my esp chips is perfectly fine running tasklets in c++ and these tasklets can still interface perfectly fine with random average garbage quality hardware manufacturer written C code.

The only reason I can think of is if you’re writing something very low speed and extremely power saving where you’re constantly looking at disassembled compiler output anyway or where you’re stuck using an ancient MCU due to some random reason and don’t get the luxury of having a few hundred KB of ram, but might have flash.

These days I mostly code on ARM-based chips, anything Cortex-M or Cortex-A, bare-metal or with an OS, often Linux, sometimes an ARINC-653 solution. But I’ve coded on pretty much everything under the sun : MC68, MCS51, AD DSP, AVR, PSoC, etc… I also code on MicroBlaze (that’s the Xilinx FPGA soft processor)

Certification ! Some of the stuff I code must meet DO-178B DAL A. While C++ isn’t forbidden in that context, certifying C++ code to that level is significantly more complex (and therefore expensive) than C code. Plus, there’s never a need in embedded systems for object oriented code.

What ends-up happening is partitioning : the life-or-death stuff is written in C and may even run on a dedicated core. You keep it small, simple, easy to prove, and safe. Everything else, i.e. GUI, you run elsewhere, or on something else, so it doesn’t need to meet the same DAL.

The car industry has similar (although more relaxed) constraints. You may want to look-up MISRA coding rules, for example :

It’s generally a good idea to follow those rules, even if your code isn’t life-or-death stuff.

Yeah, see, if you so much as whisper this anywhere the FAA can hear you, they will take your engineering degree, burn it in front if your eyes, make you eat the ashes, and then throw you out the nearest window :rofl:

By ESP, I assume you mean ESP8266 or ESP32. Those cheap gadgets aren’t qualified for applications riskier than turning a coffee maker into an cybersecurity liability. Their datasheet is what, 20 or so pages of Chinglish, and the manufacturer denies any responsibility for any accident they may cause. I have a few, they are a fun distraction on a rainy day when my girlfriend isn’t here.

FreeRTOS, as such, isn’t a safety-critical OS. Though there are commercial versions of it that claim to meet certification requirements (SafeRTOS). I’ve never seen them used in the real world. Note that those are not FreeRTOS, just compatible implementations that are designed so you could trust lives to them. That doesn’t come cheap. I have a friend at GE Medical who uses SafeRTOS, IIRC.

All in all, with programming languages, there’s one big rule : the more abstract and flexible they are, the less reliable they are. If you want to make your life easier on every level, stick to C. It’s taken me 20 years to stumble on that void pointer function call business we’ve been discussing, and that’s mainly because I almost always code to rigid rules and standards.

You may not know this, it’s old news now, but one of the many reasons for the F-35 troubles was the use of C++ in addition to C.

Anyway, here I go again ranting. I must be getting old, I need to cut this out before my hair gets any grayer :sweat_smile:

3 Likes

I’m gonna have to look into this. I’m still on K&R second edition and ANSI C but I’ve seen (void) argument lists pop up more and more in some programs. I hadn’t realized it meant an empty argument list means “any number of arguments”. Don’t we have “…” for that ?

Speaking of K&R, I borrowed a colleague’s today. Yeah, it doesn’t mention calling a void pointer as causing “undefined behavior” but it does say that to be careful around void pointers because of their ability to be recast as a different type than the data they are pointing to. Which can create undefined behavior.

Heh, nothing new : as usual, C’s pointers is always what’s gonna get you :smiley:

image

2 Likes

I have no experience with those standards, but I sympathize with the paperwork and red tape you must be going through every time you want to introduce anything new or change anything. It must be draining.

Yes, my point was that “even those” things can run it.

Ha, I wonder if any of that budget made its way into education.

I heard F-35 being called the biggest social programme ever and that the main reason for delays was simply that the delays were in the interest of various political committees whose members were incentivised to keep work going as long as they could and would also grow the programme, and that they saw that the project goal was to keep steering spending, and merely delivering a plane was secondary. ($1.5T in lifetime development costs is amazing - how many years of small countries worth of GDP is that?).

Could it have been spent on developing high speed transatlantic rail, or stabilizing other countries more directly through investment and incentive structures… would that have been more cost effective in the long run?.. hard to tell. Not sure how much of it is c++ fault.


I’m curious about what certification entails, not so much as to work in the area, but I do wonder what working with code looks like on a simple day to day basis… I’m imagining little actual code, lots of coffee and documentation, and lots of creative test writing.

You haven’t mentioned cppreference.com, so I have just in case. It has a C reference section which comprehensively details the C standards. Indispensable.

1 Like

I felt this one. And yes, you are so right. I dabble with them, but would never trust my life or anything important with them.

Also can confirm, but I think that was mostly skill and expertise induced more so than the mixing of the two. C++ is just really hard to certify, especially if it is before the C++14 standardization. but I am also not a fan of C++ so…

They ran a play straight our of Boeing/McDonald Douglas’ playbook.

Lot’s of combing through white papers, compartmentalization of code, low level analysis, and lots of proofs. At least on the side of things that I have worked on.

You didn’t mention the code reviews by third parties, generating remark documents, where each remark on your code must be answered by prose to clarify your intent, your proof, or admit that you indeed made a mistake and describe how you intend to correct it, which leads to another cycle of code review. I once worked six months on a DAL A operating system for landing gears… then I had to participate to code reviews every 6 or 12 months for the next three years. It’s now in use in several Airbus landing gear systems. The payoff is this was over 10 years ago and since then my code has landed airliners millions of times without ever causing the slightest incident.

Of course this is the kind of work that makes me look down on, say, Microsoft… who’s patching their operating systems’ bugs every week from release to sunset. The reality is that an OS like Windows is intended for markets that couldn’t bear the development costs and performance limitations of actual safe software. Shikata ga nai. It is what it is. On ne peut rien y changer.

Anyway, to answer your question, coding high-reliability software…

image

There’s a process to it, it’s not that complicated, and if you’re rigorous about it then it’s actually just as fast as any other type of software development. The best path is the path of least resistance, don’t overcomplicate things, don’t use fancy tricks to try and look smart, just take each requirement and implement it in the simplest way you can think of. If you do that, your code will go through reviews like a hot knife through butter.

Good call. I usually code on machines that aren’t connected to the internet, so I tend to resort to books.

The way they tell it, it actually went the other way around. Apparently there were a lot more C++ programmers than C programmers on the market when they started the F-35 program, so they decided to try and exploit this abundance of human resources. Or maybe they are just making-up excuses.

In peace time, big projects like the F-35 or my own country’s nuclear aircraft carrier are indeed social programs, somewhat. The real purpose is to maintain the country’s competence in designing and building military equipment in case it’s ever needed. As we’re seeing these days, as long as people like Russia’s dictator are around, that’s going to be a necessary evil.

2 Likes

I’m trying to figure this out but i have to go for the day :confused:

This.


Reason this is interesting to me is that as my day job I work as a Site Reliability Engineer at Google. The systems I work with have a new release cut and rolled out 4 days a week automatically following various automated testing and employing heavily instrumented canarying. Code during development is ofcourse reviewed and there’s a ton of helpful tools that ensure sufficient test coverage, but there’s also a lot of institutionalized best practices that humans will insist be followed.

While at the crux of it, managing reliability boils down to managing systems through code and process reliability, it’s a wildly different environment. There’s no such thing as code in production not changing for 6 months, either because it actually changed, or because one of the dependencies changed or because the code is being invoked differently. Rolling back a binary version more than a week is basically not done as it’s too risky. … and yet, we still manage to get out of it >1M user facing QPS ; at 6.5nines <250ms end-to-end.

It sounds like letting some of the auditors/reviewers over our code base and having them try to figure out how different systems fit together would be a cruel joke. For some reason the analogy often used for what SRE do is “changing the engine on a plane mid flight”. It’s ridiculous.


Perhaps void* usage gets worse in an environment where code is changed more frequently.

Do you happen to use or employ STPA or FMEA or any similar analyses?

Sounds about right. I applied for one of those jobs and the stuff that they put me through felt exactly like that.