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