{"record":{"id":"0c04ea01a05587fa","repo":"affaan-m/ECC","slug":"unsafe-nasiko-archive-missing-complete-tar-terminator","errorCode":null,"errorMessage":"Unsafe Nasiko archive: missing complete tar terminator.","messagePattern":"Unsafe Nasiko archive: missing complete tar terminator\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/nasiko-release.js","lineNumber":133,"sourceCode":"    const type = String.fromCharCode(header[156] || 48);\n    const size = readTarOctal(header, 124, 12);\n    const start = offset + 512;\n    const end = start + size;\n    const paddedEnd = start + Math.ceil(size / 512) * 512;\n    if (!Number.isSafeInteger(end) || paddedEnd > tar.length) throw new Error('Nasiko archive is truncated.');\n    const payload = tar.subarray(start, end);\n    if (!tar.subarray(end, paddedEnd).every(byte => byte === 0)) {\n      throw new Error('Unsafe Nasiko archive: nonzero tar padding.');\n    }\n    const isBinary = !prefix && name === expectedName && (type === '0' || type === '\\0');\n    const isAppleDouble = !prefix && name === `._${expectedName}` && type === '0' && size <= 1024 * 1024;\n    const isPaxMetadata = !prefix && name === `PaxHeader/${expectedName}` && type === 'x' && size <= 64 * 1024\n      && !/(?:^|\\n)(?:path|linkpath)=/i.test(payload.toString('utf8'));\n    if (isBinary && !binary && size > 0 && size <= MAX_BINARY_BYTES) binary = Buffer.from(payload);\n    else if (!isAppleDouble && !isPaxMetadata) throw new Error('Unsafe Nasiko archive: expected exactly one bounded regular binary file.');\n    offset = paddedEnd;\n  }\n  if (!terminated) throw new Error('Unsafe Nasiko archive: missing complete tar terminator.');\n  if (!binary) throw new Error('Unsafe Nasiko archive: expected exactly one bounded regular binary file.');\n  return binary;\n}\n\nfunction fetchBytes(url, options = {}) {\n  const parsed = new URL(url);\n  if (parsed.origin !== REGISTRY_ORIGIN || parsed.protocol !== 'https:') return Promise.reject(new Error('Nasiko download origin is not allowed.'));\n  const maxBytes = options.maxBytes || MAX_ARCHIVE_BYTES;\n  return new Promise((resolve, reject) => {\n    const request = https.get(parsed, { headers: options.accept ? { Accept: options.accept } : {} }, response => {\n      if (response.statusCode >= 300 && response.statusCode < 400) { response.resume(); reject(new Error('Nasiko registry redirects are not allowed.')); return; }\n      if (response.statusCode !== 200) { response.resume(); reject(new Error(`Nasiko registry returned HTTP ${response.statusCode}.`)); return; }\n      const chunks = [];\n      let total = 0;\n      response.on('data', chunk => {\n        total += chunk.length;\n        if (total > maxBytes) request.destroy(new Error('Nasiko registry response exceeded the size limit.'));\n        else chunks.push(chunk);","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/nasiko-release.js#L115-L151","documentation":"A valid tar ends with at least two consecutive 512-byte blocks of zeros. extractQualifiedTarGzip tracks whether such a terminator block was seen during its scan; if the archive runs out of bytes without encountering one, it throws this error. It is the structural-integrity check ensuring the archive was not cut short mid-entry-stream.","triggerScenarios":"Calling extractQualifiedTarGzip with a decompressed tar that ends immediately after a file entry (or mid-entry) without the required 1024 zero bytes, so the parsing loop exits with terminated === false.","commonSituations":"Truncated download (network drop, interrupted curl/wget); tarball produced by a packer that omits the end-of-archive marker; corrupted cache after a disk-full write; CDN serving a partial body.","solutions":["Re-download the archive and verify its byte length/checksum before extracting.","Use a full-featured tar writer (GNU tar, bsdtar) to repack if you control the archive.","Retry the download with resume/verification enabled.","Compare with a known-good copy of the same release artifact."],"exampleFix":"// before\nconst buf = fs.readFileSync(cachePath); // cache write was interrupted\nextractQualifiedTarGzip(buf);\n// after\nconst buf = fs.readFileSync(cachePath);\nif (sha256(buf) !== expectedDigest) fs.rmSync(cachePath); // evict corrupt cache\nextractQualifiedTarGzip(sha256(buf) === expectedDigest ? buf : await downloadFresh(url));","handlingStrategy":"validation","validationCode":"if (archiveBuffer.length !== manifestSize) throw new Error('Downloaded archive size mismatch; refusing extraction.');","typeGuard":null,"tryCatchPattern":"try { return extractQualifiedTarGzip(tar); } catch (err) { if (err.message.includes('missing complete tar terminator')) { evictCache(entry); return downloadFresh(url); } throw err; }","preventionTips":["Verify total byte count and checksum of downloads before caching","Use packers that emit the standard 1024-byte end-of-archive marker","Enable retry with resume on flaky networks","Write archives to a temp file and atomically rename only after full verification"],"tags":["archive","tar","truncation","integrity"],"backgroundTag":"checksum-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}