analysis

package
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

analysis

Static enforcement of symbol-level import restrictions for the rshell interpreter.

Purpose

rshell is a sandboxed shell. Any Go package it imports is a potential escape vector. This package maintains explicit allowlists of every importpath.Symbol that each subsystem is permitted to use, and enforces those lists via Go AST analysis run as tests.

If a symbol is not on the list, the code cannot compile it through CI — adding new capabilities requires a deliberate, reviewed allowlist entry.

Permanently Banned Packages

Some packages may never be imported regardless of the symbol, declared in symbols_common.go:

Allowlists

Each subsystem has its own allowlist file:

File Governs
symbols_builtins.go builtins/ — builtin command implementations
symbols_interp.go interp/ — interpreter core
symbols_allowedpaths.go allowedpaths/ — filesystem sandbox
symbols_internal.go builtins/internal/ — shared internal helpers
Two-layer system for builtins

Builtins use two complementary lists:

  • builtinAllowedSymbols — global ceiling: every symbol any builtin may use.
  • builtinPerCommandSymbols — per-command sublists: each builtin directory (cat/, grep/, …) declares only the symbols it actually needs.

Every symbol in a per-command list must be present in the global ceiling. Every symbol in the global ceiling must appear in at least one per-command list. This keeps each builtin's surface area minimal and auditable in isolation.

The same two-layer pattern applies to builtins/internal/ via internalAllowedSymbols and internalPerPackageSymbols.

Safety Legend

Each allowlist entry carries an inline comment prefixed with a safety emoji:

Emoji Meaning Examples
🟢 Pure — no side effects (pure functions, constants, types, interfaces) strings.Split, fmt.Sprintf, io.Reader, AST types
🟠 Read-only I/O — reads from filesystem, OS state, or kernel; or delegates writes to an io.Writer os.Open, os.ReadFile, time.Now, net.Interfaces, syscall.Getsid
🔴 Privileged — network I/O, unsafe memory, or native code loading net.DefaultResolver, pro-bing.NewPinger, unsafe.Pointer, syscall.MustLoadDLL

Enforcement

The tests in this package use Go's go/parser and go/ast to walk source files and verify:

  1. No permanently banned package is imported.
  2. Every imported package is in the allowlist.
  3. Every pkg.Symbol reference is explicitly listed.
  4. Every symbol in an allowlist is actually used (no dead entries).
  5. Every builtin subdirectory has a per-command entry.

Verification tests additionally inject banned imports or unlisted symbols into a temporary copy of the repo and assert the checker catches them.

Structural Rules

In addition to symbol-level allowlist checking, the package enforces structural rules — code patterns that must (or must not) appear together in the same function scope. These are checked by checkFileScannerBuffer and checkFileOpenFileClose in structural.go and are applied automatically to every file that passes through checkAllowedSymbols.

Both rules are also exposed as standalone go/analysis analyzers (ScannerBufferAnalyzer, OpenFileCloseAnalyzer) that can be registered with go vet or gopls.

Rule 1 — bufio.NewScanner must call .Buffer()

Why: bufio.Scanner has a fixed default buffer of 64 KiB. Any line longer than that causes Scanner.Scan() to return false and Scanner.Err() to return bufio.ErrTooLong. In a shell that must handle arbitrary user input this is a reliability and DoS risk — a single long line silently truncates or aborts processing.

What is checked: Every variable assigned from bufio.NewScanner(...) must have .Buffer(...) called on it within the same function body. Nested function literals (func() { ... }) are treated as independent scopes.

Compliant:

sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 4096), maxLineBytes)
for sc.Scan() { ... }

Violation:

sc := bufio.NewScanner(r)  // flagged: no sc.Buffer() call
for sc.Scan() { ... }
Rule 2 — callCtx.OpenFile results must be closed

Why: Every open file descriptor consumes a kernel resource. Over repeated script executions, unclosed handles exhaust the process file-descriptor limit and cause all subsequent I/O to fail.

What is checked: Every variable assigned from a .OpenFile(...) call (any receiver — the check matches the method name, not the receiver type) must have .Close() called on it — directly or via defer — within the same function body. The checker also tracks hand-off assignments: if f is reassigned to rc and rc.Close() is called, f is considered closed.

Compliant — direct close:

f, err := callCtx.OpenFile(ctx, path, os.O_RDONLY, 0)
if err != nil { return err }
defer f.Close()

Compliant — hand-off pattern:

f, err := callCtx.OpenFile(ctx, path, os.O_RDONLY, 0)
if err != nil { return err }
rc = f          // hand off to rc
defer rc.Close() // closes f transitively

Compliant — return ownership transfer:

func openHelper(callCtx cc) (io.ReadCloser, error) {
    f, err := callCtx.OpenFile(ctx, path, os.O_RDONLY, 0)
    if err != nil { return nil, err }
    return f, nil  // caller is responsible for closing
}

Violation:

f, err := callCtx.OpenFile(ctx, path, os.O_RDONLY, 0)
if err != nil { return err }
_ = f  // flagged: f is never closed

Adding a New Symbol

  1. Add a line to the appropriate allowlist (and the per-command sublist if it's a builtin).
  2. Prefix the comment with the correct safety emoji.
  3. Run go test ./analysis/ to verify the entry is valid and used.

Documentation

Overview

Package analysis provides a go/analysis.Analyzer that enforces symbol-level import restrictions on Go source files.

The analyzer checks that every imported symbol is in a given allowlist, that no permanently banned packages are imported, and that every symbol in the allowlist is actually used. It reports violations with file:line:col diagnostics.

Index

Constants

This section is empty.

Variables

View Source
var OpenFileCloseAnalyzer = &analysis.Analyzer{
	Name: "openfileclose",
	Doc:  "checks that callCtx.OpenFile results are always closed within the same function",
	Run:  runOpenFileClose,
}

OpenFileCloseAnalyzer checks that every callCtx.OpenFile call result that is assigned to a variable has a corresponding .Close() call (direct or via defer) within the same function scope. Unclosed file handles exhaust file descriptors over repeated script executions.

View Source
var ScannerBufferAnalyzer = &analysis.Analyzer{
	Name: "scannerbuffer",
	Doc:  "checks that bufio.NewScanner results have Buffer() called to set a bounded read buffer",
	Run:  runScannerBuffer,
}

ScannerBufferAnalyzer checks that every bufio.NewScanner call in the analyzed package has a corresponding .Buffer() call on the returned value within the same function scope. Without Buffer(), the scanner uses a fixed 64 KiB internal buffer and fails on lines longer than that — a reliability and DoS risk for builtins that must handle arbitrary input.

Functions

func NewAnalyzer

func NewAnalyzer(cfg AnalyzerConfig) *analysis.Analyzer

NewAnalyzer returns a go/analysis.Analyzer that enforces the symbol-level import restrictions described by cfg. Violations are reported via pass.Reportf and appear as diagnostics with proper file:line:col positions.

NewAnalyzer panics if any entry in cfg.Symbols is malformed (no dot separator), matching the behaviour of the test-harness variant.

NOTE: This analyzer only enforces symbol-level allowlist restrictions. For full static analysis coverage, callers should also register ScannerBufferAnalyzer and OpenFileCloseAnalyzer alongside this one. The test-harness path (checkAllowedSymbols) already applies all three checks automatically.

Types

type AnalyzerConfig

type AnalyzerConfig struct {
	// Symbols is the allowlist to enforce (e.g. builtinAllowedSymbols).
	// Each entry must be in "importpath.Symbol" form.
	Symbols []string
	// ExemptImport returns true for import paths that are auto-allowed and
	// should not be checked against the allowlist (e.g. same-module imports).
	ExemptImport func(importPath string) bool
	// ListName is used in diagnostic messages (e.g. "builtinAllowedSymbols").
	ListName string
}

AnalyzerConfig configures a single instance of the allowed-symbols analyzer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL