# 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 ` and `--yes`. Docker and apt updates are review-only; verify the upstream changelog, image digest, and package provenance before changing them.