Meta description: Understand how memory exhaustion, memory corruption, and memory leaks can affect Go services, with simple real-world examples and practical protections.
Suggested slug: go-memory-exhaustion-corruption-leaks-security
Memory problems can give attackers opportunities, including in Go. However, running out of memory does not automatically let someone take control of your server. Different memory problems have different consequences.
The scenarios below are illustrative examples, not evidence of an attack on your application. The Go code demonstrates memory retention; it is not a complete server.
What Is the Difference?
| Problem | Simple meaning | Possible attacker opportunity |
|---|---|---|
| Memory exhaustion | The application runs out of available or permitted memory | Make the service unavailable |
| Memory corruption | Software incorrectly reads or writes memory | Crash the service or, with some exploitable bugs, execute malicious code |
| Memory leak | Unneeded memory remains retained | Repeatedly trigger memory growth until the service fails |
1. Memory Exhaustion: Filling Every Seat
A real-world analogy
Imagine a restaurant with 100 seats. Someone occupies every seat, leaving no room for genuine customers. The restaurant still exists, but it cannot serve new visitors.
Memory exhaustion works similarly: the application consumes the memory it needs to keep serving users.
Website scenario: too many PDF jobs
Your document service accepts uploads and extracts text from PDFs. Each extraction requires memory. If it accepts unlimited simultaneous jobs, repeated expensive requests can overload it.
| Step | What happens |
|---|---|
| 1 | Many documents arrive |
| 2 | The service starts too many extraction jobs together |
| 3 | Memory consumption grows beyond available resources |
| 4 | Workers fail or the operating system terminates processes |
| 5 | Genuine users cannot process their documents |
Illustrative result:
Document processing unavailable. Please retry later.
This can become a denial-of-service attack. It can also happen accidentally during a legitimate traffic spike. The behavior depends on the runtime, operating system, and configured limits.
Does the attacker control the server?
Not from exhaustion alone. The usual effect is disruption. Executing attacker-controlled code requires a separate exploitable weakness.
How to reduce the risk
- Limit request and individual file sizes.
- Bound simultaneous processing jobs.
- Set a maximum queue length and expire old jobs.
- Apply processing deadlines and worker memory limits.
- Stream files rather than loading unlimited input into RAM.
- Leave sufficient memory for the operating system and other services.
For example, allow two extraction workers and queue additional work within a fixed capacity. A limit on each worker must also fit the total host budget.
2. Memory Corruption: Using the Wrong Locker
A real-world analogy
Imagine a locker is emptied and assigned to a new person. An employee still uses its old ownership record and accesses the wrong person's belongings.
Software can have a similar problem when it uses a reference to memory after that memory was freed. This is called use-after-free, one form of invalid memory access. Other corruption bugs involve writing outside an allocated region.
Website scenario: a vulnerable native parser
A document service calls a native library to process an uploaded file. That library contains a memory-management bug. A specially crafted document reaches the vulnerable code path.
| Step | What happens |
|---|---|
| 1 | The service receives a crafted document |
| 2 | A vulnerable parser processes it |
| 3 | The parser incorrectly accesses memory |
| 4 | The worker may crash |
| 5 | If the bug is exploitable, an attacker may influence execution |
Possible outcomes:
Outcome A: the parser worker crashes.
Outcome B: a particular exploitable bug permits malicious code execution.
A crash does not prove code execution. Exploitability depends on the bug, build, protections, and environment. An attack also runs within the permissions of the compromised process unless another weakness allows further privilege escalation.
How does this apply to Go?
Ordinary safe Go code benefits from garbage collection and bounds checks. It does not normally let application code perform arbitrary pointer arithmetic or manually free objects.
However, Go applications can interact with:
- Code using the
unsafepackage. - C libraries through
cgo. - Native document, image, or other processing components.
- Runtime or compiler implementation bugs.
Incorrect unsafe operations or a vulnerable native library can weaken the protections provided by ordinary Go code. Their presence alone does not prove a vulnerability.
How to reduce the risk
- Patch the affected runtime and libraries.
- Minimize unnecessary unsafe or native integrations.
- Review security advisories for the exact deployed components.
- Process untrusted files in isolated workers.
- Give workers only the permissions and credentials they need.
- Avoid unnecessary network access and sensitive filesystem mounts.
Isolation reduces potential damage; it does not repair the underlying parser bug.
3. Memory Leaks: Never Clearing Used Tables
A real-world analogy
Imagine restaurant staff never clear tables after customers finish. Over time, every table stays occupied by things nobody needs.
An application has a similar issue when it retains data that is no longer useful.
Go example: retaining every upload
var history [][]byte
func rememberUpload(data []byte) {
history = append(history, data)
}
Explanation:
| Code | Meaning |
|---|---|
[][]byte |
A slice containing byte slices |
history |
A global variable that remains reachable |
append(history, data) |
Keeps a reference to another uploaded byte slice |
Go's garbage collector cannot reclaim objects that remain reachable. If history continues referencing every upload, those bytes stay retained.
This is often called a logical memory leak: the data is still accessible to the program but is no longer needed. If called by concurrent request handlers, this example also needs synchronization; it is intentionally simplified.
Website scenario: repeated calls accelerate retention
An endpoint unnecessarily remembers every document. Repeated requests make the retained data grow faster.
Illustrative retained payload sizes, assuming separate 1 MiB allocations per upload:
| Uploads retained | Approximate payload memory |
|---|---|
| 10 | 10 MiB |
| 100 | 100 MiB |
| 1,000 | 1,000 MiB |
Actual process memory also includes slice metadata, allocation overhead, runtime memory, and other work. Growth can eventually cause exhaustion and service failure.
Better example: retain bounded metadata
If you only need recent upload names, do not retain full document contents:
package main
import "fmt"
const maxHistory = 3
func rememberName(history []string, name string) []string {
history = append(history, name)
if len(history) > maxHistory {
// Copy the retained names into a new slice.
history = append([]string(nil), history[len(history)-maxHistory:]...)
}
return history
}
func main() {
var history []string
for _, name := range []string{"a.pdf", "b.pdf", "c.pdf", "d.pdf"} {
history = rememberName(history, name)
}
fmt.Println(history)
}
Expected output:
[b.pdf c.pdf d.pdf]
This keeps only three names. A real service must also bound name length and protect shared state from concurrent access. Prefer durable storage when records need to be preserved.
How to reduce the risk
- Bound caches and retained history.
- Set expiry times and actually remove expired entries.
- Release references to large objects when finished.
- Close resources such as files and response bodies appropriately.
- Monitor memory over time and inspect heap profiles when growth is unexplained.
- Limit requests to endpoints that perform expensive or state-growing work.
Clearing references makes objects eligible for garbage collection; it does not guarantee immediate return of memory to the operating system.
What Could Happen to a Real Business?
| Service | Example weakness | Business impact |
|---|---|---|
| Contract upload | Unlimited parsing jobs | Users cannot analyze contracts |
| Booking API | Unbounded retained request history | Booking requests slow down or fail |
| Image processor | Exploitable native-library bug | Worker compromise; accessible data may be at risk |
| Shared host | Total worker memory exceeds host capacity | Other applications may also be interrupted |
A single bad file should ideally fail one isolated job instead of taking down every website on the server.
Does Switching to Go Solve Everything?
No. Go helps prevent many common invalid-memory operations in ordinary application code. It does not automatically prevent excessive allocations, unbounded caches, unsafe integrations, application authorization bugs, or vulnerable dependencies.
Use multiple layers together:
- Patched runtimes and dependencies.
- Input-size, time, concurrency, and queue limits.
- Streaming and bounded data retention.
- Isolated workers with resource limits.
- Non-root users and restricted credentials.
- Monitoring and controlled recovery.
- Staging tests for malformed input and overload.
Frequently Asked Questions
Can a hacker take control just because memory is full?
Usually not. Exhaustion generally threatens availability. Code execution requires an exploitable bug or another weakness.
Does garbage collection prevent every leak?
No. If your program still references unnecessary data, garbage collection normally keeps it alive.
Does try-catch or error recovery stop all memory attacks?
No. Normal error handling cannot reliably recover from a process killed by the operating system or all forms of native corruption.
Does a worker restart fix a memory leak?
It clears that process's memory, but the leak can recur. Fix the retention problem and use restarts as a controlled operational measure.
Which problem is most concerning?
All matter. Exhaustion and leaks can cause serious outages. Exploitable corruption can additionally threaten data confidentiality and process control.


Top comments (0)