- Name: proj
- Version: 1.1
- Type: C++ (binary module)
- Audit Date: 2026-04-23
- Safe for Sandbox Use: Conditional — see "Network Security" below
- Functional Domain:
QDOM_DEFAULT - Rationale: The proj module itself performs no direct filesystem,
network, subprocess, or environment-variable access in its own C++
code. However, the underlying libproj library does read files from
disk (the PROJ resource directory:
proj.db, grid-shift files,.tifCDN files) and may access the network if PROJ's CDN mode is enabled. See the Filesystem and Network sections below for the full picture.
-
Module source code performs no direct filesystem operations (no
fopen,open,fstream,ifstream,ofstream,stat,unlink, etc.) — verified by grep. -
Module does not call
proj_context_set_search_paths(),proj_context_set_database_path(), orproj_context_set_user_writable_directory()— the Qore caller cannot inject arbitrary filesystem paths into libproj. -
Indirect filesystem reads by libproj: every PROJ context created via
proj_context_create()readsproj.db(SQLite) plus any grid-shift files (.tif,.gsb) needed for a given transformation. libproj resolves these files by searching: 1. Paths from thePROJ_DATA(or legacyPROJ_LIB) environment variable, 2. The compile-timePROJ_LIB_DIR(typically/usr/share/projor/opt/homebrew/share/proj), 3. The user-writable directory (~/.local/share/projon Linux,~/Library/Application Support/projon macOS). -
Gaps Found: None in the module itself. The libproj filesystem reads are outside Qore's filesystem security manager: libproj uses direct C library calls (
fopen,mmap), notQoreFile/QoreDir, soQoreFilesystemSecurityManager::checkAccess()is not consulted. -
Severity: Low. The files read are well-known, install-time resource files whose contents are public (CRS definitions, datum shift grids). A malicious Qore caller cannot redirect these reads to arbitrary paths through the module's own API surface — the search paths come from the process environment at libproj init time.
-
Operator recommendation: when loading this module into a sandbox that restricts filesystem reads, the sandbox policy must explicitly allow reads under the PROJ resource directory (typically
/usr/share/proj/**or whatever$PROJ_DATApoints to). OtherwiseProjTransformerconstruction will fail withproj_create_crs_to_crs()returning null for even standard EPSG codes. -
Threat-model note: the
PROJ_DATA/PROJ_LIBenv var is honored by libproj and can redirect resource lookup to an attacker-controlled path. Sandbox operators should scrub these env vars or pre-set them to a trusted directory before loading the module.
-
Module source code performs no direct network operations (no
socket,connect,bind,getaddrinfo— verified by grep). -
Module explicitly disables libproj's network-CDN feature:
proj_get_context()callsproj_context_set_enable_network(ctx, 0)immediately afterproj_context_create(). This overrides anyPROJ_NETWORK=ONenvironment variable for every context the module creates, closing the env-var-bypass SSRF surface. Seesrc/proj-module.cppproj_get_context(). -
Module does not call any other network-enabling API (
proj_context_set_url_endpoint,proj_context_set_network_callbacks, etc.) — verified by grep. -
Gaps Found: None at the module boundary. A caller who obtained a raw
PJ_CONTEXT*through some future unexported interface could re-enable network; today there is no such interface. -
Severity: None.
- No subprocess invocation
- No direct environment variable access from the module — libproj
reads its own env vars (
PROJ_DATA,PROJ_LIB,PROJ_NETWORK,PROJ_DEBUG) at init time. - Gaps Found: env-var influence on libproj behavior, covered in Filesystem and Network above.
- Severity: Low
-
ProjTransformerconstructor allocates onePJ*per thread viaproj_create_crs_to_crs(). Memory per transformer is bounded by libproj internals (CRS descriptors + grid metadata + SQLite connection toproj.db). A typicalEPSG:4326 → EPSG:25832transformer is a few hundred KB. -
transform()/transformInverse()(single point): constant time, microsecond latency. -
transformBatch()/transformInverseBatch(): oneproj_trans_generic()call for the whole input — linear in the number of points, no intermediate allocations beyond the twostd::vector<double>(xs, ys) used to bundle coordinates. -
Geodesic helpers (
great_circle_distance,forward_azimuth,inverse_azimuth): constant time, no allocations. -
No native threads spawned by the module.
-
Gaps Found:
transformBatch/transformInverseBatchallocateO(n)memory proportional to the input list size. A malicious or buggy caller passing a 100-million-element list can exhaust memory. This is bounded by the caller and visible to Qore's own allocation tracking (thelist<auto>parameter is counted at Qore level). The input marshalling, libproj call, and output wrapping are all cooperatively cancellable, so a runaway allocator can still be interrupted viacancel_thread()orSandboxManager::requestInterrupt(). -
Severity: Low — user-controlled input size, standard resource-limit territory.
-
Pre-operation
qore_check_cancel()(Pattern 1) at the head of every public entry point: constructor,transform(),transformInverse(),transformBatch(),transformInverseBatch(), andgetPj()before the expensive first-time CRS setup that readsproj.dband grid-shift files. -
Batch-chunked
proj_trans_generic()calls: both batch methods process the input inPROJ_BATCH_CHUNK(= 4096 points) chunks with a cancellation check between chunks. This bounds cancellation latency on a million-point batch to the wall time of one 4096-point chunk — well under the 500 ms polling budget on any modern CPU for typical EPSG pairs. -
Periodic
qore_check_cancel()(Pattern 5) everyPROJ_CHECK_EVERY(= 1000) iterations in the input-marshalling and output-wrapping loops, so the cheap-per-point loops don't themselves become a cancellation blind spot on very large inputs. -
Geodesic helpers (
great_circle_distance,forward_azimuth,inverse_azimuth) complete in microseconds per call — no polling needed. -
Gaps Found: None.
-
Severity: None.
- ProjTransformer: thread-safe for concurrent use. Each thread
gets its own
PJ_CONTEXT(thread-local, lazily created inproj_get_context()) and its ownPJ*(per-transformerstd::unordered_map<thread::id, PJ*>guarded by astd::mutex). libproj requires eachPJto be used only with the context it was created under; this invariant is enforced by the per-thread PJ cache. - Module-level state: the
QoreNamespace PNS("Qore::PROJ")is the only global, initialized once at module load. No mutable module- level state. - Thread-local cleanup:
PJ_CONTEXTis intentionally not destroyed at thread exit (seeproj-module.cpp:51-54). Contexts leak at process shutdown only; they are small and bounded. A long-lived thread pool sees at most one context leak per worker, which is negligible.
None — the proj module does not invoke user-supplied Qore callbacks.
- Compliance Level: Full at the module boundary. Module source
does no direct filesystem or network I/O; libproj's CDN is hard-
disabled per-context regardless of
PROJ_NETWORK; every public entry point is cooperatively cancellable. - Highest Severity Finding: Low (operator-level filesystem- whitelist requirement — intrinsic to libproj, not a module bug).
- Recommendation: Safe for sandbox use provided that the
sandbox filesystem policy allows reads under the PROJ resource
directory (
/usr/share/projor wherever$PROJ_DATApoints), andPROJ_DATA/PROJ_LIBenv vars are scrubbed or pre-set to a trusted path.
| # | Location | Description | Severity | Remediation |
|---|---|---|---|---|
| 1 | libproj, indirect | libproj reads proj.db and grid-shift files from paths resolved via PROJ_DATA / PROJ_LIB / compile-time default. Bypasses QoreFilesystemSecurityManager. |
Low | Sandbox operators must whitelist the PROJ resource directory and scrub PROJ_DATA / PROJ_LIB env vars. Not fixable in the module. |