personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(spp): harden GPU attestation leg and shared tarball extraction

Follow up on the self-audit of the NVIDIA GPU attestation leg.

Tar extraction now rejects symlink and hardlink members whose link target escapes the destination and passes filter=data as the stdlib backstop. Apply the guard to all three copies of _safe_extract_tarball in rfdetr, parakeet, and local because it is a security primitive; consolidating those copies remains deferred.

nvattest_install no longer trusts a partial or stale cache. _installed now requires a parsed sidecar whose archive_sha256 and version match the current pinned spec, so crash-interrupted or re-pinned installs re-verify instead of running an unrecorded binary. The sidecar temp path is also captured before writing so write failure cannot leak it.

classify_nvattest_result now requires a non-bool int returncode equal to 0, closing the same bool/int fail-closed gap already fixed for result_code.

The purity scanner now bans dangerous os calls, including system, popen, remove/unlink, rmdir, mkdir/makedirs, rename/replace, and the exec/spawn families, so allowing os.environ in binary.py cannot smuggle process execution or destructive writes into a pure file.

Remove dead code: drop the unused build_nvattest_version_command and reduce NvattestAcceptance to the claim field actually consumed. The overall-EAT veto still runs.

+417 -30
+18 -1
solstone/think/providers/local_install.py
··· 376 376 "archive_path_traversal", 377 377 f"Unsafe tar member path: {member.name}", 378 378 ) 379 - archive.extractall(dest) 379 + if member.issym() or member.islnk(): 380 + linkname = Path(member.linkname) 381 + if linkname.is_absolute(): 382 + raise LocalProviderError( 383 + "archive_path_traversal", 384 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 385 + ) 386 + link_base = target.parent if member.issym() else dest_resolved 387 + link_target = (link_base / linkname).resolve() 388 + if ( 389 + link_target != dest_resolved 390 + and dest_resolved not in link_target.parents 391 + ): 392 + raise LocalProviderError( 393 + "archive_path_traversal", 394 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 395 + ) 396 + archive.extractall(dest, filter="data") 380 397 381 398 382 399 def _find_extracted_binary(dest: Path, binary_name: str) -> Path:
+19 -4
solstone/think/providers/nvattest_install.py
··· 112 112 """Download, verify, and install nvattest into the journal provider cache.""" 113 113 114 114 root = cache_root(journal_path) 115 - if not force and _installed(root): 115 + if not force and _installed(root, spec): 116 116 return root 117 117 118 118 archive = _archive_path(spec, journal_path) ··· 137 137 archive.unlink(missing_ok=True) 138 138 139 139 140 - def _installed(root: Path) -> bool: 140 + def _has_runtime_layout(root: Path) -> bool: 141 141 return (root / "bin" / "nvattest").is_file() and (root / "lib").is_dir() 142 142 143 143 144 + def _installed(root: Path, spec: NvattestArchiveSpec) -> bool: 145 + if not _has_runtime_layout(root): 146 + return False 147 + try: 148 + data = json.loads((root / SIDECAR_NAME).read_text(encoding="utf-8")) 149 + except (OSError, json.JSONDecodeError): 150 + return False 151 + if not isinstance(data, dict): 152 + return False 153 + return ( 154 + data.get("archive_sha256") == spec.sha256 155 + and data.get("version") == spec.version 156 + ) 157 + 158 + 144 159 def _archive_path( 145 160 spec: NvattestArchiveSpec, 146 161 journal_path: str | Path | None = None, ··· 212 227 213 228 214 229 def _find_extracted_root(extract_dir: Path) -> Path: 215 - if _installed(extract_dir): 230 + if _has_runtime_layout(extract_dir): 216 231 return extract_dir 217 232 matches = [ 218 233 path ··· 264 279 with tempfile.NamedTemporaryFile( 265 280 "w", dir=path.parent, delete=False, encoding="utf-8" 266 281 ) as handle: 267 - handle.write(record.to_json()) 268 282 tmp_path = Path(handle.name) 283 + handle.write(record.to_json()) 269 284 tmp_path.replace(path)
+18 -1
solstone/think/providers/parakeet_install.py
··· 317 317 "archive_path_traversal", 318 318 f"Unsafe tar member path: {member.name}", 319 319 ) 320 - archive.extractall(dest) 320 + if member.issym() or member.islnk(): 321 + linkname = Path(member.linkname) 322 + if linkname.is_absolute(): 323 + raise ParakeetProviderError( 324 + "archive_path_traversal", 325 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 326 + ) 327 + link_base = target.parent if member.issym() else dest_resolved 328 + link_target = (link_base / linkname).resolve() 329 + if ( 330 + link_target != dest_resolved 331 + and dest_resolved not in link_target.parents 332 + ): 333 + raise ParakeetProviderError( 334 + "archive_path_traversal", 335 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 336 + ) 337 + archive.extractall(dest, filter="data") 321 338 322 339 323 340 def _find_extracted_binary(dest: Path, binary_name: str) -> Path:
+18 -1
solstone/think/providers/rfdetr_install.py
··· 323 323 "archive_path_traversal", 324 324 f"Unsafe tar member path: {member.name}", 325 325 ) 326 - archive.extractall(dest) 326 + if member.issym() or member.islnk(): 327 + linkname = Path(member.linkname) 328 + if linkname.is_absolute(): 329 + raise RfdetrInstallError( 330 + "archive_path_traversal", 331 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 332 + ) 333 + link_base = target.parent if member.issym() else dest_resolved 334 + link_target = (link_base / linkname).resolve() 335 + if ( 336 + link_target != dest_resolved 337 + and dest_resolved not in link_target.parents 338 + ): 339 + raise RfdetrInstallError( 340 + "archive_path_traversal", 341 + f"Unsafe tar link target: {member.name} -> {member.linkname}", 342 + ) 343 + archive.extractall(dest, filter="data") 327 344 328 345 329 346 def _find_extracted_binary(dest: Path, binary_name: str) -> Path:
-10
solstone/think/services/spp_attest/nvgpu/binary.py
··· 84 84 return NvattestCommand( 85 85 argv=argv, env={**os.environ, "LD_LIBRARY_PATH": str(lib_dir)} 86 86 ) 87 - 88 - 89 - def build_nvattest_version_command(*, nvattest_dir: Path) -> NvattestCommand: 90 - """Build the nvattest version command.""" 91 - 92 - binary, lib_dir = locate_nvattest(nvattest_dir) 93 - return NvattestCommand( 94 - argv=[str(binary), "version"], 95 - env={**os.environ, "LD_LIBRARY_PATH": str(lib_dir)}, 96 - )
+12 -11
solstone/think/services/spp_attest/nvgpu/claims.py
··· 39 39 40 40 @dataclass(frozen=True, slots=True) 41 41 class NvattestAcceptance: 42 - stdout: dict[str, Any] 43 42 claim: dict[str, Any] 44 - overall_payload: dict[str, Any] 45 43 46 44 47 45 @dataclass(frozen=True, slots=True) ··· 108 106 result_message = _required_stdout(stdout_obj, "result_message") 109 107 except _ClaimReject as exc: 110 108 return NvattestRejection("gpu_appraisal_failed", str(exc)) 109 + returncode_is_green = ( 110 + isinstance(returncode, int) 111 + and not isinstance(returncode, bool) 112 + and returncode == 0 113 + ) 111 114 result_code_is_green = ( 112 115 isinstance(result_code, int) 113 116 and not isinstance(result_code, bool) 114 117 and result_code == 0 115 118 ) 116 119 result_message_is_green = isinstance(result_message, str) and result_message == "Ok" 117 - if returncode != 0 or not result_code_is_green or not result_message_is_green: 120 + if ( 121 + not returncode_is_green 122 + or not result_code_is_green 123 + or not result_message_is_green 124 + ): 118 125 return NvattestRejection( 119 126 _reason_for_result_code(result_code), 120 127 ( ··· 140 147 ) 141 148 142 149 try: 143 - overall_payload = _parse_overall_eat( 144 - _required_stdout(stdout_obj, "detached_eat") 145 - ) 150 + _parse_overall_eat(_required_stdout(stdout_obj, "detached_eat")) 146 151 _check_claim(claim, owner_nonce.hex()) 147 152 except _ClaimReject as exc: 148 153 return NvattestRejection("gpu_appraisal_failed", str(exc)) 149 - return NvattestAcceptance( 150 - stdout=stdout_obj, 151 - claim=claim, 152 - overall_payload=overall_payload, 153 - ) 154 + return NvattestAcceptance(claim=claim) 154 155 155 156 156 157 def build_gpu_appraisal(
+15
tests/services/test_spp_attest_nvgpu.py
··· 345 345 assert result.driver_version == "595.71.05" 346 346 347 347 348 + def test_bool_false_returncode_rejects( 349 + tmp_path: Path, 350 + monkeypatch: pytest.MonkeyPatch, 351 + ) -> None: 352 + with pytest.raises(GpuAppraisalError) as exc_info: 353 + _run_appraisal_with_stdout( 354 + monkeypatch, 355 + tmp_path, 356 + _stdout("positive"), 357 + returncode=False, 358 + ) 359 + 360 + assert exc_info.value.reason == "gpu_appraisal_failed" 361 + 362 + 348 363 def test_nonce_is_written_to_evidence_and_argv( 349 364 tmp_path: Path, 350 365 monkeypatch: pytest.MonkeyPatch,
+58 -2
tests/services/test_spp_attest_purity.py
··· 66 66 "atomic_replace", 67 67 } 68 68 BANNED_WRITE_NAMES = {"atomic_write", "atomic_replace"} 69 + BANNED_OS_ATTRS = { 70 + "makedirs", 71 + "mkdir", 72 + "popen", 73 + "remove", 74 + "rename", 75 + "replace", 76 + "rmdir", 77 + "system", 78 + "unlink", 79 + } 80 + BANNED_OS_ATTR_PREFIXES = ("exec", "spawn") 69 81 WRITE_MODE_CHARS = frozenset({"w", "a", "x", "+"}) 70 82 71 83 ··· 83 95 findings: list[str] = [] 84 96 for path in files: 85 97 tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) 98 + os_names = _imported_module_names(tree, "os") 86 99 for node in ast.walk(tree): 87 - findings.extend(_scan_node(path, node)) 100 + findings.extend(_scan_node(path, node, os_names=os_names)) 88 101 89 102 assert findings == [] 90 103 ··· 98 111 assert "tempfile" in imports 99 112 100 113 findings: list[str] = [] 114 + os_names = _imported_module_names(tree, "os") 101 115 for node in ast.walk(tree): 102 116 findings.extend( 103 117 _scan_node( ··· 106 120 banned_import_roots=APPRAISE_BANNED_IMPORT_ROOTS, 107 121 banned_write_attrs=BANNED_WRITE_ATTRS - {"unlink"}, 108 122 ban_solstone_utils=True, 123 + os_names=os_names, 109 124 ) 110 125 ) 111 126 ··· 197 212 assert findings == ["snippet.py:1: banned import shutil"] 198 213 199 214 215 + def test_purity_scanner_bans_aliased_os_system_call() -> None: 216 + tree = ast.parse("import os as operating_system\noperating_system.system('x')\n") 217 + os_names = _imported_module_names(tree, "os") 218 + findings = [ 219 + finding 220 + for node in ast.walk(tree) 221 + for finding in _scan_node(Path("snippet.py"), node, os_names=os_names) 222 + ] 223 + 224 + assert findings == ["snippet.py:2: banned os.system call"] 225 + 226 + 200 227 def _scan_node( 201 228 path: Path, 202 229 node: ast.AST, ··· 204 231 banned_import_roots: set[str] = BANNED_IMPORT_ROOTS, 205 232 banned_write_attrs: set[str] = BANNED_WRITE_ATTRS, 206 233 ban_solstone_utils: bool = False, 234 + os_names: set[str] | None = None, 207 235 ) -> list[str]: 236 + os_names = os_names or set() 208 237 if isinstance(node, ast.Import): 209 238 return _scan_import(path, node, banned_import_roots) 210 239 if isinstance(node, ast.ImportFrom): ··· 215 244 ban_solstone_utils, 216 245 ) 217 246 if isinstance(node, ast.Call): 218 - return _scan_call(path, node, banned_write_attrs) 247 + return _scan_call(path, node, banned_write_attrs, os_names) 219 248 return [] 220 249 221 250 ··· 248 277 ): 249 278 findings.append(f"{path}:{node.lineno}: banned import from {module}") 250 279 for alias in node.names: 280 + if module == "os" and _is_banned_os_attr(alias.name): 281 + findings.append(f"{path}:{node.lineno}: banned import from os {alias.name}") 251 282 if alias.name in BANNED_WRITE_NAMES: 252 283 findings.append(f"{path}:{node.lineno}: banned write helper {alias.name}") 253 284 return findings ··· 257 288 path: Path, 258 289 node: ast.Call, 259 290 banned_write_attrs: set[str], 291 + os_names: set[str], 260 292 ) -> list[str]: 261 293 func = node.func 262 294 if isinstance(func, ast.Attribute): 295 + if _is_banned_os_call(func, os_names): 296 + return [f"{path}:{node.lineno}: banned os.{func.attr} call"] 263 297 if _is_shutil_which(func): 264 298 return [f"{path}:{node.lineno}: banned shutil.which call"] 265 299 if _is_json_dump(func): ··· 274 308 return [] 275 309 276 310 311 + def _is_banned_os_attr(attr: str) -> bool: 312 + return attr in BANNED_OS_ATTRS or attr.startswith(BANNED_OS_ATTR_PREFIXES) 313 + 314 + 315 + def _is_banned_os_call(func: ast.Attribute, os_names: set[str]) -> bool: 316 + return ( 317 + _is_banned_os_attr(func.attr) 318 + and isinstance(func.value, ast.Name) 319 + and func.value.id in os_names 320 + ) 321 + 322 + 277 323 def _is_shutil_which(func: ast.Attribute) -> bool: 278 324 return ( 279 325 func.attr == "which" ··· 298 344 elif isinstance(node, ast.ImportFrom) and node.module: 299 345 roots.add(node.module.split(".", maxsplit=1)[0]) 300 346 return roots 347 + 348 + 349 + def _imported_module_names(tree: ast.AST, module: str) -> set[str]: 350 + names: set[str] = set() 351 + for node in ast.walk(tree): 352 + if isinstance(node, ast.Import): 353 + for alias in node.names: 354 + if alias.name == module: 355 + names.add(alias.asname or module) 356 + return names 301 357 302 358 303 359 def _open_uses_write_mode(node: ast.Call) -> bool:
+125
tests/test_nvattest_install.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import hashlib 7 + import json 8 + import shutil 9 + import tarfile 10 + from pathlib import Path 11 + 12 + import pytest 13 + 14 + from solstone.think.providers import nvattest_install 15 + 16 + 17 + def test_install_nvattest_reinstalls_partial_cache_without_sidecar( 18 + tmp_path: Path, 19 + monkeypatch: pytest.MonkeyPatch, 20 + ) -> None: 21 + root = nvattest_install.cache_root(tmp_path) 22 + (root / "bin").mkdir(parents=True) 23 + (root / "bin" / "nvattest").write_text("stale\n", encoding="utf-8") 24 + (root / "lib").mkdir() 25 + 26 + spec = _fixture_spec(tmp_path) 27 + calls: list[Path] = [] 28 + 29 + def fake_download(_url: str, dest: Path, _expected_sha256: str) -> None: 30 + calls.append(dest) 31 + dest.parent.mkdir(parents=True, exist_ok=True) 32 + shutil.copy2(tmp_path / spec.archive_name, dest) 33 + 34 + monkeypatch.setattr(nvattest_install, "_download_file", fake_download) 35 + 36 + installed = nvattest_install.install_nvattest(spec=spec, journal_path=tmp_path) 37 + 38 + assert installed == root 39 + assert len(calls) == 1 40 + assert (root / "bin" / "nvattest").read_text(encoding="utf-8") == "new\n" 41 + assert (root / "lib" / "libnvat.so").is_symlink() 42 + assert (root / "lib" / "libnvat.so").readlink() == Path("libnvat.so.1") 43 + sidecar = json.loads( 44 + (root / nvattest_install.SIDECAR_NAME).read_text(encoding="utf-8") 45 + ) 46 + assert sidecar["archive_sha256"] == spec.sha256 47 + assert sidecar["version"] == spec.version 48 + 49 + 50 + def test_install_nvattest_reinstalls_stale_sidecar( 51 + tmp_path: Path, 52 + monkeypatch: pytest.MonkeyPatch, 53 + ) -> None: 54 + root = nvattest_install.cache_root(tmp_path) 55 + (root / "bin").mkdir(parents=True) 56 + (root / "bin" / "nvattest").write_text("old\n", encoding="utf-8") 57 + (root / "lib").mkdir() 58 + (root / nvattest_install.SIDECAR_NAME).write_text( 59 + json.dumps({"archive_sha256": "old", "version": "0.0.0"}) + "\n", 60 + encoding="utf-8", 61 + ) 62 + 63 + spec = _fixture_spec(tmp_path) 64 + calls: list[Path] = [] 65 + 66 + def fake_download(_url: str, dest: Path, _expected_sha256: str) -> None: 67 + calls.append(dest) 68 + dest.parent.mkdir(parents=True, exist_ok=True) 69 + shutil.copy2(tmp_path / spec.archive_name, dest) 70 + 71 + monkeypatch.setattr(nvattest_install, "_download_file", fake_download) 72 + 73 + nvattest_install.install_nvattest(spec=spec, journal_path=tmp_path) 74 + 75 + assert len(calls) == 1 76 + assert (root / "bin" / "nvattest").read_text(encoding="utf-8") == "new\n" 77 + 78 + 79 + def test_install_nvattest_valid_sidecar_is_noop( 80 + tmp_path: Path, 81 + monkeypatch: pytest.MonkeyPatch, 82 + ) -> None: 83 + root = nvattest_install.cache_root(tmp_path) 84 + (root / "bin").mkdir(parents=True) 85 + (root / "bin" / "nvattest").write_text("installed\n", encoding="utf-8") 86 + (root / "lib").mkdir() 87 + spec = nvattest_install.NvattestArchiveSpec( 88 + version="1.0.0", 89 + url="https://example.invalid/nvattest.tar.gz", 90 + archive_name="nvattest.tar.gz", 91 + sha256="abc123", 92 + ) 93 + (root / nvattest_install.SIDECAR_NAME).write_text( 94 + json.dumps({"archive_sha256": spec.sha256, "version": spec.version}) + "\n", 95 + encoding="utf-8", 96 + ) 97 + 98 + monkeypatch.setattr( 99 + nvattest_install, 100 + "_download_file", 101 + lambda *_args, **_kwargs: pytest.fail("download should not run"), 102 + ) 103 + 104 + assert nvattest_install.install_nvattest(spec=spec, journal_path=tmp_path) == root 105 + 106 + 107 + def _fixture_spec(tmp_path: Path) -> nvattest_install.NvattestArchiveSpec: 108 + archive_name = "nvattest-fixture.tar.gz" 109 + source = tmp_path / "source" / "nvattest-fixture" 110 + (source / "bin").mkdir(parents=True) 111 + (source / "bin" / "nvattest").write_text("new\n", encoding="utf-8") 112 + (source / "lib").mkdir() 113 + (source / "lib" / "libnvat.so.1").write_text("library\n", encoding="utf-8") 114 + (source / "lib" / "libnvat.so").symlink_to("libnvat.so.1") 115 + (source / "LICENSE").write_text("license\n", encoding="utf-8") 116 + 117 + archive_path = tmp_path / archive_name 118 + with tarfile.open(archive_path, "w:gz") as archive: 119 + archive.add(source, arcname=source.name) 120 + return nvattest_install.NvattestArchiveSpec( 121 + version="9.9.9", 122 + url="https://example.invalid/nvattest-fixture.tar.gz", 123 + archive_name=archive_name, 124 + sha256=hashlib.sha256(archive_path.read_bytes()).hexdigest(), 125 + )
+134
tests/test_provider_tarball_extract.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import io 7 + import tarfile 8 + from collections.abc import Callable 9 + from pathlib import Path 10 + 11 + import pytest 12 + 13 + from solstone.think.providers import local_install, parakeet_install, rfdetr_install 14 + from solstone.think.providers.local import LocalProviderError 15 + 16 + Extractor = Callable[[Path, Path], None] 17 + 18 + 19 + @pytest.mark.parametrize( 20 + ("extract", "error_type"), 21 + [ 22 + (rfdetr_install._safe_extract_tarball, rfdetr_install.RfdetrInstallError), 23 + ( 24 + parakeet_install._safe_extract_tarball, 25 + parakeet_install.ParakeetProviderError, 26 + ), 27 + (local_install._safe_extract_tarball, LocalProviderError), 28 + ], 29 + ) 30 + def test_safe_extract_tarball_rejects_symlink_escape( 31 + tmp_path: Path, 32 + extract: Extractor, 33 + error_type: type[Exception], 34 + ) -> None: 35 + outside = tmp_path / "outside" 36 + outside.mkdir() 37 + tarball = tmp_path / "symlink-escape.tar" 38 + with tarfile.open(tarball, "w") as archive: 39 + _add_dir(archive, "payload") 40 + _add_symlink(archive, "payload/lib", str(outside)) 41 + _add_file(archive, "payload/lib/evil.txt", b"owned\n") 42 + 43 + with pytest.raises(error_type) as exc_info: 44 + extract(tarball, tmp_path / "dest") 45 + 46 + assert getattr(exc_info.value, "reason_code") == "archive_path_traversal" 47 + assert not (outside / "evil.txt").exists() 48 + 49 + 50 + @pytest.mark.parametrize( 51 + ("extract", "error_type"), 52 + [ 53 + (rfdetr_install._safe_extract_tarball, rfdetr_install.RfdetrInstallError), 54 + ( 55 + parakeet_install._safe_extract_tarball, 56 + parakeet_install.ParakeetProviderError, 57 + ), 58 + (local_install._safe_extract_tarball, LocalProviderError), 59 + ], 60 + ) 61 + def test_safe_extract_tarball_rejects_hardlink_escape( 62 + tmp_path: Path, 63 + extract: Extractor, 64 + error_type: type[Exception], 65 + ) -> None: 66 + outside = tmp_path / "outside" 67 + outside.mkdir() 68 + tarball = tmp_path / "hardlink-escape.tar" 69 + with tarfile.open(tarball, "w") as archive: 70 + _add_dir(archive, "payload") 71 + _add_hardlink(archive, "payload/libnvat.so", str(outside / "target")) 72 + 73 + with pytest.raises(error_type) as exc_info: 74 + extract(tarball, tmp_path / "dest") 75 + 76 + assert getattr(exc_info.value, "reason_code") == "archive_path_traversal" 77 + 78 + 79 + @pytest.mark.parametrize( 80 + "extract", 81 + [ 82 + rfdetr_install._safe_extract_tarball, 83 + parakeet_install._safe_extract_tarball, 84 + local_install._safe_extract_tarball, 85 + ], 86 + ) 87 + def test_safe_extract_tarball_allows_internal_relative_nvattest_symlink( 88 + tmp_path: Path, 89 + extract: Extractor, 90 + ) -> None: 91 + tarball = tmp_path / "internal-symlink.tar" 92 + with tarfile.open(tarball, "w") as archive: 93 + _add_dir(archive, "payload") 94 + _add_dir(archive, "payload/lib") 95 + _add_file(archive, "payload/lib/libnvat.so.1", b"library\n") 96 + _add_symlink(archive, "payload/lib/libnvat.so", "libnvat.so.1") 97 + 98 + dest = tmp_path / "dest" 99 + extract(tarball, dest) 100 + 101 + link = dest / "payload" / "lib" / "libnvat.so" 102 + assert link.is_symlink() 103 + assert link.readlink() == Path("libnvat.so.1") 104 + assert link.resolve() == dest / "payload" / "lib" / "libnvat.so.1" 105 + 106 + 107 + def _add_dir(archive: tarfile.TarFile, name: str) -> None: 108 + info = tarfile.TarInfo(name) 109 + info.type = tarfile.DIRTYPE 110 + info.mode = 0o755 111 + archive.addfile(info) 112 + 113 + 114 + def _add_file(archive: tarfile.TarFile, name: str, data: bytes) -> None: 115 + info = tarfile.TarInfo(name) 116 + info.size = len(data) 117 + info.mode = 0o644 118 + archive.addfile(info, io.BytesIO(data)) 119 + 120 + 121 + def _add_symlink(archive: tarfile.TarFile, name: str, linkname: str) -> None: 122 + info = tarfile.TarInfo(name) 123 + info.type = tarfile.SYMTYPE 124 + info.mode = 0o777 125 + info.linkname = linkname 126 + archive.addfile(info) 127 + 128 + 129 + def _add_hardlink(archive: tarfile.TarFile, name: str, linkname: str) -> None: 130 + info = tarfile.TarInfo(name) 131 + info.type = tarfile.LNKTYPE 132 + info.mode = 0o644 133 + info.linkname = linkname 134 + archive.addfile(info)