# Safety And Security

This extension runs inside PostgreSQL backend processes, so safety and security
bugs can affect database stability. The codebase uses typed errors, standard
PostgreSQL SQLSTATEs, stable pgGraph diagnostics, ACL checks, schema validation,
and narrow unsafe boundaries.

## Error Model

`GraphError` maps internal errors to a standard PostgreSQL SQLSTATE and a stable
pgGraph diagnostic identifier. Clients should branch on the SQLSTATE class and
use the diagnostic in `DETAIL` when they need pgGraph-specific classification.

| Variant | SQLSTATE | Diagnostic |
|---|---|---|
| `Oom` | `53200` | `PG001` |
| `AclDenied` | `42501` | `PG002` |
| `NotBuilt` | `55000` | `PG003` |
| `EdgeTypeLimit` | `54000` | `PG004` |
| `InvalidFilter` | `22023` | `PG005` |
| `BuildLocked` | `55P03` | `PG006` |
| `EdgeBufferFull` | `54000` | `PG008` |
| `CorruptFile` | `XX001` | `PG009` |
| `NodeNotFound` | `P0002` | `PG010` |
| `IncompatibleVersion` | `0A000` | `PG011` |
| `ReadOnly` | `55000` | `PG012` |
| `GqlSyntax` | `42601` | `PG013` |
| `GqlUnsupported` | `0A000` | `PG014` |
| `GqlSemantic` | `22023` | `PG015` |
| `GqlParameter` | `22023` | `PG016` |
| `GqlExecution` | `22000` | `PG017` |
| `UnsupportedOperation` | `0A000` | `PG018` |
| `OverlayLimit` | `54000` | `PG019` |
| `Disabled` | `55000` | `PG020` |
| `Internal` | `XX000` | `PG000` |

`GraphError::report()` constructs a pgrx `ErrorReport` and raises it at
`ERROR`. pgrx first unwinds the Rust stack, including live destructors, then
emits the PostgreSQL error above its guard boundary. Do not add direct
`errfinish()` calls below that boundary.

## Panic Boundary

pgrx provides the actual panic boundary for `#[pg_extern]` functions. The local
`with_panic_boundary()` helper is intentionally a uniform call site and does
not catch panics itself, because catching pgrx error-report panics inside SPI
paths can erase SQLSTATEs or abort the backend.

## ACL Checks

`acl::check_table_acl()` verifies `SELECT` privilege using:

```text
has_table_privilege(current_user, table_oid::regclass, 'SELECT')
```

Contributor rule: any code path that exposes source-table graph coordinates or
hydrated data to a user must preserve the ACL preflight checks.

## Admin Checks

Admin-only operations require either superuser or `CREATE` privilege on schema
`graph`. Keep mutation, build, sync, reset, and global analytics behind this
boundary unless the security model is intentionally changed.

## Schema Drift Validation

`catalog/validate.rs` checks:

| Object | Validation |
|---|---|
| Registered table | Still exists; ID columns match PK or unique `NOT NULL` index; registered columns still exist |
| Registered edge | Source/target tables still exist; columns still exist; weight remains numeric |
| Registered filter column | Type is supported and column still exists |

Runtime helpers mark the engine invalid when drift is detected.

## Unsafe Rules

The crate lints deny undocumented unsafe blocks. Maintain both layers:

| Unsafe site | Required docs |
|---|---|
| `unsafe fn` | rustdoc `# Safety` explaining caller obligations |
| `unsafe {}` block | Local `// SAFETY:` comment explaining why this call is sound |

Current unsafe areas:

| Area | Why unsafe is needed |
|---|---|
| `_PG_init()` temporary mmap | Pre-warm existing file pages |
| `NodeStore` mmap reads | Form typed slices from validated aligned ranges |
| `EdgeStore` mmap reads | Form typed CSR slices from validated aligned ranges |
| `sql_jobs.rs` background worker launch | Read PostgreSQL backend PID for worker notification; database and effective-role OIDs are resolved through SPI |

## Unsafe Invariants

These invariants must remain true whenever an mmap-backed engine is returned
from `load_graph_file()`.

| Invariant | Established by |
|---|---|
| Every mapped store owns an `Arc` clone of the mapping | `MappedGraphArtifact` constructs stores with owning ranges |
| Mapped bytes cannot change after typed views are formed | The loader copies the file into an anonymous mapping and calls `make_read_only()` before validation |
| Node active bytes cover `ceil(node_count / 8)` bytes | `validate_section_layout()` and `MmapNodeArrays::new_for_artifact()` |
| Node table OIDs contain `node_count` aligned `u32` values | Section size/alignment validation |
| Primary-key offsets contain `node_count + 1` aligned `u64` values | Section size/alignment validation |
| Primary-key offsets start at zero, are monotonic, end at the PK byte length, and select valid UTF-8 | `validate_persisted_contents()` and `MmapNodeArrays::new_for_artifact()` |
| Forward and inbound CSR offsets each contain `node_count + 1` aligned `u32` values | Section size/alignment validation |
| CSR offsets start at zero, are monotonic, and end at `edge_count` | `validate_persisted_contents()` and `MmapEdgeArrays::new_for_artifact()` |
| Every target node index is less than `node_count` | `validate_persisted_contents()` and `MmapEdgeArrays::new_for_artifact()` |
| `type_ids` length equals `edge_count` | Section size validation |
| `weights` is empty or exactly `edge_count * 4` bytes | Section size validation |
| `schema_reversed` is exactly `edge_count` bytes of `0` or `1` values | Section validation and `MmapEdgeArrays::new_for_artifact()` |
| Relationship-ID sidecars are aligned and match their direction's edge count | Section validation and `MmapEdgeArrays::new_for_artifact()` |
| Filter descriptors, typed values, presence bytes, and lexical dictionaries stay within their mapped sections | `validate_filter_sections()` and `FilterIndex::from_mapped_sections()` |
| Relationship identity descriptors select valid UTF-8 key ranges and reserve ID zero | Persistence validation and `MappedRelationshipIdentities` |
| Edge registry index 0 is the reserved empty label | Loader registry validation |
| Background worker notify PID is read only while a backend is registering a worker | `launch_build_worker()` and `launch_maintenance_worker()` call PostgreSQL's `MyProcPid` inside an active backend |

Both CSR directions are mapped from independently validated v5 sections. They
retain their own relationship IDs and schema-direction flags, so inbound
traversal does not rebuild graph-sized arrays in backend heap.

## Loader Hardening

Never construct mmap-backed stores from unvalidated section metadata. The loader
must validate file size, magic, version, CRC, offsets, section sizes,
alignment, both CSR directions, primary-key offsets, filters, dictionaries,
relationship identities, and bounded registry metadata
before constructing owning mapped ranges.

Production mapped-array constructors require a private capability created only
after full artifact validation. Safe node metadata lookups check the requested
index at the access site and return
`None` for out-of-range indexes. Tests exercise these accessors with aligned
in-memory backing, so the soundness checks do not depend on operating-system
file mappings.

## SQL Injection Boundaries

Dynamic SQL uses quoted identifiers and regclass-derived table names. When
adding SQL generation:

1. Use `regclass` OIDs and `regclass::text` where possible.
2. Quote identifiers with local helpers.
3. Pass values through SPI parameters instead of string interpolation.
4. Avoid accepting raw SQL fragments from user input.

## Secrets And Documentation

Do not put credentials, private hostnames, production paths, or customer data in
docs, tests, examples, logs, or panic messages.

## Supply Chain Policy

Release dependencies must be pinned and reviewed before they move:

- Rust direct dependencies use exact Cargo requirements plus committed lockfiles.
- Python sandbox dependencies use exact `==` pins.
- Docker base images use explicit version tags, plus digests where the image is
  fixed in the Dockerfile; digest or tag updates require manual review.
- New dependency versions should not be adopted into a release until they are at
  least 6 hours old and any package-manager fetch or install goes through
  `sfw`, unless a security fix requires an explicit exception.

Use the dependency checker before release prep:

```bash
python3 scripts/check_dependency_updates.py
```

The checker reports newer releases, flags releases that are too recent for the
age gate, and only rewrites supported manifests when a maintainer passes both
`--update <ecosystem:name>` and `--yes`. Docker and apt updates are review-only;
verify the upstream changelog, image digest, and package provenance before
changing them.
