Skip to content

Memory Locking

Memory locking (mlock / VirtualLock) prevents sensitive data from being swapped to disk, and is one of the core defense lines of the SecureValue security system.

Why Memory Locking Is Needed

Under normal circumstances, the operating system swaps inactive memory pages to disk (swap file / page file). This means that even if you call ClearBytes to zero memory in your code, residual copies of sensitive data may still remain on disk.

Memory (RAM)                   Disk (Swap/Page File)
┌──────────────┐               ┌──────────────┐
│ API_KEY=xxx  │ ── swap ──→   │ API_KEY=xxx  │ ← Residual!
│              │               │ (even after   │
│              │ ←─ read ──    │  zeroing,     │
└──────────────┘               │  still here)  │
                               └──────────────┘

After enabling memory locking, the OS guarantees these memory pages will not be swapped out:

Memory (RAM)                   Disk (Swap/Page File)
┌──────────────┐               ┌──────────────┐
│ API_KEY=xxx  │ ╳ no swap ╳   │              │
│ 🔒 mlock     │               │ (no residual) │
└──────────────┘               └──────────────┘

Platform Support

PlatformSystem CallSupport
Linuxmlock(2) / munlock(2)
macOSmlock(2) / munlock(2)
FreeBSDmlock(2) / munlock(2)
WindowsVirtualLock / VirtualUnlock
wasm/naclN/A

Runtime detection:

go
if env.IsMemoryLockSupported() {
    fmt.Println("Current platform supports memory locking")
} else {
    fmt.Println("Current platform does not support memory locking (e.g., wasm)")
}

Permission Requirements

Memory locking involves system resource limits; different platforms require different permissions:

Linux

Requires the CAP_IPC_LOCK capability:

bash
# Method 1: Grant via setcap to the binary
sudo setcap cap_ipc_lock=ep ./myapp

# Method 2: Via systemd service
# /etc/systemd/system/myapp.service
[Service]
CapabilityBoundingSet=CAP_IPC_LOCK
AmbientCapabilities=CAP_IPC_LOCK

# Method 3: ulimit adjustment (RLIMIT_MEMLOCK)
# /etc/security/limits.conf
*    soft    memlock    unlimited
*    hard    memlock    unlimited

macOS / FreeBSD

Usually requires no special permissions, but is limited by ulimit -l (max locked memory).

Windows

Requires the SeLockMemoryPrivilege privilege:

Group Policy → Computer Configuration → Windows Settings → Security Settings →
Local Policies → User Rights Assignment → "Lock pages in memory"

WARNING

In default mode, memory locking failures are silently ignored — SecureValue still works, but the data is not locked. To ensure locking succeeds, use strict mode.

Basic Usage

Enabling Memory Locking

Call at application startup, before creating any SecureValue:

go
package main

import (
    "fmt"
    "github.com/cybergodev/env"
)

func main() {
    // Check platform support
    if !env.IsMemoryLockSupported() {
        fmt.Println("Warning: current platform does not support memory locking")
    }

    // Globally enable memory locking
    env.SetMemoryLockEnabled(true)

    // Load configuration
    if err := env.Load(".env"); err != nil {
        panic(err)
    }

    // All subsequent SecureValues will attempt memory locking
    secret := env.GetSecure("API_KEY")
    if secret != nil {
        defer secret.Release()
        fmt.Println(secret.Masked()) // [SECURE:32 bytes locked]
    }
}

Checking Lock Status

go
sv := env.GetSecure("DB_PASSWORD")
defer sv.Release()

// Check if locked
if sv.IsMemoryLocked() {
    fmt.Println("Memory is locked, will not be swapped to disk")
} else {
    fmt.Println("Memory is not locked")
}

// Check locking error (if any)
if err := sv.MemoryLockError(); err != nil {
    fmt.Printf("Locking failed: %v\n", err)
}

Strict Mode

In default mode, locking failures are silently ignored. Strict mode makes failures observable:

Enabling Strict Mode

go
env.SetMemoryLockEnabled(true)
env.SetMemoryLockStrict(true)

// Subsequent locking failures will be output to the standard logger:
// env: memory lock failed in strict mode: operation not permitted

Explicit Error Handling

Use NewSecureValueStrict to get the locking error at creation time:

go
env.SetMemoryLockEnabled(true)
env.SetMemoryLockStrict(true)

sv, err := env.NewSecureValueStrict("my-api-key")
if err != nil {
    // Memory locking failed
    // SecureValue is still valid, but data is not protected by locking
    log.Printf("Security warning: memory locking failed: %v", err)
}
defer sv.Release()

// Normal usage
fmt.Println(sv.Masked())

TIP

In strict mode, locking failure triggers the onStrictLockFailure callback (outputs to stderr by default). SecureValue itself always remains valid — strict mode just makes locking failures observable, rather than preventing usage.

Masked Output and Lock Status

The Masked() method includes lock status information in the output:

go
env.SetMemoryLockEnabled(true)

sv := env.GetSecure("API_KEY")
defer sv.Release()

fmt.Println(sv.Masked())
// Lock succeeded:    [SECURE:32 bytes locked]
// Lock failed:       [SECURE:32 bytes lock-failed]
// Locking disabled:  [SECURE:32 bytes]
// Closed:            [CLOSED]

Complete Production Example

go
package main

import (
    "log"
    "os"

    "github.com/cybergodev/env"
)

func main() {
    // ── Initialize security configuration ──

    if env.IsMemoryLockSupported() {
        env.SetMemoryLockEnabled(true)
        env.SetMemoryLockStrict(true) // Enable strict mode in production
        log.Println("Memory locking enabled (strict mode)")
    } else {
        log.Println("Warning: platform does not support memory locking")
    }

    // ── Load configuration ──

    cfg := env.ProductionConfig()
    cfg.RequiredKeys = []string{"DB_PASSWORD", "API_KEY"}
    cfg.AutoApply = true

    loader, err := env.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer loader.Close()

    if err := loader.LoadFiles(".env"); err != nil {
        log.Fatal(err)
    }

    // ── Securely access sensitive values ──

    dbPassword := loader.GetSecure("DB_PASSWORD")
    if dbPassword == nil {
        log.Fatal("DB_PASSWORD not found")
    }
    defer dbPassword.Release()

    // Check lock status
    if !dbPassword.IsMemoryLocked() {
        log.Printf("Security warning: DB_PASSWORD not locked")
        if err := dbPassword.MemoryLockError(); err != nil {
            log.Printf("Reason: %v", err)
        }
    }

    // Only get plaintext when needed
    password := dbPassword.Reveal()
    _ = password // Used for database connection, etc.

    // Safe logging (no plaintext leakage)
    log.Printf("Database password: %s", dbPassword.Masked())
    // Output: Database password: [SECURE:12 bytes locked]

    _ = os.Stdout
}

Best Practices

Release Promptly

Locking memory increases memory pressure (cannot be swapped out), so release immediately after use:

go
// ✅ Recommended: release as soon as done
sv := env.GetSecure("API_KEY")
defer sv.Release()
value := sv.Reveal()
// Use value...
// defer triggers auto-zeroing + unlocking + returning to object pool

// ❌ Avoid: holding for long periods
var globalSecret *env.SecureValue // Not recommended

Keep Small and Short-lived

Locking large blocks of memory affects system performance. Each SecureValue should only store the necessary sensitive value (password, key, token), not entire configuration blocks.

Prefer Release Over Close

go
sv := env.GetSecure("TOKEN")

// ✅ Release: zeroing + unlocking + returning to object pool (recommended)
defer sv.Release()

// Close also works, but doesn't return to the object pool
// defer sv.Close()

Troubleshooting

ProblemPossible CauseSolution
lock-failed in Masked outputInsufficient permissionsConfigure CAP_IPC_LOCK (Linux) or SeLockMemoryPrivilege (Windows)
Strict mode log spamLocking failure when creating many SecureValuesCheck system RLIMIT_MEMLOCK limits, or use non-strict mode
IsMemoryLockSupported() returns falsewasm/nacl platformThese platforms don't support memory locking; use other security measures (e.g., encrypted storage)
Increased memory usageLocked pages cannot be swapped outReduce SecureValue hold time, Release promptly