9

I have large C++ project written by someone else long time ago. It contains code like:

string CVersion::GetVersionStr() const
{
  string ret;
  char VersionStr[100];
  DWORD v1, v2, v3, Build;
  GetVersion(&v1, &v2, &v3, &Build);
  sprintf(VersionStr, "%d.%d.%d.%d", v1, v2, v3, Build);
  return string(VersionStr);
}

Now I think because of wrong format specifier (%d) this code has undefined behaviour.

DWORD on my PC is declared as

typedef unsigned long DWORD;

My questions are:

  • does code contain undefined behaviour?
  • Is there any platform/situation where it would not be undefined behaviour? Maybe it is fine for some values of v1?
  • The software has been working correctly for long time, so can it happen that in practice, despite above is undefined behaviour, the software still works fine?

PS. This software was written something like 10 years ago using Visual Studio

  • 2
    "It appears to work correctly" is always a possible outcome of undefined behaviour. – Jonathan Wakely Nov 11 '15 at 09:45
  • What is your compiler? Some compiler have defined behavior for situations that have UB according to the standard. E.g. in former times, GCC guaranteed wrap around behavior for signed integer overflow. – MikeMB Nov 11 '15 at 09:59
  • @MikeMB: Was it UB always? this project was written something like 10 years ago using Visual studio –  Nov 11 '15 at 10:49
  • @user200312: I'd assume it was also UB 10 years ago (and it was definitively a bug), but at that time, compilers did a lot less optimizations that assumed UB-free code, so it might have been *safer* back then. Consequently programmers did care less about those things, as long as the code appeared to work. Also MS was never too good in following any standards. – MikeMB Nov 11 '15 at 11:38

8 Answers8

4

Yes, the behaviour is undefined if DWORD is an unsigned long. The correct format specifier is %lu.

Because undefined behaviour is exactly that, your second and third questions are not meaningful.

Why not use something like a std::stringstream and exploit <<?

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • What about my 3rd and 2nd question? (Like I said code may use wrong format specifiers in other places too - I need to investigate that still). Right now I am asking about this case –  Nov 11 '15 at 09:28
  • Your compiler is being kind to you. Other compilers might eat your cat. – Bathsheba Nov 11 '15 at 09:30
  • It's UB so fix it. As per the "Joel Test" (point 5), you should fix bugs before writing new code. – Bathsheba Nov 11 '15 at 09:31
  • @user200312 It depends on your definition of "to work well". According to mine, your code doesn't work well. All you can say is that it passed some tests at some point. – juanchopanza Nov 11 '15 at 09:32
  • @juanchopanza: Yeah I mean it has been working for years –  Nov 11 '15 at 09:32
  • @user200312 Not according to my definition of "working". – juanchopanza Nov 11 '15 at 09:33
  • @user200312 Linux kernel "worked" for years with undefined behavior until one update gcc got better and more aggressive at optimizations. Suddenly "worked" became "bug, security exploit, kaboom". See this LLVM blog about why UB is more dangerous than it first looks: [Part1](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html) [Part2](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html) [Part3](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html) – bolov Nov 11 '15 at 09:44
  • 1
    An interesting question would be to explore deeper the reasons just *why* a wrong format specifier has the fairly drastic consequence in the standard of UB, especially because none of the (fairly mainstream) implementations known to me did anything but produce expected outputs. I can imagine that the stack layout may get compromised because `printf` must fiddle with variable args; perhaps some cast fails; but probably not for most classes of format confusion, like here. – Peter - Reinstate Monica Nov 11 '15 at 09:46
  • 1
    @PeterSchneider If `unsigned long` is larger than `int`, you'll get incorrect output on subsequent `%d` specifiers on little endian, and incorrect output on *all* `%d` specifiers on big endian. If you have any `%n` in the mix and you're on the wrong offset in the arguments passed in, hilarity ensues. IOW, the exact consequences of wrong format specifiers depend on the implementation, so the behaviour cannot be defined universally. Hence it's Undefined. – Angew is no longer proud of SO Nov 11 '15 at 09:52
  • @PeterSchneider: Leaving it UB allows, among other things, for the possibility that an implementation might pass parameter-type information along with varargs parameters and force an abnormal termination when parameters are used incorrectly--a behavior which could in some cases genuinely be useful. The idea that UB could allow semantically-useful behaviors, however, seems to have fallen by the wayside. – supercat Nov 13 '15 at 01:55
  • @bolov: What "worked" was an absurdly under-specified language, combined with compiler writers who historically recognized the value in supporting features and guarantees beyond those required in the Standard. – supercat Nov 13 '15 at 01:57
3

Others have reported that this is indeed Undefined Behaviour and as such, should be fixed. However, I understand that in a large project, fixing things which have apparently woked for ages can be costly and hard to justify.

Why it's works for you: when using Visual Studio on Windows, int and long are the same size, and 2's complement is used for signed numbers.

If you're willing to live with having UB in your code as long as that UB seems to be doing what you want, you can add a safeguard so that the compiler tells you when the UB results might change. Add this assertion:

static_assert(sizeof(DWORD) == sizeof(int), "Fix the format specifiers NOW");

somewhere in your code (ideally near such a printf). As long as this assertion holds, the risk of things blowing up are fairly small.

They're non-zero because the compiler could say "UB isn't supposed to happen" and optimise the whole thing out or something like that. But that doesn't seem to be happening with your compiler, and is not something I would expect Visual Studio's compiler to do. But if you go this route, you should definitely rigorosuly test the relevant parts of code each time you change compiler versions or settings.

In the end, it's your call: as it's been working, the danger of something going wrong out of the blue is fairly small (especially if you add the size assertion). But it is a potential problem swept under a rug.


To summarise my view: yes, it can work in practice in your situation (VS on Windows only), but you have to be careful. Rigorously test anything before shipping it. The chance of something breaking is fairly small. Staying with the same compiler version & settings helps to keep it as small as it can be.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • Angew thanks - as I understand recompiling this project with newer version of Visual Studio even increases the risk right? –  Nov 12 '15 at 08:21
  • @user200312 Yes, that's what the answer says: any change in compiler version or settings will need rigorous testing of the parts using incorrect format specifiers. Personally, I wouldn't expect it to break, but I'm not on Microsoft's compiler team so I cannot say what it will/won't do. – Angew is no longer proud of SO Nov 12 '15 at 08:22
  • But if I find out which version of compiler was used to compile that project and recompile (say I add something) the project using that compiler version am I on the safer side? –  Nov 12 '15 at 08:48
  • @user200312 Yes. As I said, my personal feeling is that you're pretty safe even with a later VS, but you should definitely test it each time you make a compiler or settings change. – Angew is no longer proud of SO Nov 12 '15 at 08:53
  • Yes but I was wondering if using same compiler was safer? (if somoene wants to add new feature or fix other bugs) –  Nov 12 '15 at 08:55
  • @user200312 My comment starts with a "yes," which is a direct answer to the "am I on the safer side" question. But you seem to be asking for a 100% assurance that something you do will work. **Nobody** on this site can give you such 100% assurance, *you have UB in your code.* Only a developer of VS's compiler could potentially give you such assurance, *nobody else.* – Angew is no longer proud of SO Nov 12 '15 at 08:59
  • I understand but I think your answer is also reasonable and can work in practice? I am not looking for 100% answer. Yes, sorry I misunderstood "Yes". Because there maybe other similar UBs there too, and if it works, I would recommend only to recompile that project using same compiler. What do you think? –  Nov 12 '15 at 09:02
  • @user200312 If I didn't think my answer could work in practice, I wouldn't have posted it. I added a short summary, I expect that will clear out all doubts about how I mean it. – Angew is no longer proud of SO Nov 12 '15 at 09:13
  • Your static assertion is insufficient since the code would invoke Undefined Behavior if e.g. `DWORD` is an alias for `unsigned long`, even if that's the same size as `int`. There is no way to statically test whether an implementation will decide to do wacky things because it's "allowed" to. – supercat Nov 13 '15 at 01:49
  • @supercat The answer *never* says that the static assertion protects against UB. Quite the opposite. It does, however, say, that the static assertion passing decreases the likelihood of stuff going wrong because of that UB. And that is true. From my experience with MSVC, I don't expect this UB to do anything other than what the OP wants, and the answer is trying to say as much. It will always be UB, but as long as the toolchain is MSVC targetting Windows, I claim it to be benign UB. – Angew is no longer proud of SO Nov 13 '15 at 09:03
1

does code contain undefined behaviour?

Yes, as already pointed out.

Is there any platform/situation where it would not be undefined behaviour?

UB is defined by the Standard, not by implementations. So no, unless it's nonconforming (but then it wouldn't be C++).

The software has been working correctly for long time, so can it happen that in practice, despite above is undefined behaviour, the software still works fine?

A valid consequence of UB is working fine. So yes.

Emil Laine
  • 41,598
  • 9
  • 101
  • 157
1

I'm probably going against the grain a bit with this answer but I would say testing is king.

From an engineering perspective I would say that as long as your software operates according to spec during rigorous testing then its good to go. That would be my definition of "good to go".

HOWEVER you have identified some coding practices that could trip this software up in environments your testing does not cover or that may even give rigorous testing regimes false positives.

The code needs to be fixed but you also have to weigh up the cost of potentially introducing new bugs when you go through fixing all the old bugs.

First I would institute a coding practice to end the practice that produces undefined behavior.

Then I would think about incrementally fixing the old bugs so you don't make too many changes to the code base all at once. You can then concentrate on rigorously testing the new code for new bugs before moving on to a new section of the code base.

What you don't want to end up with is a code base that is less stable than you began with because you introduced new bugs all over the place while trying to fix the old bugs.

Galik
  • 47,303
  • 4
  • 80
  • 117
  • 1
    @user200312 Galik's pragmatic approach is that "correct" means "passes tests and works in the field". In this sense, your program *is not even incorrect.* Also, consider that your program, like any large code base, most likely contains many more errors, and more severe ones. They are just not encountered in the usual code paths run, or the ill effects don't surface, in the tests and use cases. Considering that new code will be buggy as well, but less well tested, such a "correct" code base should be changed only carefully, that's all. – Peter - Reinstate Monica Nov 11 '15 at 11:07
  • @PeterSchneider: it may as well be VS didn't consider that as UB?? Galik has point because this is large project and re-testing it will not be easy –  Nov 11 '15 at 11:19
  • 1
    @user200312 Only you can make that call. I wanted to point out that there is a cost to fixing these bugs and it is the maintainer's responsibility to weigh up the cost of fixing. I don't know the potential for this specific problem to blow up with a change in compiler version/platform. I would definitely want to investigate that. The behaviour may be undefined according to `C++` standards, but it may be well defined according to your specific compiler vendor. Write to them, see what they have to say. – Galik Nov 11 '15 at 11:25
1

It is undefined behavior as per the C++11 standard (more precisely, the portion of the standard "inherited" from the C99 standard, for the actual wording you would need to look mostly at the latter). However, you said it's been working fine, which, like any other behavior, is perfectly compatible with the notion of UB. Some compilers generate code that generates a "hard" run time error on specific types of (detectable) UB. Just today I saw that Clang may generate a UD2 opcode (which is undefined and guaranteed to cause a processor exception) on x86-64 for code like this (debug, no optimizations):

int &f()
{
// no "return"
}

GCC, on the other hand, will generate a no-op function and accessing the return value will cause an access to a "random" memory location, probably leading to a segfault. Both compilers may print a warning, Clang by default, GCC when -Wall is specified. So, as you can see, the practical consequences of UB differ.

You said that the printf has worked for years. Modern compilers generally may produce warnings for mismatching or outright invalid printf format string literals. I can only make guesses about the code that VC++ generates, however I do know that int and long have the same size on both x86 and x86-64 because VC++ uses the "LLP64" memory model (in which (unsigned) long long` and pointers are 64-bit, but long is only 32-bit). So, if the compiler just follows the "do what I say" approach, the worst that can happen in practice is displaying a very large unsigned value as a negative value because of the sign mismatch, and you have probably not used very large values.

GCC and Clang on x86-64 use the "LP64" model in which both long and long long are 64 bit wide, which is more likely to cause practical problems here.

Arne Vogel
  • 6,346
  • 2
  • 18
  • 31
  • 1
    Are you sure, this comes from C99 and not C89? – MikeMB Nov 11 '15 at 11:48
  • I didn't mean to discuss the chronology – by inheritance I meant that the C++11 standard references the C99 standard, though many of the referenced parts may go back to ISO C90 / ANSI C89 or pre-standard C. In the future, I should probably refer to C++14 and C11 though, simply because they are the latest standard versions. – Arne Vogel Nov 18 '15 at 12:31
  • I was actually wondering whether c++11 references the c99 or C89 standard. For some reason, I though it would still reference the version from 1989. Thanks for clarifying that. – MikeMB Nov 18 '15 at 15:05
1

This tries to answer your question in the comments whether you should in my opinion fix the code.

The answer in my opinion depends on:

  • Whether there are good tests with extreme values which cover the code paths in question, and the tests are performed whenever the compiler or the CRT changes (the links go to articles about the major overhauls that happen(ed)).
  • The actual type/format mismatch. Cf. also the answers to my follow-up question, especially Jonathan's whose opinion I always regard valuable. I'd not be concerned if in order of importance

    • no string conversions are present for non-addresses or potentially non-null-terminated data;
    • all formats are integer types (because some bit patterns for example are NaNs when interpreted as floats, but all integer bit patterns are legal);
    • the summed-up formats "promise" not more data than is actually passed as arguments (e.g. "%ld" with int args would be bad); that is because printf would access memory beyond its call stack. Unless it's Friday the 13th reading beyond printf's stack shouldn't be a problem because it's a read access and almost certainly within the programs's allocated memory, but still.
  • The target architecture and build system.

    • Many caveats in the standard do not apply to an off-the-shelf x86 PC (which you may be running). This is the case with reading uninitialized data which works like a charm unless your register has a "you made a mistake" bit, cf. Trap Representation, unsigned char and IA64 NaT.
    • On a little endian system like an x86 you'll read the same small values from a memory location independent of the integer type as which you interpret it, but not so on a big endian architecture.
    • Protect against porting (portions) of the code, however small the chances seem. I liked Angew's assert.
    • While formally the compiler is free to do what it wants -- the code is, after all, invoking undefined behaviour --, the Microsoft people are less likely to say "nyaah, nyaah, the standards people said we can do this" than the gcc people.
    • To get an idea why everybody is so picky about UB these days read this fairly popular and instructive LLVM blog article about interactions between UB and optimization which lead to unexpected results. The bottom line is that modern compilers may "see" more of the code from both the called and the calling side than they did when each translation unit was compiled separately and there was no link time optimization. For the old model it's fair to say "printf necessarily uses va_arg, the code cannot know anything about the actual arguments, and therefore we are shielded and safe". These days printf's code may be considered at link optimization time and suddenly be open to a whole new class of optimizations like circumvening va_arg altogether, using intrinsics or subsequent "code poisoning" because the type mismatch is suddenly visible (without parsing the format string).
  • The semantics. The output of printf with a conversion which does not match the exact argument type may be "wrong", i.e. interpret a negative value as a large positive unsigned etc. If a build version number in a message box of an in-house program becomes negative, who cares, really. But if the version number is on an ATM screen or on a test report submitted to the FAA it's unacceptable.

Therefore: For the example given I'd technically not be concerned, because the arguments and the expected arguments are integers of the same size (32 bit). That is true even for 64 bit architectures, probably for reasons similar to yours. And the Microsoft compiler is less likely to do surprising things.

That said, I like a clean code base and would make it a long-term goal to clean it up, but without haste, with reviews and with good tests. That can happen file by file, there is no need to fix all or nothing for a given release.

Community
  • 1
  • 1
Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62
  • I think you also agree with Answer of Angew, which I like. I will read your post in more details bit later. Thing is recompiling this project increases risk if recompiled with newer compiler, which I might have to do -if want to add new feature, fix other UB bug etc. –  Nov 12 '15 at 11:03
  • I like the "Nyah nyah" comment. It irks me that people have lost sight about why having the Standard leave things undefined was *semantically useful* in the 1980s and 1990s. – supercat Nov 13 '15 at 01:51
  • Note that Agnew's desire to ensure that "int" isn't larger or smaller than DWORD is good, but unfortunately unless one uses memcpy or decomposes things into "unsigned char"-sized pieces the Standard allows a compiler to do whatever it likes with code that writes a "long" and reads an "int" or vice versa. – supercat Nov 13 '15 at 03:27
  • @supercat yes, i agree. It's just that given the likely implementation of printf i do not think that MSVC does anything special there, and that therefore for the arithmetic types nothing unexpected happens; a pragmatic opinion, not a legal one. And yes, that may change with new compilers and stdlib implementations with more inlining and more aggressive optimizations. That's why i linked to them. – Peter - Reinstate Monica Nov 13 '15 at 06:05
0

First of all, yes, in general this is undefined behavior according to the standard.

As to whether or not it is safe on your platform:
I can't promise you anything, but on Windows, a unsigned long is actually the same as unsigned int (even on x64), so - assuming your values are non-negative numbers (which is a reasonable assumption for version numbers) - this should be ok.

I'd still recommend that you start fixing that code. It is still a bug and especially if you ever want to run it on a different platform it might explode in your face at some time.

MikeMB
  • 20,029
  • 9
  • 57
  • 102
  • Yes but still `%d` is not for `unsigned int`. It is for `int`. Isn't it?? –  Nov 11 '15 at 11:24
  • @user200312: No, it isn't, but as your version numbers are probably not negative, that should (again no promises) be not a problem. – MikeMB Nov 11 '15 at 11:28
  • What do you mean it isn't? `%d` is for `int`. `%u` for `unsigned int`. Not? or you say if you pass positive unsigned int numbers to %d it is not UB? –  Nov 11 '15 at 11:32
  • @user200312: I meant: *"No, you are right, it isn't meant for `unsigned int`"* and as I stated in the answer, it is still UB, – MikeMB Nov 11 '15 at 11:39
  • @user200312: But I believe it is one of the cases, where UB results in predictable behavior on the specified platform. – MikeMB Nov 11 '15 at 12:04
  • @user200312: You have my answer you can accept it or ignore it - beyond that, what do you want to hear? That I give you my blessing to ignore that bug? I can't do that. It is your code base, your time and your money. I know nothing about your program, your skills and your company and I don't know the consequences if the code fails - the range might be somewhere between "people might die", over "we loose money/costumers" to "no one will notice". It is your job as an engineer (or CS-scientist) to make those decisions based on your knowledge or ask your boss what to do. – MikeMB Nov 12 '15 at 08:22
  • @user200312: With accept I meant accept or ignore my reasoning and conclusion, not accept the SO answer. – MikeMB Nov 12 '15 at 08:31
0

It used to be that in cases where the Standard would impose no requirements about the consequences of an action, but on some particular platform there would be a clear and obvious meaning, compiler writers were expected to generate code to yield that meaning. In cases where there were several plausible meanings, compiler writers were expected, absent a compelling reason to do otherwise, to write code which would, at worst, select among them in Unspecified fashion [e.g. on many platforms where "int" is 16 bits, given:

long mul(int a, int b) { return a*b; }

calling mul(5, 16384); might arbitrarily yield 81920 or 16384, but would be unlikely to do anything else]. This philosophy allowed C to thrive, since it meant that code which wouldn't need to run on obscure platforms could use features and guarantees which would work on everything else, but still allowed people to use C on platforms which couldn't honor those guarantees to accomplish tasks which didn't need them.

For some reason, that view has become unfashionable. If code doesn't care whether a function like the above returns 81920 or 16384 (e.g. because its purpose is to identify "potentially-interesting" objects, objects which are interesting won't cause overflow, and excluding 95% of uninteresting objects quickly is better than excluding 100% more slowly) being able to let the compiler return whichever result it can do more easily may allow for more efficient code generation than would be possible in a modern "Undefined Behavior must be avoided at all costs even if it makes code slower" compiler which requires a programmer to indicate which result must be returned, but there's no way a modern C program can leave that choice to the compiler.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • do you think I should necessarily fix that bug in my code? In general if there are many more such occurrences, fixing all of them is risky(see answer by Gilles) –  Nov 11 '15 at 22:40
  • @user200312: Is your code only going to need to run on compilers written by sane people? If someone had asked me twenty years ago whether certain things were safe, I would have said "yes" without reservation--they were only "undefined" to allow C to be implemented on weird kinds of hardware that are never going to be used for new development--but such things have become less safe with the passage of time. – supercat Nov 11 '15 at 23:27
  • I think you are missing the point about UB. The reason, why UB code can go haywire is because the compiler can make your average code faster if it doesn't have to guarantee "sane" behavior of buggy code. So having to write UB free code makes your program - on average - faster not slower. C has always valued speed above security and exploiting UB is just one example of that philosophy. – MikeMB Nov 12 '15 at 08:01
  • @MikeMB: That is a historical revisionist viewpoint. UB has historically made it possible to write programs that focused on things the programmer actually cared about. If certain aspects of a program's output were "don't care", being able to ignore overflow on platforms where overflow would do nothing worse than yield an indeterminate value (which may be "contagious" in computations but have no other effect) could often allow code to be much faster and more efficient than would otherwise be possible. The only reason C has thrived is that compilers could fill in the gaps... – supercat Nov 12 '15 at 14:53
  • ...where the language failed to specify necessary behaviors. If compiler writers in the 1980s had adopted modern attitudes, C would never have achieved anything near its level of popularity. – supercat Nov 12 '15 at 14:56
  • As a simple example, suppose code needs to find two items in an array such that a\*b==c without overflow [all variables "int"]. For quick searching purposes, using (unsigned)a\*b==(unsigned)c, (long)a\*b==(long)c, or (unsigned)a\*b==(unsigned long)c would likely be faster than testing whether a\*b overflows, but which of those alternatives is faster may depend upon context. A dialect where a*b==c is guaranteed to yield one of the above (in Unspecified fashion) will allow more efficient code on a wider variety of platforms than one where the programmer must explicitly pick one of the above. – supercat Nov 12 '15 at 16:10