Skip to content

Fix Apron handling of escaped locals - #2100

Open
michael-schwarz wants to merge 3 commits into
masterfrom
issue_2091
Open

Fix Apron handling of escaped locals#2100
michael-schwarz wants to merge 3 commits into
masterfrom
issue_2091

Conversation

@michael-schwarz

Copy link
Copy Markdown
Member

Fix Apron’s handling of locals whose addresses escape through global pointers.

In single-threaded execution, escaped locals were read using their local relational variable but written using a separate global relational variable. Calls could also omit escaped locals from callee states and restore stale pre-call relations afterward. This allowed Apron to retain relations invalidated by indirect writes.

This change:

  • consistently updates escaped locals using their local relational variables before entering multithreaded mode;
  • retains escaped locals in callee relational states;
  • avoids restoring stale pre-call relations for escaped locals;
  • adds regressions for both entirely single-threaded execution and entering multithreaded mode inside the callee.

Tested with:

  • opam exec -- scripts/update_suite.rb group apron3 -q
  • opam exec -- dune build @runaprontest --display quiet

Closes #2091.

@michael-schwarz
michael-schwarz requested a review from sim642 August 14, 2026 09:15
@michael-schwarz
michael-schwarz marked this pull request as ready for review August 14, 2026 09:29
@michael-schwarz michael-schwarz added bug unsound relational Relational analyses (Apron, affeq, lin2var) labels Aug 14, 2026
@jprotopopov-ut

Copy link
Copy Markdown
Contributor

Locally, the change appears fine, although to me it looks like a whack-a-mole fix. For what it's worth, I tried to prompt AI to poke holes in the code for counterexamples, and (before the session got derailed by cybersecurity guardrails) it got me the following adjacent case:

// PARAM: --set ana.activated[+] apron --enable ana.sv-comp.functions
#include <goblint.h>
#include <string.h>

static int *target;

static void modify(void) {
  *target = __VERIFIER_nondet_int();
}

int main(void) {
  int left = __VERIFIER_nondet_int();
  int right = left;
  int *source = &right;
  memcpy(&target, &source, sizeof(target));

  modify();
  __goblint_check(left == right); // UNKNOWN!
}

which crashes Goblint with

Fatal error: exception Apron.Manager.Error
 {  exn = Exc_invalid_argument; funid = Funid_assign_texpr_array;
  msg = unknown variable right#671 in the environment; }

Given that the failure is different from the original one, I am approving this pull request.

@michael-schwarz

Copy link
Copy Markdown
Member Author

Although to me it looks like a whack-a-mole fix.

I took a look at your example, thank you! I think the issue there is fundamentally different. The problem is that the escape analysis does not consider the escape via memcpy. This also leads to an unsoundness in the base issue.

// PARAM: --enable ana.sv-comp.functions
#include <goblint.h>
#include <pthread.h>
#include <string.h>

static int *target;

static void *modify(void *arg) {
  *target = 7;
  return NULL;
}

int main(void) {
  int right = 42;
  int *source = &right;
  memcpy(&target, &source, sizeof(target));

  pthread_t thread;
  pthread_create(&thread, NULL, modify, NULL);
  pthread_join(thread, NULL);

  __goblint_check(right == 42); // Incorrectly succeeds
}

Given that there it is an issue with threadEscape affecting different analyses, I'll move it to a separate issue.

Comment on lines 425 to 428
let new_rel = RD.keep_filter st.rel (fun var ->
match RV.find_metadata var with
| Some (Local v) when ThreadEscape.has_escaped ask v -> false (* escaped locals may be modified through globals *)
| Some (Local _) when not (belongs_to_fundec fundec var || any_local_reachable) -> true (* keep caller locals, provided they were not passed to the function *)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this is different from the make_callee_rel change above?
Previously, both used the exact same condition (negated) to either remove or keep the corresponding locals.
Now, they are no longer symmetric like that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not semantically different here, it's just that I broke the symmetry in formatting. I have restored the symmetry now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought so too that it's just different formatting, but that's not true.
For an escaped local:

  • Before the symmetry fix, this immediately goes to false.
  • After the symmetry fix, all the negated escape condition does is not allow it to go to that true. But there are other Local cases below with different conditions that may still return true.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, I hadn't thought about this. Looking at it, the case where this can now actually go to true is

| Some ((Local v | Global v)) when not (TaintPartialContexts.VS.mem v tainted_vars) -> true (* keep locals and globals, which have not been touched by the call *)

This is correct: If the escaped variable was definitely not modified in the callee, we can preserve the caller state.

@michael-schwarz
michael-schwarz requested a review from sim642 August 18, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug relational Relational analyses (Apron, affeq, lin2var) unsound

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apron unsoundly retains relations for locals modified through an escaped pointer

3 participants