{"record":{"id":"fc158481a5aed02a","repo":"microsoft/garnet","slug":"native-handle-is-null","errorCode":null,"errorMessage":"Native handle is null.","messagePattern":"Native handle is null\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"libs/native/bftree-garnet/BfTreeService.cs","lineNumber":528,"sourceCode":"        /// Take a CPR snapshot of a tree given only its native handle (no managed wrapper).\n        /// Used by RangeIndex's <c>OnFlush</c> path which has direct access to the stub's\n        /// TreeHandle but not the managed <see cref=\"BfTreeService\"/> instance.\n        ///\n        /// <para><b>Caller contract:</b> this method does NOT self-serialize. bftree's internal\n        /// <c>snapshot_in_progress</c> flag makes a <c>cpr_snapshot</c> that races another\n        /// snapshot on the same tree <b>silently no-op</b> (no file written) while this method\n        /// still returns success. Callers MUST hold external per-tree serialization (e.g.\n        /// RangeIndex's per-tree snapshot claim) around this call so concurrent snapshots of the\n        /// same handle cannot race.</para>\n        /// </summary>\n        /// <param name=\"handle\">Native BfTree pointer.</param>\n        /// <param name=\"snapshotPath\">Destination path for the snapshot file. The snapshot\n        /// destination is supplied at call time; the caller supplies the path it\n        /// configured for this tree.</param>\n        public static void CprSnapshotByPtr(nint handle, string snapshotPath)\n        {\n            if (handle == nint.Zero)\n                throw new ArgumentException(\"Native handle is null.\", nameof(handle));\n            if (string.IsNullOrEmpty(snapshotPath))\n                throw new ArgumentException(\"Snapshot path is required.\", nameof(snapshotPath));\n\n            var snapBytes = Encoding.UTF8.GetBytes(snapshotPath);\n            int result;\n            fixed (byte* sp = snapBytes)\n            {\n                result = NativeBfTreeMethods.bftree_cpr_snapshot(handle, sp, snapBytes.Length);\n            }\n            if (result != 0)\n                throw new InvalidOperationException(\"Failed to take CPR snapshot of BfTree.\");\n        }\n\n        /// <summary>\n        /// Recover a BfTree from a CPR snapshot file. Unified API for disk-backed and\n        /// memory-backed (cache_only) trees — the storage backend is recorded in the\n        /// snapshot and inferred by the native library.\n        /// </summary>","sourceCodeStart":510,"sourceCodeEnd":546,"githubUrl":"https://github.com/microsoft/garnet/blob/951b0fc6838721f89d102c2bbe1b914e8d39d700/libs/native/bftree-garnet/BfTreeService.cs#L510-L546","documentation":"Thrown by the static CprSnapshotByPtr when the supplied native handle is IntPtr.Zero. This is a pure caller-contract violation: the method exists precisely for callers (e.g. RangeIndex's OnFlush path) that hold a raw tree pointer, and a null pointer cannot be snapshotted. It is an ArgumentException, not an InvalidOperationException, because the argument itself is the defect.","triggerScenarios":"Calling CprSnapshotByPtr(nint.Zero, path), or passing a handle whose tree has already been dropped (bftree_drop set the pointer to zero on the managed side). Passing a disposed stub's TreeHandle that was zeroed out.","commonSituations":"A RangeIndex flush path racing with tree disposal, or a recovery/migration routine that snapshots a handle obtained from a structure that has since been torn down.","solutions":["Check handle != nint.Zero before calling CprSnapshotByPtr.","Ensure the tree's Dispose has not run; gate flush/snapshot behind the same lifecycle that owns the handle.","If the handle is read from a shared stub, re-read it under the stub's lock and skip the snapshot if zero.","Log and skip rather than throw when the tree is known to be going away."],"exampleFix":"// before\nBfTreeService.CprSnapshotByPtr(stub.TreeHandle, snapPath);\n\n// after\nif (stub.TreeHandle != nint.Zero)\n    BfTreeService.CprSnapshotByPtr(stub.TreeHandle, snapPath);\nelse\n    logger.LogWarning(\"Skipping snapshot: tree handle is null\");","handlingStrategy":"validation","validationCode":"if (handle == nint.Zero) throw new ArgumentException(\"Handle is null.\", nameof(handle));\n// or skip: if (handle == nint.Zero) return;","typeGuard":"static bool IsValidTreeHandle(nint handle) => handle != nint.Zero;","tryCatchPattern":"try { BfTreeService.CprSnapshotByPtr(handle, path); }\ncatch (ArgumentException ex) when (ex.ParamName == nameof(handle)) { /* handle stale, skip snapshot */ }","preventionTips":["Read the handle under the same lock that guards tree disposal.","Zero out stored handles on disposal and check before use.","Skip the snapshot rather than throw when the tree is being torn down."],"tags":["bftree","native-interop","snapshot","invalid-argument","lifecycle"],"backgroundTag":null,"analyzedSha":"951b0fc6838721f89d102c2bbe1b914e8d39d700","analyzedAt":"2026-08-13T19:01:32.939Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}