Skip to content
VLSI Mentor

C Part II · Module 25 · Debugging DPI-C and Simulator Integration

Pointer and Lifetime Bugs Across the Boundary

Four lifetime faults, measured against every tool available. A sanitizer reports one of them by default, a second only with an option that is off, and the remaining two it can never report — because they are not memory errors at all. A static buffer silently reused and a length that outlives its buffer are both perfectly legal C doing exactly what it was told, and both look like careful code. Tool coverage here runs backwards from how likely each fault is to be written.

Expert23 min readCPart IIVerificationDPI-C

Part II · Module 25 · Chapter 25.5 · Pointer and Lifetime Bugs Across the Boundary

1. Where the Owner Is Not Visible

Module 9 established storage duration and lifetime in ordinary C, where the owner is usually visible: you can see the malloc, the block the automatic object lives in, or the static keyword.

Across this boundary the owner is often not in the code you are reading. A pointer arrives from the simulator and nothing about its type says who allocated it, how long it lasts, or whether you may keep it — Chapter 20.8 made that the central question for a string, and the same applies to every pointer that crosses.

So the diagnostic question in this chapter is not "is this pointer valid?" but "whose is it, and until when?"

2. Three Possible Owners

For any pointer at this boundary there are exactly three answers, and the bug is almost always that nobody wrote down which one applies.

The simulator's — valid for the duration of the call. Read it now; copy anything you need afterwards. From the LRM, this is the default for arguments, and Chapters 20.8 and 21.7 measured what keeping one costs.

The model's — you allocated it, so it is valid until you free it, and somebody must. Chapter 20.10 §4's rule.

A static — valid forever, and shared by everyone. Chapter 22.7 measured what that means the moment there are two of something.

A comment naming which of the three applies is the whole defence, because none of the three is distinguishable from the others by looking at the C.

3. Four Faults, and What Each Tool Sees

A static buffer reused. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  first result now reads "code=9"; p == q: yes

The first result changed into the second, through a pointer nothing touched. Not undefined behaviour — every access is to live storage with a perfectly good lifetime. The compiler is silent because nothing is wrong at the language level; the sanitizer is silent because there is no memory error to find.

A length that outlives its buffer. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  the buffer is gone; len still reads 16

Also not undefined behaviour. len is an ordinary integer holding a correct value that has simply stopped describing anything — Chapter 21.7 §5's asymmetric decay, now as a debugging problem. No tool can object to a correct integer.

Heap use after free. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  compiler at -O2: 0 warnings
  ==15588==ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000d0
  READ of size 4 at 0x6020000000d0 thread T0
      #0 0x0001028348b8 in main uaf.c:4

Undefined behaviour, invisible to the compiler, and reported by the sanitizer by default, naming the file and line. This is the good case.

Stack use after return, laundered through one helper function. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  compile warnings at -O2: 0
  ASan default           : exit 0, reports: 0
  with the option on     : reports: 1
  ==15614==ERROR: AddressSanitizer: stack-use-after-return

Undefined behaviour, and reported by nothing until detect_stack_use_after_return=1 is set — which Chapter 20.8 §7 measured and which remains off by default.

Four lifetime faults and what each tool can see of them. A static buffer reused by a second call is not undefined behaviour at all: measured, the first result silently became the second and both pointers were equal, with no compiler warning and nothing for a sanitizer to find. A length that outlives the buffer it describes is also not undefined behaviour: measured, the length still read 16 after the buffer had gone, and no tool can object to a correct integer. A heap use-after-free is undefined behaviour and the compiler says nothing at dash O 2, but AddressSanitizer reports it by default, naming the file and line. A stack use-after-return laundered through one helper function is also undefined behaviour, and here the compiler says nothing and AddressSanitizer reports nothing either until the stack-use-after-return option is switched on, at which point it reports it. So the two faults no tool can ever see are precisely the two that are not undefined behaviour, and they are the two most likely to be written.The faultstorage that was validand then was notIs it undefinedbehaviour?Compilermeasured at -O2SanitizermeasuredA static bufferreuseda second call overwritesthe first resultnoevery access is to livestoragesilentnothing is wrong to seesilentmeasured: the resultbecame code=9 and thepointers were equalA length outlivingits bufferthe pair separatednoit is an ordinary integersilenta correct valuesilentmeasured: still reads 16,describing nothingHeap use after freethe storage was returnedyesreading freed storagesilentmeasured: 0 warningsreports it, bydefaultmeasured: names the fileand lineStack use afterreturnlaundered through onehelperyesthe object's lifetimeendedsilentmeasured: 0 warnings, onehop defeats itsilent until anoption is setmeasured: 0 reports bydefault, 1 with theoptionThe two faults no tool can ever see are the two that are not undefined behaviourmeasured — and they are the ones most likely to be written, because a static buffer and a separately-stored lengthboth look like ordinary, careful code12
Figure 1 — four lifetime faults against every tool available. Read the two right-hand columns downward: the compiler is silent for all four, and the sanitizer reports one by default, one only with an option that is off, and two never. The two it can never report are the top two, and the reason is in the second column — they are not undefined behaviour. A static buffer reused and a length separated from its buffer are both perfectly legal C doing what it was told, which is precisely why they are the two most likely to be written and the two that look most like careful code.

4. The Finding: Coverage Runs Backwards From Likelihood

Read the figure's last two columns downward and the pattern is uncomfortable.

The two faults no tool can ever report are the two that are not undefined behaviour — and they are the two most likely to be written, because both look like ordinary careful code. A static char buf[32] is a reasonable way to return a string. Storing a length in a struct member while the buffer lives elsewhere is a reasonable way to organise state.

The two that tooling can reach are the two that look wrong when you read them. Returning the address of a local, or using a pointer after free — those are things a reviewer flags.

So the tool coverage is inverse to the likelihood, and the practical consequence is that a clean sanitizer run says nothing about the two faults you are most likely to have.

What covers the top two is not a tool but a question, asked of every pointer: §2's three owners, written down.

5. The Procedure

Given a symptom of the form "the data was right and then it was not", in order:

One — is it undefined behaviour at all? If a sanitizer reports something, you have a file and a line and the work is nearly done. If it reports nothing, that is not evidence of correctness — it narrows you to the top two rows.

Two — turn on the option. ASAN_OPTIONS=detect_stack_use_after_return=1. Measured, that converts row four from silence to a report.

Three — ask who owns each pointer. Simulator, model, or static. Any pointer whose answer is not written down is a candidate.

Four — look for sharing. A static in a model is correct until there are two of something, which Chapter 22.7 measured at 0x52 for one stream and a wrong answer for two. The test is two instances, not more transactions.

Five — check that pointers and lengths travel together. Chapter 21.7 §5: the pointer may be detectable when stale, the length never is, so keep them in one object.

6. The Misconception: We Run Under a Sanitizer

"The model is built with AddressSanitizer in CI, so lifetime bugs are covered."

Measured false twice over.

Two of the four faults are not memory errors at all. A reused static buffer and a detached length are perfectly legal C. There is no option, no tool and no future version that will report them, because nothing invalid is happening.

And one of the other two is off by default. Measured: the laundered stack-use-after-return produced 0 reports under a default sanitizer build and 1 with detect_stack_use_after_return=1.

So a clean sanitizer run covers one of the four, and Chapter 21.8 made the general version of this point with a whole catalogue of array faults producing zero reports.

Two things follow, and the first is cheap:

  • Turn the option on. It costs a line in the run script and converts one of the four from silent to reported.
  • Do not read a clean run as evidence about ownership. It is evidence about memory validity, which is a different claim.

The accurate position:

A sanitizer answers "did this program touch memory it had no right to touch?" — and two of the four faults in this chapter never do. What covers those is a written answer to "whose storage is this, and until when", for every pointer that crosses.

7. What You Should Be Able to Say Now

  1. The owner is not visible in the code at this boundary — Module 9's reasoning with the usual evidence removed.
  2. Three possible owners: the simulator's (valid for the call) · the model's (until you free it) · a static (forever, and shared).
  3. A reused static buffer is not undefined behaviour — measured, the first result became "code=9" and p == q.
  4. A length outliving its buffer is not undefined behaviour either — measured, len still reads 16, describing nothing.
  5. Heap use after free: 0 compiler warnings at -O2, and ASan reports it by default with a file and line.
  6. Stack use after return, laundered: 0 warnings, 0 ASan reports by default, 1 with detect_stack_use_after_return=1.
  7. The finding: the two faults no tool can see are the two that are not UB — and the two most likely to be written.
  8. Tool coverage is inverse to likelihood here.
  9. Procedure: is it UB · turn the option on · name the owner of every pointer · look for sharing · keep pointer and length together.
  10. The symptom: the data was right and then it was not, without the input changing.
  11. The first question is not about pointers: does the failure survive running that transaction on its own?
  12. A clean sanitizer run covers one of the four and is evidence about memory validity, not ownership.

8. Exercises

  1. Return a string from a static buffer and call the function twice, keeping both results. What does the first one read, and which tool reported it?
  2. Store a length in one object and its buffer in another, then let the buffer go out of scope. What does the length read?
  3. Build a heap use-after-free and run it under a default sanitizer. What does it name?
  4. Launder a stack-use-after-return through one helper. How many reports do you get with and without the option?
  5. For each pointer crossing a boundary in code you have, write down which of §2's three owners applies. How many were already written down?
  6. Reproduce Chapter 22.7's two-stream test on a model with a static. What is the smallest test that distinguishes it from a correct one?
  7. A bug disappears when the failing transaction is run on its own. What does that rule in, and what does it rule out?
  8. §6 says a clean sanitizer run is evidence about memory validity. Write the one-sentence version you would put in a review comment.

9. Summary

  • The owner is invisible at this boundary — Module 9's reasoning, without the usual evidence.
  • Three owners: simulator (for the call) · model (until freed) · static (forever, shared).
  • Measured, not UB: a reused static buffer ("code=9", p == q) and a detached length (still 16).
  • Measured, UB: heap use-after-free — 0 warnings, ASan by default, file and line.
  • Measured, UB but silent: stack use-after-return — 0 warnings, 0 reports, 1 with the option.
  • Coverage runs backwards from likelihood — the invisible two are the ones that look like careful code.
  • Procedure: UB or not · set the option · name each owner · look for sharing · pointer and length together.
  • The symptom: right, then wrong, with the input unchanged — so ask whether it survives in isolation.
  • A clean sanitizer run covers one of four.

Every fault so far has needed a running simulation to show itself. The next two need rather less than that — they happen before any simulated time exists, and the messages they produce are about files and symbols rather than about values.

Where this fits

Part of the C Programming curriculum.