Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 8b023d9

Browse files
committed
iscsi: address comments
* check symlinks * remove possible connection leaks Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent ac387f5 commit 8b023d9

2 files changed

Lines changed: 41 additions & 20 deletions

File tree

packages/jumpstarter-driver-iscsi/jumpstarter_driver_iscsi/client.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,25 @@ def _get_src_and_operator(
9898
header_map[key] = value
9999

100100
parsed = urlparse(file)
101-
with NamedTemporaryFile(
101+
tf = NamedTemporaryFile(
102102
prefix="jumpstarter-iscsi-",
103103
suffix=os.path.basename(parsed.path),
104104
delete=False,
105-
) as tf:
106-
temp_path = tf.name
107-
with requests.get(file, stream=True, headers=header_map, timeout=60) as resp:
105+
)
106+
temp_path = tf.name
107+
try:
108+
with requests.get(file, stream=True, headers=header_map, timeout=(10, 60)) as resp:
108109
resp.raise_for_status()
109110
for chunk in resp.iter_content(chunk_size=65536):
110111
if chunk:
111112
tf.write(chunk)
112-
return temp_path, None, temp_path
113+
tf.close()
114+
return temp_path, None, temp_path
115+
except Exception:
116+
tf.close()
117+
with contextlib.suppress(Exception):
118+
os.unlink(temp_path)
119+
raise
113120

114121
_, src_operator, _ = operator_for_path(file)
115122
return file, src_operator, None

packages/jumpstarter-driver-iscsi/jumpstarter_driver_iscsi/driver.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import socket
66
from contextlib import suppress
77
from dataclasses import dataclass, field
8+
from tempfile import NamedTemporaryFile
89
from typing import Any, Dict, List, Optional
910

1011
from jumpstarter_driver_opendal.driver import Opendal
@@ -66,9 +67,7 @@ def __post_init__(self):
6667
os.makedirs(self.root_dir, exist_ok=True)
6768

6869
self.children["storage"] = Opendal(
69-
scheme="fs",
70-
kwargs={"root": self.root_dir},
71-
remove_created_on_close=self.remove_created_on_close
70+
scheme="fs", kwargs={"root": self.root_dir}, remove_created_on_close=self.remove_created_on_close
7271
)
7372
self.storage = self.children["storage"]
7473

@@ -306,6 +305,15 @@ def _safe_join_under_root(self, rel_path: str) -> str:
306305
os.makedirs(os.path.dirname(full_path), exist_ok=True)
307306
return full_path
308307

308+
def _check_no_symlinks_in_path(self, path: str) -> None:
309+
"""Verify no path component is a symlink to prevent writing outside root."""
310+
path_to_check = path
311+
root_abs = os.path.abspath(self.root_dir)
312+
while path_to_check != root_abs and path_to_check != os.path.dirname(path_to_check):
313+
if os.path.lexists(path_to_check) and os.path.islink(path_to_check):
314+
raise ISCSIError(f"Destination path contains symlink: {path_to_check}")
315+
path_to_check = os.path.dirname(path_to_check)
316+
309317
@export
310318
def decompress(self, src_path: str, dst_path: str, algo: str) -> None:
311319
"""Decompress a file under storage root into another path under storage root.
@@ -315,6 +323,7 @@ def decompress(self, src_path: str, dst_path: str, algo: str) -> None:
315323
"""
316324
src_full = self._safe_join_under_root(src_path)
317325
dst_full = self._safe_join_under_root(dst_path)
326+
self._check_no_symlinks_in_path(dst_full)
318327

319328
def _copy_stream(read_f, write_f):
320329
while True:
@@ -323,26 +332,31 @@ def _copy_stream(read_f, write_f):
323332
break
324333
write_f.write(chunk)
325334

335+
tmp_path = None
326336
try:
327-
if algo == "gz":
328-
with open(dst_full, "wb") as out_f:
337+
with NamedTemporaryFile(dir=os.path.dirname(dst_full), prefix=".decomp-", delete=False) as tf:
338+
tmp_path = tf.name
339+
if algo == "gz":
329340
with gzip.open(src_full, "rb") as decomp:
330-
_copy_stream(decomp, out_f)
331-
elif algo == "xz":
332-
with open(dst_full, "wb") as out_f:
341+
_copy_stream(decomp, tf)
342+
elif algo == "xz":
333343
with lzma.open(src_full, "rb") as decomp:
334-
_copy_stream(decomp, out_f)
335-
elif algo == "bz2":
336-
with open(dst_full, "wb") as out_f:
344+
_copy_stream(decomp, tf)
345+
elif algo == "bz2":
337346
with bz2.open(src_full, "rb") as decomp:
338-
_copy_stream(decomp, out_f)
339-
else:
340-
raise ISCSIError(f"Unsupported compression algo: {algo}")
347+
_copy_stream(decomp, tf)
348+
else:
349+
raise ISCSIError(f"Unsupported compression algo: {algo}")
350+
tf.flush()
351+
os.fsync(tf.fileno())
352+
os.replace(tmp_path, dst_full)
341353
except Exception as e:
354+
with suppress(Exception):
355+
if tmp_path is not None:
356+
os.remove(tmp_path)
342357
raise ISCSIError(f"Decompression failed: {e}") from e
343358

344359
def _create_file_storage_object(self, name: str, full_path: str, size_mb: int) -> tuple:
345-
"""Create file-backed storage object and return (storage_obj, final_size_mb)"""
346360
if not os.path.exists(full_path):
347361
if size_mb <= 0:
348362
raise ISCSIError("size_mb must be > 0 for new file-backed LUNs")

0 commit comments

Comments
 (0)