Skip to content

fix: use file lock in virtctl_binary fixture for xdist compatibility - #276

Merged
myakove merged 11 commits into
mainfrom
fix/issue-273-virtctl-xdist-lock
Jan 23, 2026
Merged

myakove merged 11 commits into
mainfrom
fix/issue-273-virtctl-xdist-lock

Conversation

@myakove

@myakove myakove commented Jan 22, 2026 •

Copy link
Copy Markdown
Collaborator

User description

Summary

  • Updated virtctl_binary fixture to use filelock with a shared directory
  • First worker to acquire the lock downloads virtctl, others wait and reuse
  • Added filelock dependency to pyproject.toml
  • Works with and without xdist

Closes #273

Test plan

  • Run tests with pytest-xdist (-n auto) and verify no race conditions when downloading virtctl
  • Run tests without xdist and verify normal behavior
  • Verify virtctl binary is downloaded only once when using multiple workers

PR Type

Bug fix, Enhancement


Description

  • Add file locking to virtctl_binary fixture for pytest-xdist compatibility

  • Use shared directory instead of per-worker temporary directories

  • Prevent race conditions when multiple workers download virtctl simultaneously

  • Add filelock dependency to project dependencies


Diagram Walkthrough

flowchart LR
  A["Multiple xdist workers"] -->|acquire lock| B["FileLock on shared directory"]
  B -->|first worker| C["Download virtctl to /tmp/pytest-shared-virtctl"]
  B -->|other workers| D["Wait for lock, reuse downloaded binary"]
  C --> E["Add to PATH for all workers"]
  D --> E
  E --> F["Return virtctl_path"]
Loading

File Walkthrough

Relevant files
Bug fix
conftest.py
Implement file locking for virtctl binary download             

conftest.py

  • Import filelock module for synchronization
  • Import _add_to_path utility function from virtctl module
  • Replace tmp_path_factory parameter with fixed shared directory
    approach
  • Implement file locking mechanism to serialize virtctl downloads across
    workers
  • Add virtctl to PATH for all xdist workers
+20/-6   
Dependencies
pyproject.toml
Add filelock dependency                                                                   

pyproject.toml

  • Add filelock>=3.16.0 as a new project dependency
+1/-0     

Summary by CodeRabbit

  • Chores

    • Tests now use a shared, versioned, lock-protected cache so downloaded tooling is reused across parallel runs; tooling is validated and added to PATH.
    • Added dependency to support file locking.
  • Bug Fixes

    • Centralized cluster-version retrieval for more reliable version handling and removed the older lookup path.
  • Refactor

    • Renamed and streamlined PATH helper and coordination across workers.
  • Documentation

    • Expanded guidance on validation placement and centralized exception location.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@myakove has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 9 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Replaced per-worker temporary virtctl downloads with a shared, versioned, lock-protected cache coordinated across pytest-xdist workers; fixture signature updated (removed tmp_path_factory), PATH is augmented with the shared directory, and lock timeouts raise a TimeoutError.

Changes

Cohort / File(s) Summary
virtctl fixture & test coordination
conftest.py
Reworked virtctl_binary fixture to use a shared, versioned, permissioned cache directory (persistent across workers), guarded by a file lock with timeout. Validates ownership and executability, removes tmp_path_factory param, and calls add_to_path to augment PATH.
virtctl utility rename
utilities/virtctl.py
Renamed _add_to_path → add_to_path and updated call sites; behavior unchanged.
cluster version retrieval
utilities/utils.py, utilities/post_migration.py
Added get_cluster_version(client) (uses ClusterVersion + packaging.version.Version), replaced direct ClusterVersion usage in post_migration.py, removed legacy get_ocp_version.
dependencies
pyproject.toml
Added filelock>=3.16.0 to enable inter-process locking for shared virtctl downloads.
docs / guidance
CLAUDE.md
Clarified validation placement (favor fixtures), centralized exception definitions, and guidance forbidding pytest.fail() in fixtures/tests for auto-skip rules.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant W as Worker (pytest-xdist)
    participant L as FileLock (/tmp/.../virtctl-<version>.lock)
    participant S as SharedDir (/tmp/.../virtctl-<version>)
    participant C as OCP Cluster (download_virtctl_from_cluster)

    W->>L: acquire lock (timeout)
    alt lock granted
        L-->>W: granted
        W->>S: check virtctl exists & is executable
        alt missing or invalid
            W->>C: download & extract virtctl into S
            C-->>S: place virtctl binary
        end
        W->>W: add_to_path(S)
        W->>L: release lock
        W-->>W: return virtctl path
    else timeout
        L-->>W: timeout
        W-->>W: raise TimeoutError
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • krcmarik
  • rnetser
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Additional refactoring (utilities/post_migration.py, utilities/utils.py, utilities/virtctl.py, CLAUDE.md) improves code quality but extends scope beyond issue #273 requirements. Separate refactoring work (get_cluster_version utility, add_to_path rename, validation documentation) into a distinct PR to keep this PR focused on the xdist race condition fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: introducing file locking in virtctl_binary fixture for pytest-xdist compatibility, matching the core objective in issue #273.
Linked Issues check ✅ Passed Code changes implement all key objectives: file locking mechanism added (filelock>=3.16.0), shared directory with coordination, lock timeout, and add_to_path behavior; conftest.py and pyproject.toml modified as required.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/issue-273-virtctl-xdist-lock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rh-bot-1

Copy link
Copy Markdown

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. LGTM Count: Minimum 0 /lgtm from reviewers
  3. Status Checks: All required status checks must pass
  4. No Blockers: No WIP, hold, conflict labels
  5. Verified: PR must be marked as verified (if verification is enabled)

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove

Reviewers:

  • krcmarik
  • myakove
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is automatically removed on each new commit
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

273 - Partially compliant

Compliant requirements:

  • Ensure only one process downloads virtctl when running with pytest-xdist
  • Solution must work both with and without xdist (single process mode)
  • Use a file locking mechanism to coordinate between workers
  • Use a shared directory accessible by all workers
  • Add filelock dependency to pyproject.toml if not already present

Non-compliant requirements:

  • Add/update tests if repo has tests

Requires further human verification:

  • Update README.md if applicable
  • Verify xdist run (pytest -n auto) no longer races and downloads only once
  • Verify non-xdist run still behaves correctly
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 Security concerns

Local file clobbering / symlink attack risk:
the fixture writes to a predictable path under /tmp (/tmp/pytest-shared-virtctl) and relies on a lock file there. On shared systems, /tmp is commonly world-writable; a malicious local user could pre-create virtctl or the directory as a symlink to another location, potentially causing unintended file writes or execution of an attacker-controlled virtctl. Mitigate by using a per-user directory (e.g., under $TMPDIR plus UID), enforcing directory permissions, and verifying virtctl_path is a regular file (not symlink) before use.

⚡ Recommended focus areas for review

Insecure /tmp usage

The shared directory is hard-coded under /tmp, which is typically world-writable. This can enable symlink/hardlink attacks or lock-file tampering by another local user, potentially causing overwrites or executing an unexpected virtctl binary. Consider creating the shared directory with restricted permissions (e.g., 0o700), validating that virtctl_path is a regular file owned by the current user, and/or deriving the shared location from TMPDIR/pytest base temp to avoid predictable global paths.

# Use a fixed shared directory visible to all xdist workers
shared_dir = Path("/tmp/pytest-shared-virtctl")
shared_dir.mkdir(parents=True, exist_ok=True)

lock_file = shared_dir / "virtctl.lock"
virtctl_path = shared_dir / "virtctl"

# File lock ensures only one process downloads
with filelock.FileLock(lock_file, timeout=600):
    if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
        download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)

# Add to PATH for all workers
_add_to_path(str(shared_dir))
return virtctl_path
Stale binary risk

Reusing a persistent shared virtctl across runs may leave a stale or incompatible binary (e.g., cluster upgraded) and silently skip re-download because the file exists and is executable. Consider verifying version/sha, embedding cluster version into the cache directory name, or forcing refresh based on metadata to prevent hard-to-debug mismatches.

# File lock ensures only one process downloads
with filelock.FileLock(lock_file, timeout=600):
    if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
        download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)

# Add to PATH for all workers
_add_to_path(str(shared_dir))
return virtctl_path
Lock timeout/UX

The lock timeout was set to 600 seconds but there is no explicit error handling/logging for lock acquisition failure. If the lock can’t be acquired, tests may fail with a generic exception. Consider catching filelock.Timeout to log a clearer message (including path and suggestions) to make CI failures actionable.

# File lock ensures only one process downloads
with filelock.FileLock(lock_file, timeout=600):
    if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
        download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)

💡 Tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

@qodo-code-review

qodo-code-review Bot commented Jan 22, 2026 •

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to cd353c6

CategorySuggestion                                                                                                                                    Impact
Possible bug
Namespace shared cache per user

Add a user-specific namespace to the shared cache path and make POSIX-only
security checks conditional to improve portability and prevent multi-user
collisions.

conftest.py [463-480]

-shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
+user_ns = f"uid-{os.getuid()}" if hasattr(os, "getuid") else f"user-{os.environ.get('USER', 'unknown')}"
+shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / user_ns / str(cluster_version)
 shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 
 # Security: check for symlink hijack attack
 if shared_dir.is_symlink():
     raise PermissionError(
         f"Security error: shared directory {shared_dir} is a symlink. This may indicate a hijack attempt."
     )
 
-# Security: verify ownership and enforce permissions
-current_uid = os.getuid()
-dir_stat = shared_dir.lstat()
-if dir_stat.st_uid != current_uid:
-    raise PermissionError(
-        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
-        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
-    )
-os.chmod(shared_dir, 0o700)
+# Security: verify ownership and enforce permissions (POSIX only)
+if hasattr(os, "getuid"):
+    current_uid = os.getuid()
+    dir_stat = shared_dir.lstat()
+    if dir_stat.st_uid != current_uid:
+        raise PermissionError(
+            f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
+            f"expected current user uid {current_uid}. This may indicate a hijack attempt."
+        )
+    os.chmod(shared_dir, 0o700)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the hardcoded cache path will cause permission errors in multi-user environments and that os.getuid() is not portable. The fix of namespacing the cache per user is a robust solution to a latent bug introduced in the PR.

Medium
Security
Prevent symlink redirection of paths

Add security checks to prevent symlink attacks on the virtctl.lock and virtctl
binary paths within the shared directory.

conftest.py [482-496]

 lock_file = shared_dir / "virtctl.lock"
 virtctl_path = shared_dir / "virtctl"
 
+if lock_file.is_symlink() or virtctl_path.is_symlink():
+    raise PermissionError(
+        f"Security error: unexpected symlink found in shared virtctl directory {shared_dir}."
+    )
+
 try:
     # File lock ensures only one process downloads
-    with filelock.FileLock(lock_file, timeout=600):
+    with filelock.FileLock(str(lock_file), timeout=600):
         if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
             download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
             # Validate binary was downloaded successfully
             if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                 raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
 except filelock.Timeout as err:
     raise TimeoutError(
         f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
     ) from err
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion enhances the security measures introduced in the PR by checking for symlinks on the lock file and binary path, which prevents a potential TOCTOU vulnerability where an attacker could redirect file operations.

Medium
Possible issue
Validate and parse version robustly

Add an explicit check in get_cluster_version to ensure the version string from
the cluster is not empty before parsing, and improve the exception handling for
parsing errors.

utilities/utils.py [557-575]

 def get_cluster_version(client: DynamicClient) -> Version:
     """Get the OpenShift cluster version.
 
     Args:
         client (DynamicClient): OpenShift client
 
     Returns:
         Version: Cluster version object (e.g., Version("4.20.1"))
 
     Raises:
         ValueError: If version cannot be determined
     """
     cluster_version = ClusterVersion(client=client, name="version", ensure_exists=True)
 
     try:
         version_str = cluster_version.instance.status.desired.version
+        if not version_str:
+            raise ValueError("ClusterVersion.status.desired.version is empty")
         return Version(version_str)
     except (AttributeError, KeyError) as e:
         raise ValueError(f"Failed to get OCP version: {e}") from e
+    except Exception as e:
+        raise ValueError(f"Failed to parse OCP version string '{version_str}': {e}") from e
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion improves the robustness of the new get_cluster_version function by explicitly checking for an empty version string, providing a clearer error message than the default InvalidVersion exception.

Low
  • More

Previous suggestions

✅ Suggestions up to commit 93ecb8b
CategorySuggestion                                                                                                                                    Impact
Security
✅ Harden shared directory against symlinks
Suggestion Impact:The commit added an explicit symlink check on the shared directory and updated the ownership check to use lstat() (avoiding following symlinks), plus documented the new PermissionError. It did not implement the suggested .resolve() call or the additional "must be a real directory" (is_dir) validation.

code diff:

@@ -448,6 +448,7 @@
     Raises:
         ValueError: If virtctl download fails or binary is not executable.
         PermissionError: If shared directory ownership doesn't match current user.
+        PermissionError: If shared directory is a symlink (hijack attempt).
         TimeoutError: If timeout waiting for file lock.
     """
     # Get cluster version for versioned caching
@@ -462,9 +463,15 @@
     shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
     shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 
+    # Security: check for symlink hijack attack
+    if shared_dir.is_symlink():
+        raise PermissionError(
+            f"Security error: shared directory {shared_dir} is a symlink. This may indicate a hijack attempt."
+        )
+
     # Security: verify ownership and enforce permissions
     current_uid = os.getuid()
-    dir_stat = shared_dir.stat()
+    dir_stat = shared_dir.lstat()
     if dir_stat.st_uid != current_uid:

Harden the shared temporary directory creation by resolving its path and
explicitly checking for symlinks to prevent potential symlink attacks.

conftest.py [462-473]

-shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
+shared_dir = (Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)).resolve()
 shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
+
+if shared_dir.is_symlink() or not shared_dir.is_dir():
+    raise PermissionError(f"Security error: shared directory {shared_dir} must be a real directory (not a symlink).")
 
 # Security: verify ownership and enforce permissions
 current_uid = os.getuid()
 dir_stat = shared_dir.stat()
 if dir_stat.st_uid != current_uid:
     raise PermissionError(
         f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
         f"expected current user uid {current_uid}. This may indicate a hijack attempt."
     )
 os.chmod(shared_dir, 0o700)
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies and mitigates a symlink attack vulnerability in the newly created shared temporary directory, which is a critical security improvement for shared environments.

High
Possible bug
Use downloader’s returned binary path

Use the path returned by download_virtctl_from_cluster instead of a hardcoded
path for the virtctl binary to make the code more robust.

conftest.py [475-493]

 lock_file = shared_dir / "virtctl.lock"
 virtctl_path = shared_dir / "virtctl"
 
 try:
     # File lock ensures only one process downloads
     with filelock.FileLock(lock_file, timeout=600):
         if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
-            download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
+            virtctl_path = download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
             # Validate binary was downloaded successfully
             if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                 raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
 except filelock.Timeout as err:
     raise TimeoutError(
         f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
     ) from err
 
 # Add to PATH for all workers
-add_to_path(str(shared_dir))
+add_to_path(str(virtctl_path.parent))
 return virtctl_path
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that using the return value of download_virtctl_from_cluster is more robust than assuming a hardcoded path, improving code maintainability and preventing potential bugs.

Medium

best practice
Prevent PATH substring matching issues

Improve the PATH update logic by splitting the PATH by os.pathsep and checking
for the exact directory to avoid incorrect substring matches.

utilities/virtctl.py [205-208]

 current_path = os.environ.get("PATH", "")
-if virtctl_dir not in current_path:
-    os.environ["PATH"] = f"{virtctl_dir}:{current_path}"
+path_parts = [p for p in current_path.split(os.pathsep) if p]
+if virtctl_dir not in path_parts:
+    os.environ["PATH"] = os.pathsep.join([virtctl_dir, *path_parts])
     LOGGER.info(f"Added {virtctl_dir} to PATH")
Suggestion importance[1-10]: 6

__

Why: The suggestion improves the robustness of the PATH modification logic by correctly checking for the directory's existence as a whole path component, avoiding potential bugs from partial substring matches.

Low
✅ Suggestions up to commit 6390b90
CategorySuggestion                                                                                                                                    Impact
Security
✅ Harden temp directory security checks
Suggestion Impact:The commit added a symlink check on the shared temp directory to mitigate hijack attacks (and updated the docstring accordingly), aligning with the security hardening suggestion. It did not implement the suggested cross-platform guard around os.getuid(); the code still calls os.getuid() unconditionally. It also switched to lstat() for the ownership check.

code diff:

@@ -448,6 +448,7 @@
     Raises:
         ValueError: If virtctl download fails or binary is not executable.
         PermissionError: If shared directory ownership doesn't match current user.
+        PermissionError: If shared directory is a symlink (hijack attempt).
         TimeoutError: If timeout waiting for file lock.
     """
     # Get cluster version for versioned caching
@@ -462,9 +463,15 @@
     shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
     shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 
+    # Security: check for symlink hijack attack
+    if shared_dir.is_symlink():
+        raise PermissionError(
+            f"Security error: shared directory {shared_dir} is a symlink. This may indicate a hijack attempt."
+        )
+
     # Security: verify ownership and enforce permissions
     current_uid = os.getuid()
-    dir_stat = shared_dir.stat()
+    dir_stat = shared_dir.lstat()
     if dir_stat.st_uid != current_uid:

Harden the temporary directory security by adding a symlink check and ensure
cross-platform compatibility by making the os.getuid() call conditional.

conftest.py [462-473]

 shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
 shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 
 # Security: verify ownership and enforce permissions
-current_uid = os.getuid()
-dir_stat = shared_dir.stat()
-if dir_stat.st_uid != current_uid:
-    raise PermissionError(
-        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
-        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
-    )
+if shared_dir.is_symlink():
+    raise PermissionError(f"Security error: shared directory {shared_dir} must not be a symlink")
+
+if hasattr(os, "getuid"):
+    current_uid = os.getuid()
+    dir_stat = shared_dir.stat()
+    if dir_stat.st_uid != current_uid:
+        raise PermissionError(
+            f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
+            f"expected current user uid {current_uid}. This may indicate a hijack attempt."
+        )
+
 os.chmod(shared_dir, 0o700)
Suggestion importance[1-10]: 8

__

Why: The suggestion addresses a valid security concern by checking for symlinks and improves cross-platform compatibility by conditionally calling the POSIX-specific os.getuid().

Medium
Possible bug
Make PATH updates cross-platform

Improve PATH modification to be cross-platform compatible and more robust by
using os.pathsep and checking for full path entries instead of substrings.

utilities/virtctl.py [199-208]

 def add_to_path(virtctl_dir: str) -> None:
     """Add directory to PATH environment variable.
 
     Args:
         virtctl_dir: Directory path to add to PATH
     """
     current_path = os.environ.get("PATH", "")
-    if virtctl_dir not in current_path:
-        os.environ["PATH"] = f"{virtctl_dir}:{current_path}"
+    entries = [p for p in current_path.split(os.pathsep) if p]
+    if virtctl_dir not in entries:
+        os.environ["PATH"] = os.pathsep.join([virtctl_dir, *entries])
         LOGGER.info(f"Added {virtctl_dir} to PATH")
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a cross-platform compatibility bug and a subtle string-matching bug in the PATH manipulation logic, offering a more robust implementation.

Medium
✅ Suggestions up to commit a2bedf8
CategorySuggestion                                                                                                                                    Impact
Security
✅ Harden shared temp lock handling
Suggestion Impact:The commit partially adopted the suggestion's error-handling improvement by capturing the filelock.Timeout exception as a variable and chaining it using "from err". However, it did not implement the suggested TimeoutError type, symlink checks, POSIX-conditional ownership/permission checks, or converting lock_file to a string for FileLock.

code diff:

-    except filelock.Timeout:
-        raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")
+    except filelock.Timeout as err:
+        raise RuntimeError(
+            f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
+        ) from err

Harden the virtctl_binary fixture by adding checks to prevent symlink attacks on
the shared directory and lock file, making POSIX-specific calls conditional for
better portability, and raising a more specific TimeoutError on lock timeouts.

conftest.py [461-488]

 shared_dir = (
     Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / f"{cluster_version.major}.{cluster_version.minor}"
 )
 shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 
-# Security: verify ownership and enforce permissions
-current_uid = os.getuid()
-dir_stat = shared_dir.stat()
-if dir_stat.st_uid != current_uid:
-    raise ValueError(
-        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
-        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
-    )
-os.chmod(shared_dir, 0o700)
+# Security: reject symlinks in shared path
+if shared_dir.is_symlink():
+    raise ValueError(f"Security error: shared directory {shared_dir} must not be a symlink")
+
+# Security/portability: enforce ownership and permissions on POSIX only
+if os.name == "posix" and hasattr(os, "getuid"):
+    current_uid = os.getuid()
+    dir_stat = shared_dir.stat()
+    if dir_stat.st_uid != current_uid:
+        raise ValueError(
+            f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
+            f"expected current user uid {current_uid}. This may indicate a hijack attempt."
+        )
+    os.chmod(shared_dir, 0o700)
 
 lock_file = shared_dir / "virtctl.lock"
+if lock_file.exists() and lock_file.is_symlink():
+    raise ValueError(f"Security error: lock file {lock_file} must not be a symlink")
+
 virtctl_path = shared_dir / "virtctl"
 
 try:
-    # File lock ensures only one process downloads
-    with filelock.FileLock(lock_file, timeout=600):
+    with filelock.FileLock(str(lock_file), timeout=600):
         if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
             download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
-            # Validate binary was downloaded successfully
             if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
                 raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
-except filelock.Timeout:
-    raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")
+except filelock.Timeout as e:
+    raise TimeoutError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.") from e
Suggestion importance[1-10]: 8

__

Why: The suggestion provides valid security enhancements by checking for symlinks and improves portability by making POSIX-specific code conditional, which are important for code running in shared or diverse environments.

Medium
Possible bug
Validate and parse version robustly

Improve the get_cluster_version function by adding a check for a None or empty
version_str and wrapping the Version(version_str) call in a try...except block
to raise a ValueError with a more descriptive message on failure.

utilities/utils.py [557-575]

 def get_cluster_version(client: DynamicClient) -> Version:
     """Get the OpenShift cluster version.
 
     Args:
         client (DynamicClient): OpenShift client
 
     Returns:
         Version: Cluster version object (e.g., Version("4.20.1"))
 
     Raises:
         ValueError: If version cannot be determined
     """
     cluster_version = ClusterVersion(client=client, name="version", ensure_exists=True)
 
     try:
         version_str = cluster_version.instance.status.desired.version
-        return Version(version_str)
     except (AttributeError, KeyError) as e:
         raise ValueError(f"Failed to get OCP version: {e}") from e
 
+    if version_str is None or version_str == "":
+        raise ValueError("Failed to get OCP version: empty version string")
+
+    try:
+        return Version(version_str)
+    except Exception as e:
+        raise ValueError(f"Failed to parse OCP version string '{version_str}': {e}") from e
+
Suggestion importance[1-10]: 7

__

Why: The suggestion improves error handling by adding an explicit check for an empty version string and provides a more informative error message, which enhances the function's robustness and debuggability.

Medium
Prevent PATH substring false-positives

To prevent false positives when checking if a directory is in the PATH, split
the PATH string by os.pathsep and check for an exact match in the resulting
list.

utilities/virtctl.py [199-208]

 def add_to_path(virtctl_dir: str) -> None:
     """Add directory to PATH environment variable.
 
     Args:
         virtctl_dir: Directory path to add to PATH
     """
     current_path = os.environ.get("PATH", "")
-    if virtctl_dir not in current_path:
-        os.environ["PATH"] = f"{virtctl_dir}:{current_path}"
+    path_entries = [p for p in current_path.split(os.pathsep) if p]
+    if virtctl_dir not in path_entries:
+        os.environ["PATH"] = os.pathsep.join([virtctl_dir, *path_entries])
         LOGGER.info(f"Added {virtctl_dir} to PATH")
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential bug in how the PATH environment variable is checked, and the proposed fix using os.pathsep is more robust and portable.

Low
✅ Suggestions up to commit 288a589
CategorySuggestion                                                                                                                                    Impact
Security
✅ Validate shared directory safety
Suggestion Impact:The commit added security validation for the shared virtctl directory by checking that it is owned by the current user (UID match) and enforcing restrictive permissions via chmod(0o700). It did not implement the full suggested checks (e.g., symlink/non-directory validation and explicit "too open" mode checks before use), but it clearly addresses part of the security concern around unsafe shared temp directories.

code diff:

+    shared_dir = (
+        Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / f"{cluster_version.major}.{cluster_version.minor}"
+    )
     shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
+
+    # Security: verify ownership and enforce permissions
+    current_uid = os.getuid()
+    dir_stat = shared_dir.stat()
+    if dir_stat.st_uid != current_uid:
+        raise ValueError(
+            f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
+            f"expected current user uid {current_uid}. This may indicate a hijack attempt."
+        )
+    os.chmod(shared_dir, 0o700)

Validate the shared temporary directory to prevent security risks. Before use,
check if the existing directory is a symlink, has insecure permissions, or is
not owned by the current user.

conftest.py [440-441]

 shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl"
-shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
+if shared_dir.exists():
+    if shared_dir.is_symlink() or not shared_dir.is_dir():
+        raise ValueError(f"Invalid shared virtctl directory path: {shared_dir}")
+    st = shared_dir.stat()
+    if st.st_uid != os.getuid():
+        raise ValueError(f"Shared virtctl directory not owned by current user: {shared_dir}")
+    if (st.st_mode & 0o077) != 0:
+        raise ValueError(f"Shared virtctl directory permissions too open: {oct(st.st_mode)}")
+else:
+    shared_dir.mkdir(mode=0o700, parents=True, exist_ok=False)
Suggestion importance[1-10]: 9

__

Why: The suggestion addresses a potential security vulnerability by adding checks for ownership and permissions of a shared temporary directory, which is critical when downloading and executing a binary.

High
✅ Prevent path escape execution
Suggestion Impact:The commit partially implements the suggestion by tightening validation from `exists()` to `is_file()` when checking the virtctl binary before/after download, reducing risk of using non-regular files. However, it does not implement the key path-traversal mitigation of resolving the path and verifying it remains within the shared_dir (no `.resolve()` / parents check was added).

code diff:

     lock_file = shared_dir / "virtctl.lock"
     virtctl_path = shared_dir / "virtctl"
@@ -446,16 +478,18 @@
     try:
         # File lock ensures only one process downloads
         with filelock.FileLock(lock_file, timeout=600):
-            if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+            if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                 download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
                 # Validate binary was downloaded successfully
-                if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+                if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                     raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")

Add security checks to prevent path traversal attacks. Before using
virtctl_path, resolve it and verify it remains within the expected shared_dir
and is a regular file.

conftest.py [443-453]

 lock_file = shared_dir / "virtctl.lock"
 virtctl_path = shared_dir / "virtctl"
 
 try:
     # File lock ensures only one process downloads
     with filelock.FileLock(lock_file, timeout=600):
-        if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+        resolved_virtctl_path = virtctl_path.resolve()
+        if shared_dir.resolve() not in resolved_virtctl_path.parents:
+            raise ValueError(f"Resolved virtctl path escapes shared directory: {resolved_virtctl_path}")
+
+        if (
+            not resolved_virtctl_path.exists()
+            or not resolved_virtctl_path.is_file()
+            or not os.access(resolved_virtctl_path, os.X_OK)
+        ):
             download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
-            # Validate binary was downloaded successfully
-            if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
-                raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
 
+        # Validate binary was downloaded successfully
+        if (
+            not resolved_virtctl_path.exists()
+            or not resolved_virtctl_path.is_file()
+            or not os.access(resolved_virtctl_path, os.X_OK)
+        ):
+            raise ValueError(f"Failed to download or make executable virtctl at {resolved_virtctl_path}")
+
Suggestion importance[1-10]: 9

__

Why: This is a critical security suggestion that prevents path traversal attacks by ensuring the virtctl binary path resolves within the intended shared directory, mitigating the risk of executing an unintended file.

High

best practice
✅ Use appropriate timeout exception
Suggestion Impact:The commit updated the exception handling to capture the original filelock.Timeout exception and chain it using "raise ... from err", preserving context as suggested. However, it still raises RuntimeError instead of switching to TimeoutError.

code diff:

-    except filelock.Timeout:
-        raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")
+    except filelock.Timeout as err:
+        raise RuntimeError(
+            f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
+        ) from err

Replace RuntimeError with the more specific TimeoutError when handling a file
lock timeout. Chain the original exception to preserve the full context for
easier debugging.

conftest.py [454-455]

-except filelock.Timeout:
-    raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")
+except filelock.Timeout as exc:
+    raise TimeoutError(
+        f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
+    ) from exc
Suggestion importance[1-10]: 6

__

Why: This suggestion improves exception handling by using the more specific TimeoutError instead of a generic RuntimeError and correctly chains the exceptions, which is a good practice.

Low
✅ Suggestions up to commit 86991d5
CategorySuggestion                                                                                                                                    Impact
Security
✅ Harden shared temp directory usage
Suggestion Impact:The commit replaced the hardcoded "/tmp/pytest-shared-virtctl" with a directory under tempfile.gettempdir() and created it with mode 0o700, improving portability and tightening permissions. However, it did not add the per-user suffix using os.getuid(), so the shared directory is still not user-specific.

code diff:

+import tempfile
 from collections.abc import Generator
 from copy import deepcopy
 from pathlib import Path
@@ -435,17 +436,23 @@
     Uses file locking to handle pytest-xdist parallel execution safely.
     The binary is downloaded to a shared directory that all workers can access.
     """
-    # Use a fixed shared directory visible to all xdist workers
-    shared_dir = Path("/tmp/pytest-shared-virtctl")
-    shared_dir.mkdir(parents=True, exist_ok=True)
+    # Use cross-platform temp directory with restricted permissions
+    shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl"
+    shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
 

Replace the hardcoded /tmp/pytest-shared-virtctl path with a per-user temporary
directory using tempfile.gettempdir() and os.getuid() to improve security and
prevent conflicts.

conftest.py [438-443]

-# Use a fixed shared directory visible to all xdist workers
-shared_dir = Path("/tmp/pytest-shared-virtctl")
-shared_dir.mkdir(parents=True, exist_ok=True)
+import tempfile
+...
+shared_dir = Path(tempfile.gettempdir()) / f"pytest-shared-virtctl-{os.getuid()}"
+shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
+shared_dir.chmod(0o700)
 
 lock_file = shared_dir / "virtctl.lock"
 virtctl_path = shared_dir / "virtctl"
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a security risk with a hardcoded, world-writable /tmp path and proposes a robust solution using a per-user directory, which prevents potential race conditions and permission issues in a multi-user environment.

Medium
✅ Suggestions up to commit 69690cf
CategorySuggestion                                                                                                                                    Impact
Possible bug
✅ Validate binary after download
Suggestion Impact:The commit added a post-download validation inside the file lock that checks virtctl exists and is executable, raising a ValueError if the download did not produce a usable binary. It also wrapped the lock in a try/except to surface lock timeouts as a RuntimeError.

code diff:

-    # File lock ensures only one process downloads
-    with filelock.FileLock(lock_file, timeout=600):
-        if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
-            download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
+    try:
+        # File lock ensures only one process downloads
+        with filelock.FileLock(lock_file, timeout=600):
+            if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+                download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
+                # Validate binary was downloaded successfully
+                if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+                    raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
+    except filelock.Timeout:
+        raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")

After downloading virtctl and before releasing the file lock, add a check to
verify that the binary was downloaded successfully and is executable, raising an
error if it is not.

conftest.py [445-448]

 # File lock ensures only one process downloads
 with filelock.FileLock(lock_file, timeout=600):
     if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
         download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
+        if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
+            raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out a potential flaw where a failed download could lead to subsequent test failures. Adding a verification step after the download, within the lock, significantly improves the robustness of the fixture.

Medium

best practice
✅ Use cross-platform temporary directory
Suggestion Impact:The commit added `import tempfile` and changed the shared directory from `Path("/tmp/pytest-shared-virtctl")` to `Path(tempfile.gettempdir()) / "pytest-shared-virtctl"`, making the temp path portable. It also added restricted permissions and extra error/timeout handling beyond the suggestion.

code diff:

+import tempfile
 from collections.abc import Generator
 from copy import deepcopy
 from pathlib import Path
@@ -435,17 +436,23 @@
     Uses file locking to handle pytest-xdist parallel execution safely.
     The binary is downloaded to a shared directory that all workers can access.
     """
-    # Use a fixed shared directory visible to all xdist workers
-    shared_dir = Path("/tmp/pytest-shared-virtctl")
-    shared_dir.mkdir(parents=True, exist_ok=True)
+    # Use cross-platform temp directory with restricted permissions
+    shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl"
+    shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

Replace the hardcoded /tmp path with tempfile.gettempdir() to ensure the
temporary directory path is portable across different operating systems.

conftest.py [438-440]

+import tempfile
+
 # Use a fixed shared directory visible to all xdist workers
-shared_dir = Path("/tmp/pytest-shared-virtctl")
+shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl"
 shared_dir.mkdir(parents=True, exist_ok=True)
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that hardcoding /tmp is not portable and proposes using tempfile.gettempdir() for cross-platform compatibility, which is a valid best practice.

Low

@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

273 - Partially compliant

Compliant requirements:

  • Ensure only one process downloads virtctl when running with pytest-xdist
  • Solution must work both with and without xdist (single process mode)
  • Use a file locking mechanism to coordinate between workers
  • Use a shared directory accessible by all workers
  • Code changes
  • Add filelock dependency to pyproject.toml if not already present

Non-compliant requirements:

  • Add/update tests (if repo has tests)

Requires further human verification:

  • Run tests with pytest-xdist (-n auto) and verify no race conditions
  • Run tests without xdist and verify normal behavior
  • Verify virtctl binary is downloaded only once with multiple workers
  • Update README.md (if applicable)
  • Update CLAUDE.md (if codebase structure changes)
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 Security concerns

Sensitive information exposure / Local privilege risk:
The use of a fixed path Path("/tmp/pytest-shared-virtctl") plus adding it to PATH can be abused on shared runners. Another local user/process could pre-create or replace /tmp/pytest-shared-virtctl/virtctl (or use symlinks) so tests execute a malicious binary. Mitigate by using a per-user/per-job directory (e.g., include os.getuid() and/or CI job id), enforcing secure permissions (0700), avoiding following symlinks, and validating the resulting binary before _add_to_path.

⚡ Recommended focus areas for review

Security

The fixture uses a fixed shared directory under /tmp. On shared/multi-user systems this can be tampered with (pre-create/symlink/replace virtctl or virtctl.lock), potentially leading to executing an attacker-controlled binary. Consider using a user-specific directory (include uid), ensuring strict permissions (e.g., 0700), and/or validating the extracted binary (ownership, mode, checksum/signature) before adding to PATH.

# Use a fixed shared directory visible to all xdist workers
shared_dir = Path("/tmp/pytest-shared-virtctl")
shared_dir.mkdir(parents=True, exist_ok=True)

lock_file = shared_dir / "virtctl.lock"
virtctl_path = shared_dir / "virtctl"

# File lock ensures only one process downloads
with filelock.FileLock(lock_file, timeout=600):
    if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
        download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)

# Add to PATH for all workers
_add_to_path(str(shared_dir))
return virtctl_path
Robustness

If a download/extract fails while holding the lock, subsequent workers may observe a partial or non-executable virtctl file and repeatedly attempt downloads. Consider downloading/extracting into a temporary subdir and atomically renaming into place on success, plus logging and clearer error handling when the binary is still missing after the locked section.

# File lock ensures only one process downloads
with filelock.FileLock(lock_file, timeout=600):
    if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
        download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)

# Add to PATH for all workers
_add_to_path(str(shared_dir))
return virtctl_path
API usage

Importing and calling _add_to_path (leading underscore) suggests a private API. Private functions are more likely to change and can break fixtures unexpectedly. Prefer a public helper (e.g., add_to_path) or move PATH manipulation into download_virtctl_from_cluster so the fixture relies on stable, exported behavior.

from utilities.virtctl import _add_to_path, download_virtctl_from_cluster

💡 Tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@conftest.py`:
- Around line 438-443: Add a clarifying comment above the shared_dir definition
explaining that the hard-coded path "/tmp/pytest-shared-virtctl" is
intentionally persistent across test runs for caching and visibility among xdist
workers, and should not be changed to a session-scoped temp directory; reference
the shared_dir variable and related artifacts lock_file and virtctl_path so
future maintainers understand this is deliberate for cross-worker caching and
not a leftover.
- Line 70: Rename the private helper _add_to_path to a public add_to_path:
update the function definition in utilities.virtctl from def _add_to_path(...)
to def add_to_path(...), update all internal references inside utilities.virtctl
to call add_to_path, and change the import in conftest.py from _add_to_path to
add_to_path; ensure any other modules importing the old name are updated and run
tests to confirm there are no remaining references to _add_to_path.
- Around line 446-448: The current existence/executable check around
filelock.FileLock for virtctl_path can reuse a stale binary from a different
cluster version; add version validation so we only accept a binary that matches
the target cluster version. Implement this by querying the cluster version
(using the ocp_admin_client) and comparing it to the virtctl binary version
before returning; either update _check_existing_virtctl to run the
cluster-version vs binary-version comparison and return False on mismatch, or in
conftest.py extract the cluster version and invalidate/delete the binary when it
doesn't match, then call download_virtctl_from_cluster(…) to fetch the correct
version; ensure the comparison uses stable identifiers (e.g., semantic version
or full cluster version string) and consider storing version metadata next to
the binary for quick checks.

Comment thread conftest.py Resolved
Comment thread conftest.py Resolved
Comment thread conftest.py Resolved
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

273 - Partially compliant

Compliant requirements:

  • Ensure only one process downloads virtctl when running with pytest-xdist
  • Solution must work both with and without xdist (single process mode)
  • Use file locking mechanism to coordinate between workers
  • Use a shared directory accessible by all workers
  • Add filelock dependency to pyproject.toml if not already present

Non-compliant requirements:

  • Add/update tests (if repo has tests)

Requires further human verification:

  • Update README.md (if applicable)
  • Update CLAUDE.md (if codebase structure changes)
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 Security concerns

LOW - Shared temp directory usage:
The fixture stores an executable in a predictable temp location (.../pytest-shared-virtctl). You do set directory permissions to 0o700, which helps, but on some platforms/filesystems permissions may not apply as expected. Consider additional validation (e.g., ensure owner matches current user, avoid following symlinks when reading/executing) to reduce local tampering risk on shared hosts.

⚡ Recommended focus areas for review

Private API

The fixture imports and uses _add_to_path (leading underscore), which implies a private/internal helper. This increases fragility because internal functions can change without contract; consider exposing a public helper (e.g., add_to_path) or moving PATH mutation into download_virtctl_from_cluster so callers don’t depend on private APIs.

from utilities.virtctl import _add_to_path, download_virtctl_from_cluster
Error handling

The download validation raises ValueError on failure, and converts lock timeout into a generic RuntimeError. Prefer more semantically appropriate exceptions (e.g., RuntimeError for operational failure throughout) and consider chaining original exceptions (raise ... from e) to preserve root cause for debugging in CI.

try:
    # File lock ensures only one process downloads
    with filelock.FileLock(lock_file, timeout=600):
        if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
            download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
            # Validate binary was downloaded successfully
            if not virtctl_path.exists() or not os.access(virtctl_path, os.X_OK):
                raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
except filelock.Timeout:
    raise RuntimeError(f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck.")
Shared path

Using a fixed shared directory under the system temp can cause cross-run interference (stale/corrupted binary) and concurrent jobs on the same host (e.g., multiple CI workspaces) sharing the same virtctl. Consider scoping the directory by cluster identifier/version or by repo/workspace (e.g., include a hash of API server/cluster name) to avoid unintended reuse while still sharing across xdist workers.

# Use cross-platform temp directory with restricted permissions
shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl"
shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

lock_file = shared_dir / "virtctl.lock"
virtctl_path = shared_dir / "virtctl"

💡 Tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

- Use cross-platform tempfile.gettempdir() instead of hardcoded /tmp
- Restrict directory permissions with mode=0o700
- Add explicit validation after download to fail fast
- Handle filelock.Timeout with descriptive error message

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@conftest.py`:
- Around line 428-438: The docstring for the fixture virtctl_binary is missing
the required Args/Returns/Raises sections; update the docstring to include an
Args section describing the ocp_admin_client parameter and its type/role, a
Returns section describing that a Path to the downloaded virtctl binary is
returned, and a Raises section documenting any exceptions that can be raised
(e.g., download or permission errors) during setup; keep the existing
descriptive summary and mention file-lock behavior in the body, using the
function name virtctl_binary and parameter ocp_admin_client to make the contract
explicit.
- Around line 439-445: The shared temp directory creation (shared_dir) can be
hijacked if it already exists with wrong owner or loose permissions; update
conftest.py to after creating shared_dir verify and enforce ownership and
permissions: check that shared_dir.owner/uid matches the current process
(os.getuid()/getpwuid) and fail if not, or chown it to the current uid/gid, and
set mode to 0o700 (use os.chmod) atomically; also ensure lock_file and
virtctl_path are created with safe flags/permissions (open/create with O_EXCL
where possible) and validate their ownership before use; refer to the existing
symbols shared_dir, lock_file, and virtctl_path to locate and modify the logic.

Comment thread conftest.py Resolved
Comment thread conftest.py Resolved
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jan 22, 2026
Use PermissionError instead of ValueError for ownership mismatch
to better reflect the security nature of the error. Use is_file()
instead of exists() to ensure directories don't count as valid
executables.
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

273 - Partially compliant

Compliant requirements:

  • Ensure only one process downloads virtctl when running with pytest-xdist
  • Solution must work both with and without xdist (single process mode)
  • Use file locking mechanism to coordinate between workers
  • Use a shared directory accessible by all workers
  • Code changes
  • Update CLAUDE.md (if codebase structure changes)
  • Add filelock dependency to pyproject.toml if not already present

Non-compliant requirements:

  • Add/update tests (if repo has tests)

Requires further human verification:

  • Run with pytest -n auto to confirm the lock reliably prevents parallel downloads and no worker hangs on the lock
  • Run without xdist to confirm behavior is unchanged
  • Verify virtctl is downloaded only once and reused across workers and subsequent runs (cache works as intended)
  • Confirm whether README.md needs an update (e.g., new dependency/tooling notes)
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

Insecure temp-path execution:
virtctl_binary caches an executable in a shared directory under the OS temp directory. Even with the uid ownership check, temp directories are a common attack surface (symlink/hardlink swaps, pre-creation races, or running as a privileged user). Since the cached file is later executed via PATH, further hardening is recommended (e.g., verify shared_dir.resolve() is within the intended base, avoid following symlinks for virtctl/lock paths, and ensure atomic file creation/extraction into a fresh temp dir before renaming into place).

⚡ Recommended focus areas for review

Cross-platform

The fixture uses os.getuid() and POSIX-style permissions (chmod 0o700). This will fail or behave unexpectedly on non-POSIX platforms (e.g., Windows CI). If the project is Linux-only that’s fine, but otherwise guard these calls or provide a platform-safe fallback.

def virtctl_binary(ocp_admin_client: "DynamicClient") -> Path:
    """Download and configure virtctl binary from the cluster.

    This fixture ensures virtctl is available in PATH for all tests
    that need to interact with VMs via virtctl commands.

    Uses file locking to handle pytest-xdist parallel execution safely.
    The binary is downloaded to a shared directory that all workers can access.
    The directory includes the cluster version for automatic cache invalidation
    when switching between clusters with different versions.

    Args:
        ocp_admin_client (DynamicClient): OpenShift cluster client for accessing
            the cluster to download the virtctl binary.

    Returns:
        Path: Path to the downloaded virtctl binary.

    Raises:
        ValueError: If virtctl download fails or binary is not executable.
        PermissionError: If shared directory ownership doesn't match current user.
        TimeoutError: If timeout waiting for file lock.
    """
    # Get cluster version for versioned caching
    cluster_version = get_cluster_version(ocp_admin_client)

    # Persistent shared directory for virtctl binary caching:
    # - Path is intentionally persistent across test runs (not session-scoped tmp)
    # - Visible to all pytest-xdist workers for cross-worker caching
    # - Avoids re-downloading virtctl on every test session
    # - Includes cluster version for automatic cache invalidation
    # - Do NOT change to pytest's tmp_path or similar session-scoped directories
    shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
    shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

    # Security: verify ownership and enforce permissions
    current_uid = os.getuid()
    dir_stat = shared_dir.stat()
    if dir_stat.st_uid != current_uid:
        raise PermissionError(
            f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
            f"expected current user uid {current_uid}. This may indicate a hijack attempt."
        )
    os.chmod(shared_dir, 0o700)

    lock_file = shared_dir / "virtctl.lock"
    virtctl_path = shared_dir / "virtctl"

    try:
        # File lock ensures only one process downloads
        with filelock.FileLock(lock_file, timeout=600):
            if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
                # Validate binary was downloaded successfully
                if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                    raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
    except filelock.Timeout as err:
        raise TimeoutError(
            f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
        ) from err

    # Add to PATH for all workers
    add_to_path(str(shared_dir))
    return virtctl_path
Temp dir safety

The shared directory under the system temp directory can be influenced by other processes/users. Although you check directory ownership, consider hardening against symlink/hardlink tricks (especially for virtctl.lock and virtctl) by ensuring the resolved path is under the expected directory and/or using atomic create patterns. This matters because the code later executes virtctl from that location.

shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

# Security: verify ownership and enforce permissions
current_uid = os.getuid()
dir_stat = shared_dir.stat()
if dir_stat.st_uid != current_uid:
    raise PermissionError(
        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
    )
os.chmod(shared_dir, 0o700)

lock_file = shared_dir / "virtctl.lock"
virtctl_path = shared_dir / "virtctl"

try:
    # File lock ensures only one process downloads
    with filelock.FileLock(lock_file, timeout=600):
        if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
            download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
            # Validate binary was downloaded successfully
            if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
except filelock.Timeout as err:
    raise TimeoutError(
        f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
    ) from err

# Add to PATH for all workers
add_to_path(str(shared_dir))
return virtctl_path
Error handling

get_cluster_version() now uses ClusterVersion(..., ensure_exists=True) and assumes status.desired.version exists. If the resource is missing/unready, this can raise exceptions other than AttributeError/KeyError (e.g., wrapper-specific errors). Consider catching and re-raising a clearer ValueError for fixture/utility callers so failures are actionable.

def get_cluster_version(client: DynamicClient) -> Version:
    """Get the OpenShift cluster version.

    Args:
        client (DynamicClient): OpenShift client

    Returns:
        Version: Cluster version object (e.g., Version("4.20.1"))

    Raises:
        ValueError: If version cannot be determined
    """
    cluster_version = ClusterVersion(client=client, name="version", ensure_exists=True)

    try:
        version_str = cluster_version.instance.status.desired.version
        return Version(version_str)
    except (AttributeError, KeyError) as e:
        raise ValueError(f"Failed to get OCP version: {e}") from e

💡 Tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

- Define validation (checking required inputs/config) vs assertions (verifying test outcomes)
- Add explicit definition in Validate at Source section
- Add cross-reference to Deterministic Tests section in wrong example

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@conftest.py`:
- Around line 456-473: The shared_dir symlink-hijack risk: before calling
shared_dir.stat() or os.chmod(), ensure the path is not a symlink by using a
non-following lstat check (e.g., Path.lstat() or os.lstat(shared_dir)) and fail
if it reports a symlink (or differing inode/owner) so we never follow
attacker-controlled symlinks; keep the existing checks (current_uid, dir_stat)
after confirming no symlink and then proceed to os.chmod(shared_dir, 0o700).
Ensure the guard specifically references shared_dir, uses lstat rather than
stat, and raises a PermissionError if a symlink is detected.

Comment thread conftest.py Resolved
@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

273 - Partially compliant

Compliant requirements:

  • Ensure only one process downloads virtctl when running with pytest-xdist
  • Solution must work both with and without xdist (single process mode)
  • Use file locking mechanism to coordinate between workers
  • Use a shared directory accessible by all workers
  • Code changes
  • Update CLAUDE.md (if codebase structure changes)
  • Add filelock dependency to pyproject.toml if not already present

Non-compliant requirements:

  • Add/update tests (if repo has tests)

Requires further human verification:

  • Run with pytest -n auto and confirm only a single download happens across workers
  • Run without xdist and confirm fixture behavior is unchanged
  • Verify cache invalidation works when switching between clusters of different versions
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

Shared temp directory usage:
virtctl_binary uses a predictable shared directory under the system temp directory. The PR adds good symlink and ownership checks, but there is still a potential local DoS risk where another local process can pre-create directories/files in the temp hierarchy to block the tests (causing PermissionError / lock contention). If this test suite can run on shared CI hosts with untrusted co-tenants, consider using a less predictable per-user base dir (e.g., include uid in the path) or a configurable shared path.

⚡ Recommended focus areas for review

Portability

The fixture uses os.getuid() and POSIX permission semantics (chmod 0o700). This will fail or behave unexpectedly on non-POSIX platforms (e.g., Windows). If the project is Linux-only it should be explicitly documented; otherwise guard these checks or use cross-platform alternatives.

shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

# Security: check for symlink hijack attack
if shared_dir.is_symlink():
    raise PermissionError(
        f"Security error: shared directory {shared_dir} is a symlink. This may indicate a hijack attempt."
    )

# Security: verify ownership and enforce permissions
current_uid = os.getuid()
dir_stat = shared_dir.lstat()
if dir_stat.st_uid != current_uid:
    raise PermissionError(
        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
    )
os.chmod(shared_dir, 0o700)
Cache Safety

Using a persistent directory under the system temp dir can accumulate stale/partial downloads. Consider handling corrupted caches (e.g., leftover virtctl.tar.gz / partial extraction) more defensively, and/or writing an atomic “download complete” marker so other workers never see an incomplete binary.

shared_dir = Path(tempfile.gettempdir()) / "pytest-shared-virtctl" / str(cluster_version)
shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

# Security: check for symlink hijack attack
if shared_dir.is_symlink():
    raise PermissionError(
        f"Security error: shared directory {shared_dir} is a symlink. This may indicate a hijack attempt."
    )

# Security: verify ownership and enforce permissions
current_uid = os.getuid()
dir_stat = shared_dir.lstat()
if dir_stat.st_uid != current_uid:
    raise PermissionError(
        f"Security error: shared directory {shared_dir} is owned by uid {dir_stat.st_uid}, "
        f"expected current user uid {current_uid}. This may indicate a hijack attempt."
    )
os.chmod(shared_dir, 0o700)

lock_file = shared_dir / "virtctl.lock"
virtctl_path = shared_dir / "virtctl"

try:
    # File lock ensures only one process downloads
    with filelock.FileLock(lock_file, timeout=600):
        if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
            download_virtctl_from_cluster(client=ocp_admin_client, download_dir=shared_dir)
            # Validate binary was downloaded successfully
            if not virtctl_path.is_file() or not os.access(virtctl_path, os.X_OK):
                raise ValueError(f"Failed to download or make executable virtctl at {virtctl_path}")
except filelock.Timeout as err:
    raise TimeoutError(
        f"Timeout (600s) waiting for virtctl lock at {lock_file}. Another process may be stuck."
    ) from err
Error Handling

get_cluster_version() relies on ensure_exists=True and then accesses status.desired.version without handling parsing errors. If the version string is missing or malformed, packaging.version.Version() can raise an exception that currently bubbles up unwrapped. Consider catching InvalidVersion and raising a clearer ValueError for callers.

def get_cluster_version(client: DynamicClient) -> Version:
    """Get the OpenShift cluster version.

    Args:
        client (DynamicClient): OpenShift client

    Returns:
        Version: Cluster version object (e.g., Version("4.20.1"))

    Raises:
        ValueError: If version cannot be determined
    """
    cluster_version = ClusterVersion(client=client, name="version", ensure_exists=True)

    try:
        version_str = cluster_version.instance.status.desired.version
        return Version(version_str)
    except (AttributeError, KeyError) as e:
        raise ValueError(f"Failed to get OCP version: {e}") from e

💡 Tool usage guide:

Overview:
The review tool scans the PR code changes, and generates a PR review which includes several types of feedbacks, such as possible PR issues, security threats and relevant test in the PR. More feedbacks can be added by configuring the tool.

The tool can be triggered automatically every time a new PR is opened, or can be invoked manually by commenting on any PR.

  • When commenting, to edit configurations related to the review tool (pr_reviewer section), use the following template:
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
[pr_reviewer]
some_config1=...
some_config2=...

See the review usage page for a comprehensive guide on using this tool.

Add is_symlink() check before stat/chmod operations to prevent
symlink hijack attacks on the shared virtctl cache directory.
Also change stat() to lstat() for ownership verification to
ensure we check the link itself, not the target.
@myakove
myakove merged commit ee3521b into main Jan 23, 2026
4 of 6 checks passed
@myakove
myakove deleted the fix/issue-273-virtctl-xdist-lock branch January 23, 2026 12:12
@redhat-qe-bot

Copy link
Copy Markdown

Failed to build and push ghcr.io/redhatqe/mtv-api-tests:latest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: virtctl_binary fixture race condition with pytest-xdist

5 participants