메뉴
HN
Hacker News • 28일 전

실수로 LLM 메모리를 프로그램 분석으로 바꿔버린 이야기

IMP
6/10
핵심 요약

취약점 연구에 LLM 에이전트를 활용하던 중 장시간 조사 후 모델이 기존에 확립한 사실을 잊거나 이미 반박된 가정을 계속 믿는 문제를 겪은 저자는, 일반적인 메모리 시스템(대화 저장 + 임베딩 검색)의 한계를 발견했습니다. 저자는 이 문제가 사실 변경 시 영향받는 결론만 갱신하는 프로그램 분석과 유사하다는 점에 착안해, LLM을 위한 Datalog 엔진을 직접 구현하게 되었다고 설명합니다.

번역된 본문

실수로 LLM 메모리를 프로그램 분석으로 바꿔버린 이야기 [2026년 8월 28일] // Jordy Zomer // 19분 읽기

지난 몇 달간 나는 LLM 에이전트를 가지고 꽤 많이 실험해왔는데, 특히 취약점 연구 분야에서였다. LLM은 대규모 코드베이스를 탐색하고, 낯선 서브시스템을 설명하고, 잠재적 공격 표면을 탐색하는 데 놀랄 만큼 유능해지고 있다. 하지만 조사가 몇 시간씩 걸리기 시작하면 항상 같은 문제에 부딪혔다. 모델이 우리가 실제로 확립한 내용을 서서히 잊어버리는 것이다. 이미 제외된 접근법을 다시 제안하거나, 어떤 가정이 틀렸다는 사실을 잊어버리거나, 더 이상 유효하지 않은 관찰에 근거해 자신만만하게 추론을 계속하기도 했다. 당연히 LLM에게 뭔가 잘못됐다고 말한다고 해서 그것에 의존하던 모든 믿음을 버리는 것은 아니라는 점이 문제다 :)

처음에는 이런 환각(hallucination)을 줄이고 복잡한 취약점 연구에 LLM을 더 유용하게 만들고 싶어서 메모리 시스템을 연구하기 시작했다. 물론 LLM에 메모리를 부여하는 솔루션은 이미 많다. 보통은 과거 대화나 관찰 내용을 어딘가에 저장하고, 임베딩한 뒤, 모델이 다시 필요할 때 가장 관련성 높은 부분을 검색하는 방식이다. 이 방식은 그럭저럭 잘 작동하지만, 뭔가 마음에 걸리는 부분이 있었다.

취약점 연구 세션 동안 나는 모델이 우리가 한 말을 단순히 기억하는 것을 원하지 않는다. 우리가 현재 알고 있는 것을 유지하기를 원한다. 예를 들어 조사 중에 다음과 같은 사실을 확립했다고 해보자:

  • 공격자가 object_a를 제어한다
  • object_a는 object_b를 가리킨다
  • object_b는 커널 객체다

이로부터 우리는 공격자가 커널 객체를 제어할 수 있다고 결론 내릴 수 있다. 일반적인 메모리 시스템은 이 모든 관찰을 저장했다가 버그의 악용 가능성에 대해 질문할 때 다시 검색할 수 있다. 그러면 LLM이 같은 결론에 도달한다. 훌륭하다! 하지만 두 시간 뒤 LLDB에서 object_a가 실제로는 object_b를 가리키지 않으며, 이전 관찰이 잘못된 가정에 기반한 것이었다는 사실을 발견했다고 해보자. 그 시점에 메모리에는 다음과 같은 내용이 담기게 된다:

  • object_a는 object_b를 가리킨다
  • 공격자는 object_b를 제어할 수 있다
  • object_a는 실제로는 object_b를 가리키지 않는다

이제 이 메모리들의 일부를 검색해서 LLM이 어떤 결론이 여전히 유효한지 올바르게 판별해주기를 기도할 수밖에 없다.

이건 프로그램 분석과 비슷해 보인다

내가 평소에 하는 작업 상당수는 프로그램 분석(program analysis)과 관련되어 있다. 프로그램을 분석할 때 우리는 보통 프로그램에 관한 여러 사실(facts)과 이것들로부터 추가 사실을 도출하는 규칙(rules)을 갖는다. 예를 들어 다음을 안다고 해보자:

calls(foo, bar) calls(bar, baz)

어떤 함수가 다른 함수를 호출하고, 그 함수가 세 번째 함수에 도달할 수 있다면 첫 번째 함수도 세 번째 함수에 도달할 수 있다는 규칙을 정의할 수 있다. 결국 우리는 프로그램에서 도출할 수 있는 모든 것을 담은 고정점(fixed point)을 계산한다. 더 중요한 것은, 입력 사실 중 하나가 변경되면 전부 처음부터 다시 계산하는 대신 영향받는 결과만 업데이트하는 다양한 기법이 존재한다는 점이다.

이것이 바로 취약점 연구 중인 LLM에게 내가 원했던 것이다. 관찰이 변경되면 모델에게 대화 기록에서 전체 조사를 재구성하게 하고 모든 파급 효과를 운 좋게 알아채기를 바라는 게 아니라, 영향받는 결론들이 자동으로 무효화되기를 원했다. 이 관점에서 문제를 바라보다 보니, 우리가 왜 LLM에게 전체 상태를 반복해서 재구성하게 만들고 있는지 궁금해지기 시작했다. 그냥 상태를 유지하면 안 되는 걸까? 그리고 이렇게 해서 나는 어쩌다 보니 LLM용 Datalog 엔진을 작성하게 되었다 :)

Datalog 계속하기 전에 Datalog이 실제로 무엇인지 간단히 설명하는 것이 유용할 것이다. Datalog은 선언적 논리 프로그래밍 언어다. 무언가를 어떻게 계산해야 하는지 지시문을 작성하는 대신, 우리는 새로운 사실을 도출해낼 수 있는 사실과 규칙을 기술한다. 예를 들어...

원문 보기
원문 보기 (영어)
I accidentally turned LLM memory into program analysis [ 28 Aug 2026 ] // JORDY ZOMER // 19 MIN READ Over the past few months I have been playing around quite a bit with LLM agents, particularly for vulnerability research. They are becoming surprisingly good at navigating large codebases, explaining unfamiliar subsystems and helping explore potential attack surfaces. However, once an investigation starts taking a few hours, I kept running into the same problem: the model would slowly lose track of what we had actually established. It might suggest an approach that we had already ruled out, forget that an assumption turned out to be false, or confidently continue reasoning from an observation that was no longer valid. Obviously, telling an LLM that something is wrong does not necessarily mean that it will stop believing all of the things that depended on it :) I initially started looking into memory systems because I wanted to make LLMs more useful for complex vulnerability research and reduce this type of hallucination. There are of course already plenty of solutions for giving LLMs memory. Usually this involves storing old conversations or observations somewhere, embedding them, and then retrieving the most relevant pieces whenever the model needs them again. This works reasonably well, but there was something about it that bothered me. During a vulnerability research sesh, I don’t just want the model to remember what we said. I want it to maintain what we currently know . Imagine that during an investigation we establish the following: attacker controls object_a object_a points to object_b object_b is a kernel object From this, we may conclude that the attacker can control a kernel object. A normal memory system could store all of these observations and retrieve them again whenever we ask about the exploitability of the bug. The LLM then figures out the same conclusion. Great! However, suppose that two hours later we discover in LLDB that object_a does not actually point to object_b , and that our previous observation was based on a wrong assumption. At that point our memory may contain something like: object_a points to object_b attacker can control object_b object_a does not actually point to object_b Now we retrieve some subset of these memories and hope that the LLM correctly figures out which conclusions are still valid. This started to feel a little familiar to me. This looks like program analysis A lot of the work I normally do involves program analysis. When analysing a program, we usually have a bunch of facts about the program and some rules that derive additional facts from them. For example, imagine we know: calls(foo, bar) calls(bar, baz) We could define a rule stating that if one function calls another function, which itself can reach a third function, then the first function can reach the third function as well. Eventually we calculate a fixed point containing everything we can derive from the program. More importantly, if one of our input facts changes, there are plenty of techniques for updating only the affected results instead of rerunning everything from scratch. This is also exactly what I wanted from an LLM during vulnerability research. If an observation changes, I don’t want the model to reconstruct the entire investigation from a transcript and hopefully notice all of the consequences. I want the affected conclusions to become invalid automatically. When looking at the problem from this perspective, I started wondering why we were making the LLM reconstruct its entire state over and over again. What if we just maintained it? And this is how I somehow ended up writing a Datalog engine for LLMs :) Datalog Before we continue, it is probably useful to briefly explain what Datalog actually is. Datalog is a declarative logic programming language. Instead of writing instructions describing how something should be calculated, we describe facts and rules from which new facts can be derived. For example, we could store the following facts: controls ( attacker , object_a ). points_to ( object_a , object_b ). kernel_object ( object_b ). And then define the following rule: controls_kernel_object ( Attacker ) :- controls ( Attacker , ObjectA ), points_to ( ObjectA , ObjectB ), kernel_object ( ObjectB ). From our existing facts, the engine can therefore derive: controls_kernel_object ( attacker ). Nothing particularly exciting yet. However, suppose we later discover that: points_to ( object_a , object_b ). was incorrect. If controls_kernel_object(attacker) was derived from that fact, we know exactly which conclusion depends on the observation that just changed, and we can automatically invalidate it. This is considerably nicer than putting all of the old information into a prompt and asking an LLM to hopefully notice the same thing. Lemmalog This eventually turned into Lemmalog . The basic idea is that an LLM should not necessarily be responsible for maintaining its own knowledge. Instead, I split the problem into two parts. The LLM handles the fuzzy part: "LLDB shows that the freed object is later reused as the destination of the write." | v freed(object_a) reused_as(object_a, write_target) And Lemmalog handles the deterministic part: facts | v rules | v derived facts This means that the LLM is still responsible for understanding natural language, source code, debugger output and all the other messy information that appears during an investigation. LLMs happen to be quite good at this. But once that information has been converted into structured facts, we no longer need the model to repeatedly determine all of its consequences. The database can do that instead. Retractions One of the first interesting problems I ran into was removing facts. Adding facts to a Datalog database is relatively straightforward: add the new fact and evaluate any rules which may now produce additional results. Removing something is a little more annoying. Take the following example: a . b . c :- a . c :- b . Here c has two separate reasons for being true. If we remove a , we cannot simply remove c , because b still provides another derivation for it. However, if we remove both a and b , c should disappear as well. This turns out to be quite important during vulnerability research, because a conclusion may be supported by multiple observations. For example: candidate_3_is_exploitable may remain true even if one particular exploit primitive turns out not to work, because there is another independent path to the same result. So Lemmalog has to keep track of how facts were derived and update their support when something changes. Conveniently, this also gives us another useful property: we can ask why something is true. Why? Imagine we have been running an agent for a few hours while investigating something and it eventually concludes: candidate_3_is_exploitable That is nice, but I would also quite like to know why. Because Lemmalog already tracks the dependencies of derived facts, we can ask it for the provenance of a conclusion. For example, we may get something that conceptually looks like this: candidate_3_is_exploitable | +-- attacker_controls_pointer | | | +-- observation_41 | +-- pointer_reaches_target | +-- observation_57 +-- rule_12 If observation_41 later turns out to be incorrect, we know that this conclusion may no longer be valid, and because the database knows this as well, it can remove the affected conclusions automatically. This was originally mostly necessary to make incremental evaluation work correctly, but it turns out that being able to ask an AI agent why it believes something is quite useful as well :) It also addresses one of the more annoying failure modes I encountered with LLM-assisted research. Sometimes a model will confidently say something like: we already established that this pointer is attacker-controlled when that is not actually true. If a conclusion exists in Lemmalog, I can ask where it came from. If there