Skip to content

Fixed the issue:Supports specifying port ranges - #6281

Open
xianglongfei-8888 wants to merge 1 commit into
avocado-framework:masterfrom
xianglongfei-8888:dev
Open

Fixed the issue:Supports specifying port ranges#6281
xianglongfei-8888 wants to merge 1 commit into
avocado-framework:masterfrom
xianglongfei-8888:dev

Conversation

@xianglongfei-8888

@xianglongfei-8888 xianglongfei-8888 commented Mar 4, 2026

Copy link
Copy Markdown

Fixed the issue:Supports specifying port ranges, with automatic matching between listen and URI.

Issue: #5550

Command line:
avocado run examples/tests/passtest.py --status-server-disable-auto

--status-server-listen 127.0.0.1:8888-9000 (defalut) --status-server-uri 127.0.0.1:8888-9000 (defalut)

Use the testing program to occupy ports 8888/8889 for verification

Logs:
...
2026-03-04 15:06:58,073 avocado.job job L0291 INFO | 'run.status_server_listen': '127.0.0.1:8888-9000',
2026-03-04 15:06:58,073 avocado.job job L0291 INFO | 'run.status_server_uri': '127.0.0.1:8888-9000',
...
2026-03-04 15:06:58,439 avocado.job server L0068 INFO | Status server listening on 127.0.0.1:8890
2026-03-04 15:06:58,740 avocado.job testlogs L0132 INFO | examples/tests/passtest.py:PassTest.test: STARTED
2026-03-04 15:06:58,948 avocado.job testlogs L0138 INFO | examples/tests/passtest.py:PassTest.test: PASS
...

Signed-off-by: xianglongfei xianglongfei@uniontech.com

Summary by CodeRabbit

  • New Features

    • Status servers now support port-range addresses and automatically select an available port.
    • The default status-server address uses 127.0.0.1:8888-9000.
    • Listening endpoints are reported after startup.
    • Invalid or unavailable port ranges now produce clear run errors.
  • Documentation

    • Updated avocado run options to explain automatic selection, manual configuration, supported address formats, and configuration precedence.

@mr-avocado mr-avocado Bot moved this to Review Requested in Default project Mar 4, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility of the Avocado framework's status server by enabling the specification of port ranges for both listening and connection URIs. This change allows the system to automatically identify and utilize an available port within a defined range, which is particularly useful in dynamic or constrained network environments. The implementation also ensures consistent configuration by synchronizing the resolved port across relevant settings, streamlining the setup process for users.

Highlights

  • Port Range Support: Implemented the ability to specify port ranges (e.g., '127.0.0.1:8888-9000') for the status server's listening address and URI.
  • Automatic Port Resolution: Introduced a new utility function, resolve_listen_uri, which automatically finds and binds to an available port within a given range.
  • Configuration Synchronization: Ensured that when a port range is resolved to a specific port, both the status-server-listen and status-server-uri configurations are updated to reflect the chosen port.
Changelog
  • avocado/core/status/server.py
    • Added socket and LOG_JOB imports.
    • Introduced a new function resolve_listen_uri to parse port ranges and find an available port by attempting to bind to sockets.
    • Modified StatusServer.__init__ to use resolve_listen_uri for the _uri attribute.
    • Updated create_server to use rsplit for host/port separation and added logging for the listening URI.
  • avocado/plugins/runner_nrunner.py
    • Imported the new resolve_listen_uri function.
    • Integrated resolve_listen_uri into _create_status_server to process the run.status_server_listen configuration.
    • Added error handling for ValueError and OSError during port resolution, raising a JobError.
    • Updated test_suite configuration for run.status_server_listen and run.status_server_uri if a port range was resolved to a specific port.
  • man/avocado.rst
    • Updated the documentation for --status-server-listen to explain support for 'HOST:START-END' port ranges.
    • Updated the documentation for --status-server-uri to also mention support for 'HOST:START-END' port ranges.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a valuable feature by adding support for specifying port ranges for the status server. However, the implementation of the port range resolution logic is vulnerable to a Denial of Service (DoS) due to unhandled OverflowError exceptions when port numbers are outside the valid range (0-65535), potentially crashing the Avocado runner process via malformed command-line arguments. Additionally, a high-severity issue exists with IPv6 support in the new port-finding logic, currently limited to IPv4, and a minor refactoring is suggested to remove a duplicated logging statement.

Comment thread avocado/core/status/server.py Outdated
Comment on lines +30 to +33
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))

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.

high

The current implementation for finding an open port is hardcoded to IPv4 (socket.AF_INET) and does not correctly handle IPv6 addresses (e.g., [::1]). The bind call will fail for an IPv6 literal with brackets, and the address family is incorrect.

To properly support both IPv4 and IPv6, you should use socket.getaddrinfo to determine the correct address family and prepare the host string for the bind call.

Comment thread avocado/core/status/server.py Outdated
Comment on lines +21 to +28
start = int(start_s)
end = int(end_s)
if start > end:
raise ValueError(
f"Invalid port range (start > end) in status server URI: {uri}"
)

last_exc = None

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.

security-medium medium

The resolve_listen_uri function is vulnerable to a Denial of Service (DoS) vulnerability. It iterates through a user-provided port range and attempts to bind a socket to each port. If the port number is outside the valid range (0-65535), socket.bind() raises an OverflowError. This exception is not caught in the calling code in avocado/plugins/runner_nrunner.py (which only handles ValueError and OSError), causing the entire Avocado runner process to crash. An attacker or user providing a port range like 127.0.0.1:65530-70000 or 127.0.0.1:-1-10 can trigger this crash. Validating the port range before entering the loop is recommended.

    start = int(start_s)
    end = int(end_s)
    if start > end:
        raise ValueError(
            f"Invalid port range (start > end) in status server URI: {uri}"
        )
    if not (0 <= start <= 65535 and 0 <= end <= 65535):
        raise ValueError(
            f"Port range out of bounds (0-65535) in status server URI: {uri}"
        )

    last_exc = None

Comment thread avocado/core/status/server.py Outdated
Comment on lines +67 to +78
if ":" in self._uri:
host, port = self._uri.split(":")
host, port = self._uri.rsplit(":", 1)
port = int(port)
self._server_task = await asyncio.start_server(
self.cb, host=host, port=port, limit=limit
)
LOG_JOB.info("Status server listening on %s", self._uri)
else:
self._server_task = await asyncio.start_unix_server(
self.cb, path=self._uri, limit=limit
)
LOG_JOB.info("Status server listening on %s", self._uri)

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.

medium

The logging statement LOG_JOB.info(...) is duplicated in both branches of the if/else statement. You can avoid repetition by moving it after the block.

        if ":" in self._uri:
            host, port = self._uri.rsplit(":", 1)
            port = int(port)
            self._server_task = await asyncio.start_server(
                self.cb, host=host, port=port, limit=limit
            )
        else:
            self._server_task = await asyncio.start_unix_server(
                self.cb, path=self._uri, limit=limit
            )
        LOG_JOB.info("Status server listening on %s", self._uri)

@codecov

codecov Bot commented Mar 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.58974% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.65%. Comparing base (74b7379) to head (c19157a).
⚠️ Report is 63 commits behind head on master.

Files with missing lines Patch % Lines
avocado/core/status/server.py 42.85% 16 Missing ⚠️
avocado/plugins/runner_nrunner.py 45.45% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6281      +/-   ##
==========================================
+ Coverage   73.48%   73.65%   +0.17%     
==========================================
  Files         206      206              
  Lines       22494    22655     +161     
==========================================
+ Hits        16530    16687     +157     
- Misses       5964     5968       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xianglongfei-8888
xianglongfei-8888 force-pushed the dev branch 3 times, most recently from b7f0504 to c19157a Compare March 4, 2026 10:32

@clebergnu clebergnu 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.

Hi @xianglongfei-8888 , thanks for working on this.

Issue #5550 mentions that the outcome desired is to have this behavior by default, that is, it should allow users running avocado multiple times to not hit the status server port allocation. Have you looked into making the port range the default setting?

Comment thread avocado/core/status/server.py Outdated

last_exc = None
for port in range(start, end + 1):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

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.

@xianglongfei-8888 xianglongfei-8888 Mar 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This should really reuse avocado.utils.network.ports.find_free_port()

@clebergnu Thank you, it has been modified

@xianglongfei-8888
xianglongfei-8888 force-pushed the dev branch 2 times, most recently from 88b0438 to bf1310f Compare March 5, 2026 06:14
@xianglongfei-8888

xianglongfei-8888 commented Mar 5, 2026

Copy link
Copy Markdown
Author

Hi @xianglongfei-8888 , thanks for working on this.

Issue #5550 mentions that the outcome desired is to have this behavior by default, that is, it should allow users running avocado multiple times to not hit the status server port allocation. Have you looked into making the port range the default setting?

@clebergnu Thank you, it has been changed to default

@xianglongfei-8888

Copy link
Copy Markdown
Author

@richtja Could you help review the code? Thanks

@pevogam

pevogam commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Hi @xianglongfei-8888, would you mind rebasing on most recent master so we can clear the merge conflicts and possibly rerun the CI?

@xianglongfei-8888

Copy link
Copy Markdown
Author

Hi @xianglongfei-8888, would you mind rebasing on most recent master so we can clear the merge conflicts and possibly rerun the CI?

@pevogam Well noted with thanks.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dcca76e1-cda4-4e7b-8ba8-4e0273a2295e

📥 Commits

Reviewing files that changed from the base of the PR and between c7c50d5 and 2fafc38.

📒 Files selected for processing (2)
  • avocado/core/status/server.py
  • avocado/plugins/runner_nrunner.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • avocado/core/status/server.py
  • avocado/plugins/runner_nrunner.py

Walkthrough

Status-server listen URIs now support port ranges. The server selects an available port, rejects reversed or unavailable ranges, and logs the concrete endpoint. The nrunner plugin uses the new default range, converts resolution errors to JobError, and propagates resolved addresses. It also removes temporary UNIX-socket handling and cleanup. Documentation describes URI formats, automatic selection, and configuration precedence.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: support for specifying status-server port ranges.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
avocado/utils/podman.py (1)

1339-1345: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

AsyncPodman.execute fails when an argument is not a string.

Line 1340 calls " ".join(args) in the error path. The user branch at Line 1315 already converts each argument with str(arg), and Podman.execute was updated at Line 640 to use " ".join(str(a) for a in args). This error path was not updated. A non-string argument, for example the integer timeout used by restart, raises TypeError instead of PodmanException.

AsyncPodman.restart passes f"-t={timeout}", which is a string, so the current callers are safe. New callers that pass integers are not.

🐛 Proposed fix
     if proc.returncode:
-        command_args = " ".join(args)
+        command_args = " ".join(str(a) for a in args)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1339 - 1345, Update the error-path
command formatting in AsyncPodman.execute to convert every args element to str
before joining, matching the existing user branch and Podman.execute behavior.
Preserve the existing PodmanException message and return-code handling for
commands containing non-string arguments.
🟠 Major comments (22)
optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py-89-97 (1)

89-97: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not block the asyncio event loop with remote commands.

session.cmd_status_output() can block for its full timeout. is_task_alive() calls it up to ten times and then calls time.sleep(1). wait_task() invokes this method from the event loop. A slow or disconnected remote host can block all runtime tasks for more than 100 seconds. Setup hooks can block the same loop for setup_timeout.

Run the blocking session calls in an executor, or make the helper asynchronous. Replace time.sleep() with await asyncio.sleep() after making is_task_alive() asynchronous.

Also applies to: 151-160, 186-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py` around
lines 89 - 97, Make remote session operations non-blocking throughout
run_remote_cmd, is_task_alive, wait_task, and the setup-hook flow: execute
blocking session calls such as session.cmd_status_output() in an executor or
convert the helpers to async, and replace time.sleep() with await
asyncio.sleep() in is_task_alive. Ensure wait_task and setup hooks await the
asynchronous helpers so the asyncio event loop is never blocked during
remote-command timeouts.
optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py-197-199 (1)

197-199: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep spawner.remote.test_timeout effective for background tasks.

The trailing & makes cmd_status_output() wait only for the launch shell. It no longer enforces spawner.remote.test_timeout on task-run. A hung remote task can therefore outlive this spawner-specific timeout.

Enforce the timeout in the background process or track its PID and terminate it when the deadline expires.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py` around
lines 197 - 199, Update the background task execution around
RemoteSpawner.run_remote_cmd so spawner.remote.test_timeout remains effective
for task-run despite the trailing “&”. Enforce the deadline within the remote
background process or capture its PID and terminate it when the timeout expires,
while preserving the existing command launch and status handling.
avocado/plugins/runner_nrunner.py-231-235 (1)

231-235: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate the resolved port to a ranged status URI with a different host.

Line 234 only updates run.status_server_uri when it exactly equals listen or DEFAULT_SERVER_URI. A manual configuration such as run.status_server_listen=0.0.0.0:8888-9000 and run.status_server_uri=127.0.0.1:8888-9000 leaves tasks with the unresolved range. The server selects one port, but task status connections use an invalid endpoint.

When run.status_server_uri contains a range, preserve its host and replace its port specification with the port selected for resolved_listen. Validate that the configured URI range can represent that selected port.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/plugins/runner_nrunner.py` around lines 231 - 235, Update the
resolved-listen handling in the runner flow to also rewrite ranged
run.status_server_uri values when their host differs from
run.status_server_listen: preserve the URI host, replace its port range with the
port selected in resolved_listen, and validate that the configured range
includes that port. Retain the existing exact-match and DEFAULT_SERVER_URI
behavior, and avoid updating ranges that cannot represent the selected port.
avocado/core/suite.py-374-380 (1)

374-380: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow a resume with no remaining runnables.

If every runnable is in completed_set, Line 377 makes suite.tests empty. Lines 384-390 then raise TestSuiteError for a valid fully completed resume. Handle this case separately from unresolved references so avocado replay --resume can complete successfully when there is nothing left to run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/core/suite.py` around lines 374 - 380, Update the resume handling
around cls._runnable_name and suite.resume_start_index so an empty suite.tests
after filtering completed_set is treated as a valid fully completed resume.
Distinguish this from unresolved completed-test references, preserving
TestSuiteError for references that do not resolve while allowing avocado replay
--resume to finish successfully when all runnables were completed.
avocado/plugins/runner_nrunner.py-305-309 (1)

305-309: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bind the status server before starting workers.

resolve_listen_uri() only probes a free port. Another process can bind that port before serve_forever() runs. If asyncio.start_server() then fails, status_server_task finishes with an unobserved exception and self.status_server.close() later calls close() on an unset server object.

Create and await the server before creating workers. Convert bind failures to JobError. If the selected port is no longer available, retry another port in the configured range or fail the job with the bind error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/plugins/runner_nrunner.py` around lines 305 - 309, Update the
status-server startup flow around resolve_listen_uri() and status_server_task to
bind and await the server before creating workers. Handle asyncio.start_server()
bind failures by retrying an available port within the configured range, and
convert an unrecoverable bind error to JobError; only create status_server_task
after successful binding and ensure cleanup never closes an uninitialized
server.
avocado/plugins/replay.py-171-179 (1)

171-179: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Traverse replay ancestry and reject an empty source reference.

Line 178 matches every job with a missing job.replay.source_job_id, because every string starts with "". This adds unrelated PASS/SKIP names and can skip tests in the new replay. The scan also only finds jobs that directly reference results_dir; when a user resumes a replay job, it does not collect completed tests from that replay job’s source ancestors.

Resolve each non-empty source reference to a canonical job directory. Walk ancestors with cycle detection. Match descendants only against those canonical chain members. Add coverage for a replay whose source is another replay and for a job with no replay source key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/plugins/replay.py` around lines 171 - 179, Update the replay job
matching logic around the source reference and results directory scan: ignore
empty or missing job.replay.source_job_id values, resolve each non-empty
reference to a canonical job directory, and traverse its replay ancestry with
cycle detection. Match descendants only against directories in that canonical
ancestor chain, preserving direct results_dir and absolute-path support, and add
coverage for nested replays and jobs without a replay source.
avocado/utils/podman.py-1609-1611 (1)

1609-1611: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

AsyncPodman.collect_container_stats drops the user parameter.

Podman.collect_container_stats accepts user at Line 877 and forwards it to execute. The asynchronous version has no user parameter, and _collect_single_container_stats at Line 1661 calls execute without a user. Code that runs rootless containers as a specific user cannot collect statistics through the asynchronous API.

Add user=None to both asynchronous methods and forward it to execute.

Also applies to: 1652-1654, 1661-1663

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1609 - 1611, Update
AsyncPodman.collect_container_stats and _collect_single_container_stats to
accept user=None, propagate the value through their internal call, and pass it
to execute so asynchronous statistics collection supports user-specific rootless
containers.
avocado/utils/podman.py-852-857 (1)

852-857: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The synchronous and asynchronous su/nohup nesting differ.

The synchronous variant at Line 853 produces nohup su - USER -c '<cmd>' > /dev/null 2>&1 &. The asynchronous variant at Line 1586 produces su - USER -c 'nohup bash -c ... &'. In the asynchronous form, the background job belongs to the su login shell, and su exits when that shell exits. The lifetime of the collector and the meaning of the returned process.pid therefore differ between the two methods.

Both methods document the same behavior and return subprocess.Popen. Use one form in both.

Also applies to: 1584-1590

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 852 - 857, Unify the `su`/`nohup`
command nesting in the synchronous and asynchronous execution paths so both use
the same process-lifetime behavior and `subprocess.Popen` PID semantics. Update
the command construction around `full_command` in the shown path and its
corresponding asynchronous implementation, preserving the existing user and
non-user branches.
avocado/utils/podman.py-98-128 (1)

98-128: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Quote container_id and user in the su -c command strings.

get_container_port, save_container_logs, and wait_for_vllm_startup interpolate container_id into a shell command string that su -c runs. A container_id value with shell metacharacters executes arbitrary commands as the target user. The non-user branches pass argument lists and are safe, so the risk exists only in the su branches.

Use shlex.quote for every interpolated value.

🔒 Proposed change (apply the same pattern at Lines 195 and 295)
-                    f"XDG_RUNTIME_DIR={xdg_runtime_dir} podman port {container_id} {port}",
+                    f"XDG_RUNTIME_DIR={shlex.quote(xdg_runtime_dir)} "
+                    f"podman port {shlex.quote(str(container_id))} {int(port)}",

Also applies to: 178-209, 276-310

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 98 - 128, Update the su -c command
construction in get_container_port, save_container_logs, and
wait_for_vllm_startup to shell-quote every interpolated container_id and user
value using shlex.quote (including the XDG runtime path where interpolated).
Keep the non-user subprocess argument-list branches unchanged and apply the same
protection to all affected su command strings.
avocado/utils/podman.py-612-628 (1)

612-628: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Podman.execute and AsyncPodman.execute treat user="root" differently.

Podman.execute uses if user: at Line 623. AsyncPodman.execute uses if user and user != "root" at Line 1312. With user="root", the synchronous path runs su - root -c ... and the asynchronous path runs podman directly. su - root starts a login shell, which changes the environment, the working directory, and the XDG_RUNTIME_DIR value. The two paths therefore produce different results for the same argument.

Align the synchronous condition with the asynchronous one.

🐛 Proposed fix
-            if user:
+            if user and user != "root":
                 podman_cmd = [self.podman_bin] + list(args)

Also applies to: 1301-1329

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 612 - 628, Update the user-selection
condition in Podman.execute to bypass su when user is "root", matching
AsyncPodman.execute; only non-root user values should construct and run the su
command, while root and absent user values should execute podman directly.
avocado/utils/podman.py-846-857 (1)

846-857: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Unquoted interpolation into the shell=True command in both collect_container_aiu_metrics methods. Both methods call shlex.join for the podman arguments, then embed the result in a larger shell string where output_file, timeout, and user are interpolated raw. A path or user name with a space or a metacharacter breaks the command or executes extra shell code.

  • avocado/utils/podman.py#L846-L857: apply shlex.quote to output_file and to user, and cast timeout to int.
  • avocado/utils/podman.py#L1576-L1590: apply the same shlex.quote treatment to output_file and user, and cast timeout to int.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 846 - 857, In both
collect_container_aiu_metrics methods, update command construction to
shell-quote output_file and user before interpolation and cast timeout to int.
Apply these changes at avocado/utils/podman.py lines 846-857 and 1576-1590,
preserving the existing timeout, tee, and privilege-switching behavior.

Source: Linters/SAST tools

avocado/utils/podman.py-1223-1237 (1)

1223-1237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The password_stdin branch is unreachable when username and password are both set.

If username, password, and password_stdin=True are all supplied, the elif username and password branch at Line 1226 runs. It appends --username but never appends --password-stdin. podman login then prompts for a password on a non-interactive stdin.

Test password_stdin before the username and password combination.

🐛 Proposed fix
             if api_key:
                 args.extend(["--username", username or api_key_username])
                 args.extend(["--password", api_key])
+            elif password_stdin:
+                if username:
+                    args.extend(["--username", username])
+                args.append("--password-stdin")
             elif username and password:
                 args.extend(["--username", username])
-                if not password_stdin:
-                    args.extend(["--password", password])
-            elif password_stdin:
-                if username:
-                    args.extend(["--username", username])
-                args.append("--password-stdin")
+                args.extend(["--password", password])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1223 - 1237, Update the authentication
branch ordering in the surrounding argument-construction logic so the
password_stdin case is evaluated before the username-and-password case. When
password_stdin is true, append --password-stdin and include username when
provided, even if password is also set; preserve the existing api_key handling
and fallback validation.
avocado/utils/podman.py-504-581 (1)

504-581: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The SHA256 validation is unsound.

Line 564 tests actual_sha[:16] in sha_content. This performs a substring search of a 16-character prefix over the whole checksum file. Two problems follow:

  1. A truncated 64-bit prefix is not a valid integrity check.
  2. The prefix is not bound to the file it belongs to. The prefix of file A matches an entry for file B, so a corrupted file passes.

Also, Line 556 tests filename in sha_content, which matches any substring, and the except Exception at Line 572 records the failure but does not set is_valid = False.

Parse SHA256SUMS into a {filename: digest} mapping and compare the full digest.

🔒 Proposed change
-            with open(sha_file, "r", encoding="utf-8") as f:
-                sha_content = f.read()
-            for weight_file in weight_files:
-                filename = os.path.basename(weight_file)
-                if filename in sha_content:
+            expected = {}
+            with open(sha_file, "r", encoding="utf-8") as f:
+                for line in f:
+                    parts = line.split()
+                    if len(parts) == 2:
+                        expected[os.path.basename(parts[1].lstrip("*"))] = parts[0]
+            for weight_file in weight_files:
+                filename = os.path.basename(weight_file)
+                if filename in expected:
                     validation_messages.append(f"Validating SHA256 for: {filename}")
                     sha256_hash = hashlib.sha256()
                     try:
                         with open(weight_file, "rb") as f:
-                            for byte_block in iter(lambda: f.read(4096), b""):
+                            for byte_block in iter(lambda: f.read(1024 * 1024), b""):
                                 sha256_hash.update(byte_block)
                         actual_sha = sha256_hash.hexdigest()
-                        if actual_sha[:16] in sha_content:
+                        if actual_sha == expected[filename]:
                             validation_messages.append(f"SHA256 VALID: {filename}")
                         else:
                             validation_messages.append(f"SHA256 MISMATCH: {filename}")
-                            validation_messages.append(
-                                f"  Calculated: {actual_sha[:16]}..."
-                            )
                             is_valid = False
                     except Exception as sha_ex:
                         validation_messages.append(
                             f"SHA256 calculation failed for {filename}: {sha_ex}"
                         )
+                        is_valid = False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 504 - 581, Update
validate_model_with_sha to parse SHA256SUMS into a filename-to-full-digest
mapping, then validate each weight_file only against the digest associated with
its exact basename; compare the complete calculated SHA256 value rather than a
truncated prefix or unrestricted substring. Mark is_valid = False when a
checksum is missing, mismatched, or cannot be calculated/read, while preserving
the existing validation messages.
avocado/utils/podman.py-40-83 (1)

40-83: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle failures in setup_user_and_group and avoid a literal "None" password.

All subprocess.run calls use check=False and the return codes are ignored. The caller cannot detect that user creation or group modification failed. Later container operations then fail with unrelated errors.

If password is None, chpasswd receives the literal string username:None. Validate password before you call chpasswd.

🛠️ Proposed change
         if user_check.returncode != 0:
             log.info("Create user: %s", username)
-            subprocess.run(["useradd", "-m", username], check=False)
-            # Use input parameter to pass password securely to chpasswd
-            subprocess.run(
-                ["chpasswd"], input=f"{username}:{password}\n".encode(), check=False
-            )
+            result = subprocess.run(["useradd", "-m", username], check=False)
+            if result.returncode != 0:
+                raise PodmanException(f"Failed to create user {username}")
+            if password is not None:
+                # Use input parameter to pass password securely to chpasswd
+                result = subprocess.run(
+                    ["chpasswd"], input=f"{username}:{password}\n".encode(), check=False
+                )
+                if result.returncode != 0:
+                    raise PodmanException(f"Failed to set password for {username}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 40 - 83, Update setup_user_and_group to
validate that password is not None before invoking chpasswd, and replace ignored
check=False subprocess calls with failure propagation so user creation, password
setup, and group membership operations raise or otherwise report errors to the
caller. Preserve the existing root and add/remove branching behavior while
ensuring no command receives a literal "None" password.
avocado/utils/podman.py-954-997 (1)

954-997: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the UTC clock change, but drop the stderr sample-discard rule.

datetime.datetime.utcnow() is deprecated in Python 3.12, so the replacement with timezone-aware UTC is appropriate. Skipping collection samples when stderr is non-empty drops valid podman stats --no-stream --format=json output, because diagnostic messages can go to stderr while valid JSON is still returned on stdout. Keep logging the stderr warnings, but validate samples against stdout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 954 - 997, The stats collection loop
should continue parsing valid stdout even when stderr is non-empty. In the stats
sampling flow, retain the stderr warning log, but remove the branch that sleeps
and skips the sample; validate and process the decoded stdout JSON as before,
while preserving the timezone-aware UTC timestamp change.
avocado/utils/nvme.py-361-383 (1)

361-383: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not fabricate supported LBA formats when discovery fails.

get_supported_lba_formats() marks indices 0 and 1 as valid formats when the namespace query fails, but NVMe LBA format indices are only meaningful from the controller-supported LBAF list. An unsupported create_one_ns(..., flbas=-1) can then select the fabricated entry and issue nvme create-ns with an invalid FLBAS value. Return no formats, or raise NvmeException, when no namespace provides queryable LBA format data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/nvme.py` around lines 361 - 383, Update
get_supported_lba_formats() to remove the fallback that fabricates valid formats
at indices 0 and 1 when namespace discovery or querying fails. Return an empty
format list, or raise NvmeException, so create_one_ns cannot select an
unsupported FLBAS value from fabricated data.
avocado/utils/nvme.py-298-306 (1)

298-306: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the valid nvme attach-ns controller option.

nvme attach-ns accepts --controllers=... or short option -c, not -controllers=.... Replace this invalid shorthand with --controllers= so the namespace attach command is accepted and parsed as intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/nvme.py` around lines 298 - 306, Update the attach command
construction in the namespace-attachment flow to use the valid `--controllers=`
option instead of `-controllers=`. Preserve the existing controller identifier
and command execution behavior.
avocado/core/utils/entry_points.py-64-65 (1)

64-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Deduplicate entry points from all selection paths.

get_entry_points_for() documents deduplicated results, but lines 64-65 return _entry_points(group=group) raw. Apply the (ep.name, ep.value) deduplication after collecting entry points from both the dict branch and the group= branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/core/utils/entry_points.py` around lines 64 - 65, Update
get_entry_points_for so entry points returned from both the dict-based selection
path and the _entry_points(group=group) path are deduplicated by the (ep.name,
ep.value) pair before returning. Preserve the documented result ordering and
existing selection behavior.
avocado/core/nrunner/runnable.py-271-272 (1)

271-272: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve schema validation for package resources outside the filesystem.

resource_files("avocado").joinpath(...) returns a Traversable resource, not always a native path. Converting it with str() and checking os.path.exists() can fail for archive-loaded packages; then the fallback checks only /usr/share/avocado/schemas, so JSON Schema validation can be skipped. Open the package resource through the Traversable API, and keep native paths for the system-wide schema fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/core/nrunner/runnable.py` around lines 271 - 272, Update the schema
resolution in the runnable schema-validation flow to retain the Traversable
returned by resource_files("avocado").joinpath(...) and open package resources
through its Traversable API instead of converting to str() or relying on
os.path.exists(). Preserve native filesystem paths for the
/usr/share/avocado/schemas fallback so validation remains enabled for both
packaged and system-wide schemas.
.github/workflows/autils_migration_announcement.yml-31-31 (1)

31-31: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Replace the archived GitHub App token action across all workflows.

tibdex/github-app-token is archived and read-only, so it cannot receive security fixes. Migrate the token steps to actions/create-github-app-token and map the inputs: replace app_id, installation_id, and private_key with the supported inputs, then limit token permissions as needed.

Locations to update:

  • .github/workflows/pr_announcement.yml
  • .github/workflows/project.yml
  • .github/workflows/autils_migration_announcement.yml
  • .github/workflows/release.yml (both token-generator steps)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/autils_migration_announcement.yml at line 31, Replace the
archived tibdex/github-app-token action with actions/create-github-app-token in
.github/workflows/autils_migration_announcement.yml:31,
.github/workflows/pr_announcement.yml:15, and .github/workflows/project.yml:20,
and update each token step’s app_id, installation_id, and private_key inputs to
the supported names while restricting permissions as needed. Apply the same
migration to both token-generator steps in .github/workflows/release.yml.

Source: Linters/SAST tools

.github/workflows/setup.yml-168-169 (1)

168-169: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep devel-wide-system-installation system-wide.

This job name and step name say “system wide”, but python3 setup.py develop --user installs into the user site. The uninstall step uses python3 setup.py develop --uninstall without --user, so it does not reliably remove the user installation either. Keep installation and uninstallation scoped together. Use system-wide installation with sudo if this job should test system-wide commands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/setup.yml around lines 168 - 169, Update the installation
and corresponding uninstall commands in the devel-wide-system-installation job
to use the same system-wide scope: remove --user from setup.py develop and its
uninstall invocation, and run the installation with sudo as needed for
system-wide access. Keep the setuptools upgrade and existing job flow unchanged.
.github/workflows/release.yml-45-49 (1)

45-49: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Replace the archived GitHub App token action.

tibdex/github-app-token is archived and no longer maintained. The release workflow passes a private key to it and uses the generated token for release pushes and build-and-publish-eggs release uploads. Replace the occurrences in .github/workflows/release.yml with a maintained action, pin the replacement to an immutable commit, preserve the installation-id scope, and grant the generated token only the permissions needed for the following steps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 45 - 49, Replace every
tibdex/github-app-token usage in the release workflow with a maintained GitHub
App token action pinned to an immutable commit. Preserve the existing app ID,
private key, and installation ID inputs, using the replacement’s installation-id
input, and configure the generated token with only the permissions required by
the release push and build-and-publish-eggs upload steps.

Source: Linters/SAST tools

🟡 Minor comments (11)
docs/source/guides/contributor/chapters/how.rst-223-228 (1)

223-228: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Initialize the submodule before the first static-checks command.

The pre-commit setup uses static-checks/requirements.txt at Line 209. The new initialization commands appear at Lines 226-227. An existing clone can therefore fail before reaching these recovery instructions. Move the submodule commands before the first static-checks command.

Line 223 also calls static-checks a symlink. Describe it as a Git submodule instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/source/guides/contributor/chapters/how.rst` around lines 223 - 228,
Update the contributor setup instructions around the first static-checks command
to initialize and update the Git submodule beforehand, including the existing
recursive sync and update commands. Replace the description that calls the path
a symlink with wording identifying it as a Git submodule, while preserving the
requirements command afterward.
docs/source/quickstart/index.rst-238-249 (1)

238-249: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the setuptools quickstart warning wording and ordering.

The pkg_resources issue comes from setuptools 82+, not Python 3.11+, so the warning title should not say Python 3.11+. Move the warning before the default pip3 install --user avocado-framework command, or make it clear this only applies to builds/documentation installers that also install requirements-doc.txt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/source/quickstart/index.rst` around lines 238 - 249, The quickstart
warning currently attributes the pkg_resources problem to Python 3.11+ and
appears after the default installation command. Update the warning title and
text to identify setuptools 82+ as the trigger, and place it before the standard
pip3 install command so affected users see the workaround first; otherwise
explicitly scope it to documentation/build installations that install
requirements-doc.txt.
avocado/utils/software_manager/distro_packages.py-91-98 (1)

91-98: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Quote the custom tool path when calling process.run().

process.run() receives a string and SubProcess splits it with shlex.split() when shell=False, so a valid path containing whitespace can be parsed as the executable plus arguments, causing a failed version check or command injection if shell metacharacters are present. Pass the command as a shell array, or quote the path before interpolating it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/software_manager/distro_packages.py` around lines 91 - 98,
Update the custom_path branch in the distro package tool lookup to pass the
executable path safely to process.run(), preserving paths containing whitespace
and shell metacharacters. Prefer the supported shell-argument array form, or
quote custom_path before interpolation, while keeping the existing version
validation and return behavior unchanged.
avocado/utils/sysinfo.py-417-423 (1)

417-423: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Put AVOCADO_SYSINFO_CONF last in the load order.

ConfigParser.read() applies later files over earlier files. The current list makes ~/.config/avocado/sysinfo.conf override the package default, /etc/avocado/sysinfo.conf, and AVOCADO_SYSINFO_CONF. Move os.environ.get("AVOCADO_SYSINFO_CONF") to the end if it should be the highest-priority override.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/sysinfo.py` around lines 417 - 423, Update the candidates list
used by the sysinfo configuration loader so the AVOCADO_SYSINFO_CONF entry comes
last, after the package, system, and user configuration paths; preserve the
existing filtering and ConfigParser.read flow so the environment-provided
configuration has highest priority.
avocado/utils/podman.py-876-923 (1)

876-923: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

collect_container_stats blocks the caller for the whole duration.

The method loops until duration seconds elapse and calls time.sleep(interval). For container_id="all", the loop is repeated for each container in sequence, so the total wall time is duration * number_of_containers, not duration. The docstring does not state this.

Either collect the containers concurrently, or document the sequential behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 876 - 923, Update
collect_container_stats to document that container_id="all" processes containers
sequentially, causing total collection time to scale with duration multiplied by
the number of containers; clarify this behavior in the docstring without
changing the implementation.
avocado/utils/podman.py-754-805 (1)

754-805: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Splitting a string command with str.split() breaks quoted arguments.

Line 795 and Line 1521 use command.split(). An argument that contains spaces, for example echo "hello world", is split into separate tokens and the quotes are passed literally. Use shlex.split for shell-like tokenization.

🐛 Proposed fix (apply at both sites)
             if isinstance(command, str):
-                cmd_args.extend(command.split())
+                cmd_args.extend(shlex.split(command))

Also applies to: 1476-1531

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 754 - 805, Replace command.split() with
shlex.split() in both exec_command and the other command-building path around
the second occurrence, adding the required shlex import. Preserve list commands
unchanged so quoted string arguments are tokenized correctly without literal
quote characters.
avocado/utils/podman.py-24-35 (1)

24-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add tests for the new high-risk public API functions.

Podman and AsyncPodman already have some functional tests, but the new module-level functions — setup_user_and_group, get_container_port, save_container_logs, wait_for_vllm_startup, install_huggingface_cli, download_model_from_hf, and validate_model_with_sha — have no test coverage. Cover the shell-quoting paths, checksum parsing, and user propagation to prevent command-injection and validation regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 24 - 35, Add focused tests for the
module-level functions setup_user_and_group, get_container_port,
save_container_logs, wait_for_vllm_startup, install_huggingface_cli,
download_model_from_hf, and validate_model_with_sha. Mock subprocess and
filesystem/network boundaries to cover shell-quoting behavior, checksum parsing
and validation, and propagation of the user argument through generated commands,
including both success and relevant failure paths.
avocado/utils/podman.py-1749-1853 (1)

1749-1853: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The asynchronous inference method wraps the timeout error twice.

Line 1848 raises PodmanException("Inference request timed out after 300 seconds") inside the inner except asyncio.TimeoutError. The outer except Exception at Line 1850 catches that PodmanException and replaces the message with Failed to send inference request to {host}:{port}. The timeout cause is lost from the message, and the raise at Line 1848 also lacks a from clause.

Re-raise PodmanException unchanged in the outer handler.

🐛 Proposed fix
-            except asyncio.TimeoutError:
+            except asyncio.TimeoutError as timeout_ex:
                 proc.kill()
                 await proc.wait()
                 error_msg = "Inference request timed out after 300 seconds"
                 LOG.error(error_msg)
-                raise PodmanException(error_msg)
+                raise PodmanException(error_msg) from timeout_ex
 
+        except PodmanException:
+            raise
         except Exception as ex:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1749 - 1853, Update the outer exception
handler in send_vllm_inference_request to re-raise existing PodmanException
instances unchanged, while preserving the current wrapping and context for other
exceptions. This must retain the timeout message raised by the inner
asyncio.TimeoutError handler.

Source: Linters/SAST tools

avocado/utils/dmesg.py-214-216 (1)

214-216: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match complete kernel command-line parameters.

Line 216 matches substrings. foo=on also matches foo=only. An empty param also matches every command line. Split the command line into parameters and compare exact values. Add a test for prefix collisions.

Proposed fix
-            return param in cmdline
+            return param in cmdline.split()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/dmesg.py` around lines 214 - 216, Update the /proc/cmdline
check to split the command line into individual parameters and test exact
parameter membership, ensuring empty inputs and prefix collisions such as foo=on
versus foo=only do not match. Add a focused test covering the prefix-collision
case.
.github/workflows/prerelease.yml-20-23 (1)

20-23: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Python version in the step name.

The step installs Python 3.14. Its name still states Python 3.9. Update the name so CI logs match the configured runtime.

Proposed fix
-      - name: Set up Python 3.9
+      - name: Set up Python 3.14
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/prerelease.yml around lines 20 - 23, Update the
setup-python step name in the prerelease workflow to state Python 3.14, matching
the python-version configured for actions/setup-python.
.github/workflows/ci.yml-284-289 (1)

284-289: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Hash the avocado-vt download configuration.

The checkout stores avocado-vt under avocado-vt/, but the current glob starts at the workspace root with virttest/. hashFiles() therefore matches no files and returns an empty hash. All configuration revisions use the same cache key.

Proposed fix
-          key: avocado-vt-images-${{ runner.os }}-${{ hashFiles('virttest/shared/downloads/*.ini') }}
+          key: avocado-vt-images-${{ runner.os }}-${{ hashFiles('avocado-vt/virttest/shared/downloads/*.ini') }}

GitHub evaluates hashFiles() relative to GITHUB_WORKSPACE and returns an empty string for no matches. (docs.github.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 284 - 289, Update the cache key
expression in the actions/cache step to hash the avocado-vt download
configuration under the checked-out avocado-vt/ directory, ensuring hashFiles
matches the intended virttest/shared/downloads/*.ini files while preserving the
existing runner-specific key and restore prefix.
🧹 Nitpick comments (7)
avocado/utils/sysinfo.py (1)

176-179: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid sudo checks for commands outside the allowlist.

Line 177 constructs SysinfoCommand for every collected command. Its constructor loads configuration and calls can_sudo() before Line 178 checks is_sudo_cmd(). This repeats sudo capability checks for every sysinfo command, including commands that cannot use sudo.

Load and cache the configuration once. Check the command allowlist before the sudo capability check.

Also applies to: 412-425

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/sysinfo.py` around lines 176 - 179, Update the sysinfo command
execution flow around SysinfoCommand and the corresponding logic at the
additional sudo-check location to load and cache configuration once rather than
constructing/loading it for every command. Evaluate is_sudo_cmd(self.cmd) first,
and only call use_sudo() for commands in the sudo allowlist; preserve the
existing sudo_flag and logging behavior.
avocado/utils/podman.py (5)

228-233: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit return None in the final except block.

save_container_logs documents a return of the log path or None. The except Exception handler at Line 231 has no return, so the function falls through and returns None implicitly. The behavior is correct, but the exit path is inconsistent with the other handlers.

♻️ Proposed change
     except Exception as ex:
         log.warning("Failed to save container logs: %s", ex)
+        return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 228 - 233, Update the final except
Exception handler in save_container_logs to explicitly return None after logging
the failure, matching the existing timeout handler and documented return
contract.

511-512: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move glob and hashlib imports to the module header.

Both imports are unconditional inside the function body. The module already imports json, os, and shlex at the top.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 511 - 512, Move the unconditional glob
and hashlib imports from the function body to the module-level import section
alongside json, os, and shlex, and remove the now-redundant local imports.

368-443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Reconsider installing packages with pip install -U from a utility module.

install_huggingface_cli runs pip install -U huggingface_hub[cli] in the active environment. This mutates the host or virtual environment as a side effect of a test utility. It can upgrade transitive dependencies of Avocado itself.

If the function must stay, document the side effect and consider --user or an explicit target directory. avocado.utils.software_manager already provides an installation abstraction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 368 - 443, Update
install_huggingface_cli to avoid directly mutating the active environment with
pip install -U; reuse the installation abstraction from
avocado.utils.software_manager instead. If direct installation must remain,
document the side effect and constrain it to an explicit user or target
directory while preserving the existing verification behavior.

98-128: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use self.podman_bin instead of the hardcoded podman string.

_Podman.__init__ resolves the binary through which(podman_bin or "podman") and stores it in self.podman_bin. The module-level helpers get_container_port, save_container_logs, and wait_for_vllm_startup hardcode "podman". The Podman.get_container_port, Podman.save_container_logs, AsyncPodman.get_container_port, AsyncPodman.save_container_logs, and AsyncPodman.wait_for_vllm_startup wrappers therefore ignore a custom podman_bin.

Add a podman_bin parameter to the helpers and pass self.podman_bin from the wrappers.

Also applies to: 1275-1284

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 98 - 128, Update the module-level
helpers get_container_port, save_container_logs, and wait_for_vllm_startup to
accept a podman_bin parameter and use it for every Podman subprocess invocation
instead of hardcoding "podman". Pass self.podman_bin from the corresponding
Podman and AsyncPodman wrapper methods, preserving existing behavior when no
custom binary is provided.

273-365: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

wait_for_vllm_startup retrieves the full log on every iteration and tracks position by line count.

Each check calls podman logs without --tail, so the whole log is fetched every check_interval seconds. For a long-running container the transferred data grows without bound.

last_log_position counts lines of the previous full output. If the log is rotated or truncated, the index no longer matches and new lines are skipped.

Consider --since or --tail for the incremental display.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 273 - 365, The wait_for_vllm_startup
log polling currently fetches the entire container log each iteration and uses
an invalid line-count cursor after truncation or rotation. Update the podman
logs invocations to retrieve only incremental output for live-log display, using
an appropriate --since or --tail strategy, and adjust the tracking logic so
newly emitted lines are not skipped when logs shrink or rotate. Preserve failure
and success pattern checks against the available log content.
.github/workflows/setup.yml (1)

189-189: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Move workflow context values out of the shell source.

zizmor flags both commands. GitHub expands ${{ ... }} before the shell parses the script. If a trigger supplies attacker-controlled text, shell metacharacters can execute commands. Pass the values through env and expand quoted variables instead. GitHub warns that context values can contain untrusted input. (docs.github.com)

Proposed safe pattern
-      - run: echo "Job triggered by a ${{ github.event_name }} event on branch is ${{ github.ref }} in repository is ${{ github.repository }}, runner on ${{ runner.os }}"
+      - name: Log workflow context
+        env:
+          EVENT_NAME: ${{ github.event_name }}
+          REF: ${{ github.ref }}
+          REPOSITORY: ${{ github.repository }}
+          RUNNER_OS: ${{ runner.os }}
+        run: >-
+          printf 'Job triggered by a %s event on branch %s in repository %s, runner on %s\n'
+          "$EVENT_NAME" "$REF" "$REPOSITORY" "$RUNNER_OS"

Also applies to: 223-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/setup.yml at line 189, Update the workflow steps
containing the job-trigger and corresponding second context log commands to pass
each GitHub context value through the step’s env block, then reference the
resulting environment variables with quoted shell expansions in run. Remove all
direct `${{ ... }}` expressions from shell source while preserving the logged
values and message.

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46ca450f-628a-461b-9604-92e38b4d2afe

📥 Commits

Reviewing files that changed from the base of the PR and between 8fa8228 and f44dfb8.

📒 Files selected for processing (63)
  • .github/dependabot.yml
  • .github/workflows/ansible.yml
  • .github/workflows/autils_migration_announcement.yml
  • .github/workflows/ci.yml
  • .github/workflows/pr_announcement.yml
  • .github/workflows/prerelease.yml
  • .github/workflows/project.yml
  • .github/workflows/push_ci.yml
  • .github/workflows/release.yml
  • .github/workflows/setup.yml
  • .github/workflows/vmimage.yml
  • .github/workflows/weekly.yml
  • .pylintrc_utils
  • avocado/core/__init__.py
  • avocado/core/extension_manager.py
  • avocado/core/nrunner/app.py
  • avocado/core/nrunner/runnable.py
  • avocado/core/output.py
  • avocado/core/status/server.py
  • avocado/core/suite.py
  • avocado/core/task/runtime.py
  • avocado/core/utils/eggenv.py
  • avocado/core/utils/entry_points.py
  • avocado/core/utils/path.py
  • avocado/core/version.py
  • avocado/etc/avocado/sysinfo.conf
  • avocado/plugins/exec_path.py
  • avocado/plugins/replay.py
  • avocado/plugins/resolvers.py
  • avocado/plugins/runner_nrunner.py
  • avocado/plugins/runners/exec_test.py
  • avocado/plugins/runners/package.py
  • avocado/plugins/runners/podman_image.py
  • avocado/utils/archive.py
  • avocado/utils/cpu.py
  • avocado/utils/disk.py
  • avocado/utils/dmesg.py
  • avocado/utils/nvme.py
  • avocado/utils/pci.py
  • avocado/utils/pmem.py
  • avocado/utils/podman.py
  • avocado/utils/software_manager/backends/zypper.py
  • avocado/utils/software_manager/distro_packages.py
  • avocado/utils/sysinfo.py
  • docs/source/guides/contributor/chapters/how.rst
  • docs/source/quickstart/index.rst
  • man/avocado.rst
  • optional_plugins/html/avocado_result_html/__init__.py
  • optional_plugins/resultsdb/setup.py
  • optional_plugins/robot/tests/resolver.py
  • optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py
  • selftests/check.py
  • selftests/deployment/roles/avocado/tasks/rpm/examples.yml
  • selftests/deployment/roles/common/tasks/repos.yml
  • selftests/deployment/roles/tests/tasks/main.yml
  • selftests/pre_release/tests/check-copr-rpm-version.sh
  • selftests/unit/output.py
  • selftests/unit/safeloader_module.py
  • selftests/unit/test_replay_resume.py
  • selftests/unit/utils/cpu.py
  • selftests/unit/utils/pci.py
  • selftests/utils.py
  • setup.py

Comment thread avocado/utils/archive.py
Comment on lines +365 to 369
if self.is_tar:
self._engine.extraction_filter = getattr(
tarfile, "fully_trusted_filter", (lambda member, path: member)
)
self._engine.extractall(path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)avocado/utils/archive\.py$|requirements|setup\.py|setup\.cfg|pyproject\.toml|tox\.ini|VERSION|README|rtd|README'

echo "== archive outline =="
if [ -f avocado/utils/archive.py ]; then
  wc -l avocado/utils/archive.py
  ast-grep outline avocado/utils/archive.py || true
fi

echo "== relevant archive lines =="
if [ -f avocado/utils/archive.py ]; then
  sed -n '320,390p' avocado/utils/archive.py | cat -n -v
fi

echo "== python/tarfile filter availability and docs =="
python3 - <<'PY'
import tarfile, inspect, sys
print("python", sys.version)
for name in ["fully_trusted_filter", "data_filter", "default_filter"]:
    obj = getattr(tarfile, name, None)
    print(name, bool(obj), obj)
PY

echo "== python behavioral probe with crafted tar/ZIP archive names =="
python3 - <<'PY'
import tarfile, zipfile, tempfile, os, io, pathlib

def run_extractall(name, create_archive, extract_filter=None, use_tar_filter=True):
    target = tempfile.mkdtemp(prefix="avocado_archive_probe_")
    path = os.path.join(target, "sandbox")
    os.makedirs(path)
    archive = create_archive()
    try:
        with tarfile.open(name=name, fileobj=archive, mode=archive.mode if hasattr(archive, "mode") else None) as tar:
            if use_tar_filter and extract_filter is not None:
                tar.extractall(path=path, filter=extract_filter)
            elif tarfile.extractall is zipfile:
                # unreachable for tarfile fixture because opened through tarfile.open
                pass
            else:
                tar.extractall(path=path)
        entries = set()
        for root, dirs, files in os.walk(path):
            for d in dirs:
                entries.add(os.path.join(root, d))
            for f in files:
                entries.add(os.path.join(root, f))
        print(name, "ok", sorted(entries), "outside_sandbox", any(not str(x).startswith(path) for x in entries))
    except Exception as e:
        print(name, "error", repr(e))
        print(name, "outside_sandbox after exception", False)
    finally:
        import shutil
        shutil.rmtree(target, ignore_errors=True)

members = [
    ("bad-normal", tarfile.TarInfo("../../../tmp/evil.txt"), b"evil"),
    ("bad-link", tarfile.TarInfo("bad-link"), b"", type=tarfile.SYMTYPE, linkname="../../etc/passwd"),
]
for name, info, data in members:
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w") as tar:
        tar.addfile(info.replace=name[5:], fileobj=io.BytesIO(data))
    archive = io.BytesIO(buf.getvalue())
    archive.mode = "r"
    for f in [None, tarfile.data_filter, tarfile.fully_trusted_filter]:
        run_extractall(f"{name}_filter={getattr(f, '__name__', str(f))}", lambda a=archive: a, f)

zip_members = ["evil.txt", "../../../tmp/e.txt"]
for zname in zip_members:
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as z:
        z.writestr(zipfile.ZipInfo(zname + "/"), "")
        z.writestr(zipfile.ZipInfo(zname + "/out.txt"), "evil")
    archive = io.BytesIO(buf.getvalue())
    print("zip", zname, "unsupported_filter_attempt", list(zipfile.ZipFile(archive, "r").extractall.__kwdefaults__.keys()) if hasattr(zipfile.ZipFile(archive, "r").extractall, "__kwdefaults__") else "{}")
    target = tempfile.mkdtemp(prefix="avocado_zip_probe_")
    sandbox = os.path.join(target, "sandbox")
    os.makedirs(sandbox)
    with zipfile.ZipFile(archive, "r") as z:
        z.extractall(path=sandbox)
    entries = []
    for root, dirs, files in os.walk(sandbox):
        for e in dirs + files:
            entries.append(os.path.join(root, e))
    outside = [e for e in entries if not os.path.dirname(e).startswith(sandbox) or e.startswith(os.path.join(target, "tmp"))]
    print("zip", zname, "outdir", os.listdir(sandbox), "outside", outside)
    os.unlink(os.path.join(target, "tmp", "e.txt"))
    os.rmdir(os.path.join(target, "tmp"))
    os.rmdir(target)
PY

Repository: avocado-framework/avocado

Length of output: 7743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== usages of archive/archive_extractor classes =="
rg -n "class .*Archive|Archive|_engine|\.extract|extractall|fully_trusted_filter|data_filter" avocado/utils/archive.py avocado -g '*.py' || true

echo "== dependency/version constraints around Python =="
for f in pyproject.toml setup.cfg setup.py tox.ini .python-version .tool-versions requirements.txt requirements/*.txt docs/conf.py README.md README.rst README.txt; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done

echo "== deterministic path handling based on source =="
python3 - <<'PY'
from pathlib import Path
import ast, tarfile, zipfile
src = Path("avocado/utils/archive.py")
text = src.read_text()
tree = ast.parse(text)
print("contains fully_trusted_filter fallback:", "fully_trusted_filter" in text)
print("contains data_filter:", "data_filter" in text)
print("contains extractall call:", "extractall" in text)
PY

Repository: avocado-framework/avocado

Length of output: 22452


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)avocado/utils/archive\.py$|requirements|setup\.py|setup\.cfg|pyproject\.toml|tox\.ini|VERSION|README|rtd|README'

echo "== archive outline =="
if [ -f avocado/utils/archive.py ]; then
  wc -l avocado/utils/archive.py
  ast-grep outline avocado/utils/archive.py || true
fi

echo "== relevant archive lines =="
if [ -f avocado/utils/archive.py ]; then
  sed -n '320,390p' avocado/utils/archive.py | cat -n -v
fi

echo "== usages of archive/archive_extractor classes =="
rg -n "class .*Archive|Archive|_engine|\.extract|extractall|fully_trusted_filter|data_filter" avocado/utils/archive.py avocado -g '*.py' || true

echo "== dependency/version constraints around Python =="
for f in pyproject.toml setup.cfg setup.py tox.ini .python-version .tool-frames requirements.txt requirements/*.txt docs/conf.py README.md README.rst README.txt; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done

echo "== python/tarfile filter availability and docs =="
python3 - <<'PY'
import tarfile, sys
print("python", sys.version)
for name in ["fully_trusted_filter", "data_filter", "default_filter"]:
    obj = getattr(tarfile, name, None)
    print(name, bool(obj), obj and obj.__name__)
PY

echo "== deterministic path handling based on source =="
python3 - <<'PY'
from pathlib import Path
src = Path("avocado/utils/archive.py")
text = src.read_text()
print("contains fully_trusted_filter fallback:", "fully_trusted_filter" in text)
print("contains data_filter:", "data_filter" in text)
print("contains extractall call:", "extractall" in text)
PY

echo "== behavioral probe with crafted tar/ZIP archive names =="
python3 - <<'PY'
import tarfile, zipfile, tempfile, os, io

def run_tar_extract(name, archive, extract_filter):
    target = tempfile.mkdtemp(prefix="avocado_archive_probe_")
    path = os.path.join(target, "sandbox")
    os.makedirs(path)
    archive.seek(0, io.SEEK_SET)
    try:
        with tarfile.open(fileobj=archive, mode="r") as tar:
            if extract_filter is not None:
                tar.extractall(path=path, filter=extract_filter)
            else:
                tar.extractall(path=path)
        entries = set()
        for root, dirs, files in os.walk(path):
            for d in dirs:
                entries.add(os.path.join(root, d))
            for f in files:
                entries.add(os.path.join(root, f))
        print("tar", name, "fs_entries", sorted(entries), "outside_sandbox", [e for e in entries if not os.path.abspath(e).startswith(os.path.abspath(path))])
    except Exception as e:
        print("tar", name, "error", repr(e).splitlines()[0], "outside_sandbox", False)
    finally:
        import shutil
        shutil.rmtree(target, ignore_errors=True)

members = [
    ("bad-normal", tarfile.TarInfo("../../../tmp/evil.txt"), b"evil"),
    ("bad-link", tarfile.TarInfo("bad-link"), b"", type=tarfile.SYMTYPE, linkname="../../etc/passwd"),
]
for name, info, data in members:
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w") as tar:
        info.name = name
        tar.addfile(info, fileobj=io.BytesIO(data))
    archive = io.BytesIO(buf.getvalue())
    for f in [None, getattr(tarfile, "data_filter", None), getattr(tarfile, "fully_trusted_filter", None)]:
        run_tar_extract(f"{name}_filter={getattr(f, '__name__', 'none')}", archive, f)

target = tempfile.mkdtemp(prefix="avocado_zip_probe_")
sandbox = os.path.join(target, "sandbox")
os.makedirs(sandbox)
for zname in ["../../../tmp/e.txt", "../../../etc/e.txt"]:
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as z:
        z.writestr(zipfile.ZipInfo(zname), "evil")
    with zipfile.ZipFile(buf, "r") as z:
        z.extractall(path=sandbox)
    entries = []
    for root, dirs, files in os.walk(sandbox):
        for e in dirs + files:
            entries.append(os.path.join(root, e))
    outside = [e for e in entries if not os.path.abspath(e).startswith(os.path.abspath(sandbox))]
    print("zip", zname, "files_in_sandbox_root", os.listdir(sandbox), "outside_sandbox", outside)
os.rmdir(sandbox)
os.rmdir(target)
PY

Repository: avocado-framework/avocado

Length of output: 29967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AVOCADO_VERSION =="
cat -n VERSION

echo "== python package metadata =="
for f in setup.cfg pyproject.toml tox.ini docs/source/conf.py; do
  [ -f "$f" ] && { echo "--- $f"; rg -n "python_requires|Programming Language :: Python|python_version|python\s+3|requires-python|trove" "$f" -C 2 || true; }
done

echo "== archive imports and extraction helper =="
sed -n '1,80p' avocado/utils/archive.py | cat -n
sed -n '360,390p' avocado/utils/archive.py | cat -n
sed -n '378,450p' avocado/utils/archive.py | cat -n

echo "== Python 3.9 tarfile filter and crafted member behavior =="
python3 - <<'PY'
import tarfile, zipfile, tempfile, os, io, pathlib, shutil, importlib, pkgutil

def probe_tar(name, buf, engine_filter=None):
    target = tempfile.mkdtemp(prefix="avocado_tar_probe_")
    sandbox = os.path.join(target, "sandbox")
    os.makedirs(sandbox)
    try:
        with tarfile.open(fileobj=buf, mode="r") as tar:
            if engine_filter is not None:
                # Mirrors the current ArchiveFile.extract implementation at line 366-369.
                tar.extraction_filter = engine_filter
            tar.extractall(path=sandbox)
        outside = []
        for root, dirs, files in os.walk(sandbox):
            for e in dirs + files:
                path = os.path.join(root, e)
                if not os.path.abspath(path).startswith(os.path.abspath(sandbox)):
                    outside.append((path, os.path.islink(path), os.readlink(path) if os.path.islink(path) else None))
        print("tar", name, "engine_filter", getattr(engine_filter, "__name__", str(engine_filter)), len(outside), outside)
    except Exception as e:
        print("tar", name, "engine_filter", getattr(engine_filter, "__name__", str(engine_filter)), "ERROR", repr(e).splitlines()[0])
    finally:
        shutil.rmtree(target, ignore_errors=True)

tmp = pathlib.Path("/tmp")
if not tmp.exists():
    tmp.mkdir(mode=0o755, parents=True, exist_ok=True)

members = [
    ("bad-normal", tarfile.TarInfo("../../../tmp/avocado_e.txt"), b"evil"),
    ("bad-link", tarfile.TarInfo("bad-link"), b"", type=tarfile.SYMTYPE, linkname="../tmp/avocado_e.txt"),
    ("absolute", tarfile.TarInfo("/tmp/avocado_e.txt"), b"evil"),
    ("zero-size-link", tarfile.TarInfo("link"), b"", type=tarfile.SYMTYPE, linkname="../tmp/avocado_e.txt"),
]
for name, info, data in members:
    buf = io.BytesIO(io.BytesIO()).getvalue()  # empty buffer
    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w") as tar:
        tar.addfile(info, fileobj=io.BytesIO(data))
    for filter_value in [None, tarfile.data_filter, tarfile.fully_trusted_filter]:
        probe_tar(name, io.BytesIO(buf.getvalue()), filter_value)

probe_tar("tricky-continued-data-filter", io.BytesIO(tarfile.TarInfo("a").tarfile), tarfile.data_filter)

print("zip extractall kwdefaults", zipfile.ZipFile(io.BytesIO(), "r").extractall.__kwdefaults__ if hasattr(zipfile.ZipFile(io.BytesIO(), "r").extractall, "__kwdefaults__") else None)

for zname in ["../../../tmp/avocado_z.txt", "/tmp/avocado_z.txt"]:
    target = tempfile.mkdtemp(prefix="avocado_zip_probe_")
    sandbox = os.path.join(target, "sandbox2")
    os.makedirs(sandbox)
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as z:
        z.writestr(zipfile.ZipInfo(zname), "evil")
    with zipfile.ZipFile(buf, "r") as z:
        z.extractall(path=sandbox)
    outside = []
    for root, dirs, files in os.walk(sandbox):
        for e in dirs + files:
            path = os.path.join(root, e)
            if not os.path.abspath(path).startswith(os.path.abspath(sandbox)):
                outside.append(path)
    print("zip", zname, "outside", outside)
    shutil.rmtree(target, ignore_errors=True)

# Remove test files used by crafted samples if present.
for p in [tmp / "avocado_e.txt", tmp / "avocado_z.txt"]:
    if p.exists():
        p.unlink(missing_ok=True)
PY

Repository: avocado-framework/avocado

Length of output: 8615


🌐 Web query:

Python tarfile.extractall fully_trusted_filter data_filter CVE-2007-6063

💡 Result:

The vulnerability CVE-2007-4559 refers to a long-standing directory traversal flaw in the Python tarfile module's extract and extractall methods [1][2]. Because these methods historically trusted archive metadata, a maliciously crafted tar file could use ".." path components to overwrite arbitrary files outside the intended destination directory [2][3]. To address this, Python introduced the filter mechanism via PEP 706 [4][5]. This allows developers to restrict which files and paths are extracted [6][7]. Key Filters and Mechanisms: 1. 'data' filter (tarfile.data_filter): This is the recommended secure default [6][7]. It prevents directory traversal by blocking paths that escape the destination, refuses to extract dangerous file types (e.g., device files, pipes), and restricts unsafe symbolic/hard links [6][8][7]. 2. 'fully_trusted' filter (tarfile.fully_trusted_filter): This maintains the original, insecure behavior [4][9]. It honors all archive metadata without restriction and should only be used if the archive source is entirely trusted [4][9]. 3. 'tar' filter (tarfile.tar_filter): A middle ground that blocks directory traversal but remains more permissive than the 'data' filter regarding certain UNIX-specific features [10][6]. Implementation: Upgrading your Python interpreter is necessary but not sufficient, as the 'filter' argument must be explicitly used in your code [3]. You should use the 'data' filter as follows: tar.extractall(path, filter='data') To ensure protection process-wide—including for third-party libraries—you can set the filter globally [3]: tarfile.TarFile.extraction_filter = staticmethod(tarfile.data_filter) Compatibility Note: These features were introduced in Python 3.12 and backported to several older versions (e.g., 3.8.17+, 3.9.17+, 3.10.12+, 3.11.4+) [3]. You can check for availability in your environment using hasattr(tarfile, 'data_filter') [6][11]. In Python 3.14 and later, the 'data' filter will become the default [4][5].

Citations:


🌐 Web query:

Python zipfile.extractall path traversal before_filter zipfile.Path traversal

💡 Result:

Python's zipfile.extractall and extract methods include built-in mitigations against path traversal (often referred to as Zip Slip) [1][2][3]. These methods sanitize file names by removing leading slashes, drive letters, and parent directory references ('..') before creating files on the filesystem [4][3]. Despite these protections, the official Python documentation maintains a warning advising against extracting archives from untrusted sources [5][6]. This is due to a few key factors: 1. Potential for Bypass or Edge Cases: While the internal sanitization logic covers many common scenarios, it may not be exhaustive against all platform-specific nuances or future bypass techniques [1][7]. 2. Zip Bombs: The module does not inherently protect against denial-of-service attacks known as "zip bombs," which can exhaust system resources (disk space, memory, or inodes) during extraction [7]. 3. Complexity of Security: The Python development team considers it best practice to keep this warning to ensure users remain cautious when handling untrusted input, even if the implementation is significantly safer than it was in older versions (e.g., prior to the 2013/Python 2.7.4 era) [8][1][7]. The zipfile.Path class, which provides an alternative way to access archive contents, does not automatically sanitize file names [5][6]. If you are using zipfile.Path to iterate and manually extract files, it is your responsibility to validate paths to prevent traversal [5][6]. Best practices for handling untrusted ZIP files: - Use zipfile.extractall or zipfile.extract for general use, as they include the standard sanitization [2][3]. - For high-security environments, manually validate that the resolved output path starts with the intended base directory using os.path.abspath and os.path.commonpath [5][9]. - Be aware of the risks beyond path traversal, such as zip bombs [7]. - Monitor for relevant CVEs in third-party libraries (e.g., python-libarchive), as vulnerabilities often arise in external wrappers rather than the core zipfile module [10][11][12].

Citations:


Do not set the tar extraction filter to fully trusted.

ArchiveFile.extract() intentionally selects tarfile.fully_trusted_filter, which lets crafted tar members with .. paths, absolute names, or unsafe symlinks write outside path before extractall() runs. Only use this for trusted input. Prefer tarfile.data_filter and add a compatibility fallback that rejects traversal names and links that escape the destination.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 368-368: Calling extractall() on a zipfile.ZipFile or tarfile archive without validating member paths lets a crafted entry (e.g. "../../etc/passwd") write outside the destination directory (Zip Slip). Validate each member resolves inside the target directory, or pass a safe filter (tarfile: filter="data" / tarfile.data_filter).
Context: self._engine.extractall(path)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(archive-extractall-path-traversal-python)

🪛 Ruff (0.16.1)

[error] 369-369: Uses of tarfile.extractall()

(S202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/archive.py` around lines 365 - 369, The ArchiveFile.extract()
tar path must not select tarfile.fully_trusted_filter, since it permits members
to escape the destination. Use tarfile.data_filter when available, with a
compatibility fallback that rejects traversal, absolute paths, and symlinks or
links escaping path before calling self._engine.extractall(path).

Source: Linters/SAST tools

Comment thread avocado/utils/nvme.py
Comment on lines +510 to +533
# Determine retry strategy based on flbas parameter
# Only enable auto-detection fallback for default flbas=0
should_retry_with_auto_detect = flbas == 0

# Handle explicit auto-detection request (flbas=-1)
# This skips the FLBAS=0 attempt and goes straight to optimal detection
if flbas == -1:
try:
flbas = get_optimal_flbas(controller_name)
LOGGER.info(
f"Auto-detected FLBAS={flbas} for namespace creation on {controller_name}"
)
except NvmeException as e:
LOGGER.error(f"Failed to auto-detect FLBAS: {e}")
raise NvmeException(
f"Cannot create namespace: FLBAS auto-detection failed: {e}"
)

# Build and execute namespace creation command
cmd = f"nvme create-ns /dev/{controller_name} --nsze={ns_size} --ncap={ns_size} --flbas={flbas} --dps=0"
if shared_ns:
cmd = f"{cmd} -m 1"
if process.system(cmd, shell=True, ignore_status=True):
raise NvmeException(f"namespace create command failed {cmd}")

result = process.system(cmd, shell=True, ignore_status=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate nvme.py =="
fd -a 'nvme.py|nvme.*' . | sed 's#^\./##' | head -50

echo "== inspect relevant avocado/utils/nvme.py section =="
wc -l avocado/utils/nvme.py
sed -n '460,550p' avocado/utils/nvme.py

echo "== locate create_namespace definitions/usages =="
rg -n "def create_namespace|create_namespace|flbas" avocado/utils/nvme.py avocado -g '*.py' | head -120

echo "== inspect call sites =="
python3 - <<'PY'
import ast, pathlib
p=pathlib.Path('avocado/utils/nvme.py')
tree=ast.parse(p.read_text())
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        if isinstance(node.func, ast.Attribute) and node.func.attr == 'create_namespace':
            print(p, node.lineno, ast.get_source_segment(p.read_text(), node))
PY

echo "== behavioral probe: process.system shell=True interpolation =="
python3 - <<'PY'
import ast
PY

Repository: avocado-framework/avocado

Length of output: 9226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect shell escaping behavior for candidate unvalidated inputs =="
python3 - <<'PY'
from shlex import join
inputs = ["0", "0; command", "0 --nsze=1", "0 && id", "-1", "abc", "16", "0; rm -rf /"]
cmds = [f"nvme create-ns /dev/controller --nsze={x} --ncap={x} --flbas={x} --dps=0" for x in inputs]
for x, cmd in zip(inputs, cmds):
    print(f"input={x!r} command={cmd!r}")
PY

Repository: avocado-framework/avocado

Length of output: 1082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect create_namespace wrapper =="
sed -n '630,665p' avocado/utils/nvme.py

echo "== inspect create-ns usage in avocado tests/usages =="
rg -n "create-one-ns|create_one_ns|create-namespace|create_namespace|--flbas=|flbas=\[" avocado -g '*.py' | head -200

echo "== process.system implementation =="
rg -n "def system|class .*Process|shell=True|system32|subprocess|check_output" avocado/utils process.py avocado/utils/process.py -g '*.py' | head -80

echo "== targeted AST for create-one-ns signature/body =="
python3 - <<'PY'
import ast
from pathlib import Path
src = Path('avocado/utils/nvme.py').read_text()
tree = ast.parse(src)
for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef) and node.name == 'create_one_ns':
        print(node.lineno, node.argnames)
        for stmt in node.body:
            if isinstance(stmt, (ast.If, ast.Try, ast.Raise, ast.Assign, ast.Call, ast.Expr)):
                start, end = stmt.lineno, getattr(stmt, 'end_lineno', stmt.lineno)
                print(f"{start}-{end}: {ast.dump(stmt)[:200]}")
PY

echo "== Python shell interpolation demonstration =="
python3 - <<'PY'
flbas = "0; rm -rf /"
cmd = f"nvme create-ns /dev/controller --nsze=100 --ncap=100 --flbas={flbas} --dps=0"
print(cmd)
print(repr(cmd))
PY

Repository: avocado-framework/avocado

Length of output: 9341


Validate flbas before building the shell command.

create_one_ns() accepts arbitrary flbas values and interpolates them directly into process.system(..., shell=True). A caller can pass a string containing shell syntax, so reject non-integers and integers outside -1..15 before command construction.

Proposed fix
+    if (
+        isinstance(flbas, bool)
+        or not isinstance(flbas, int)
+        or not -1 <= flbas <= 15
+    ):
+        raise NvmeException("flbas must be an integer from -1 through 15")
+
     # Determine retry strategy based on flbas parameter
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Determine retry strategy based on flbas parameter
# Only enable auto-detection fallback for default flbas=0
should_retry_with_auto_detect = flbas == 0
# Handle explicit auto-detection request (flbas=-1)
# This skips the FLBAS=0 attempt and goes straight to optimal detection
if flbas == -1:
try:
flbas = get_optimal_flbas(controller_name)
LOGGER.info(
f"Auto-detected FLBAS={flbas} for namespace creation on {controller_name}"
)
except NvmeException as e:
LOGGER.error(f"Failed to auto-detect FLBAS: {e}")
raise NvmeException(
f"Cannot create namespace: FLBAS auto-detection failed: {e}"
)
# Build and execute namespace creation command
cmd = f"nvme create-ns /dev/{controller_name} --nsze={ns_size} --ncap={ns_size} --flbas={flbas} --dps=0"
if shared_ns:
cmd = f"{cmd} -m 1"
if process.system(cmd, shell=True, ignore_status=True):
raise NvmeException(f"namespace create command failed {cmd}")
result = process.system(cmd, shell=True, ignore_status=True)
if (
isinstance(flbas, bool)
or not isinstance(flbas, int)
or not -1 <= flbas <= 15
):
raise NvmeException("flbas must be an integer from -1 through 15")
# Determine retry strategy based on flbas parameter
# Only enable auto-detection fallback for default flbas=0
should_retry_with_auto_detect = flbas == 0
# Handle explicit auto-detection request (flbas=-1)
# This skips the FLBAS=0 attempt and goes straight to optimal detection
if flbas == -1:
try:
flbas = get_optimal_flbas(controller_name)
LOGGER.info(
f"Auto-detected FLBAS={flbas} for namespace creation on {controller_name}"
)
except NvmeException as e:
LOGGER.error(f"Failed to auto-detect FLBAS: {e}")
raise NvmeException(
f"Cannot create namespace: FLBAS auto-detection failed: {e}"
)
# Build and execute namespace creation command
cmd = f"nvme create-ns /dev/{controller_name} --nsze={ns_size} --ncap={ns_size} --flbas={flbas} --dps=0"
if shared_ns:
cmd = f"{cmd} -m 1"
result = process.system(cmd, shell=True, ignore_status=True)
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 524-526: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[error] 533-533: Function call with shell=True parameter identified, security issue

(S604)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/nvme.py` around lines 510 - 533, Validate flbas at the start of
create_one_ns(), before should_retry_with_auto_detect or command construction,
accepting only integers in the inclusive range -1..15 and rejecting strings or
other non-integer values. Raise the existing appropriate exception for invalid
input, while preserving the special -1 auto-detection path and normal handling
of valid values.

Source: Linters/SAST tools

Comment thread avocado/utils/podman.py
Comment on lines +1067 to +1071
if use_jq:
curl_cmd_str = " ".join(
[f"'{arg}'" if " " in arg else arg for arg in curl_cmd]
)
full_cmd = f"{curl_cmd_str} | jq"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Hand-rolled shell quoting in both send_vllm_inference_request methods. Both methods quote an argument only when it contains a space, and they use single quotes. payload_json contains double quotes, and prompt is caller-controlled. A prompt with a single quote ends the quoting and the remaining text runs as shell code. Use shlex.join for the argument vector.

  • avocado/utils/podman.py#L1067-L1071: replace the manual " ".join([f"'{arg}'" ...]) construction with shlex.join(curl_cmd), and pass the result through ["sh", "-c", ...] instead of shell=True.
  • avocado/utils/podman.py#L1806-L1809: replace the identical manual construction with shlex.join(curl_cmd) before you pass full_cmd_str to asyncio.create_subprocess_shell.
📍 Affects 1 file
  • avocado/utils/podman.py#L1067-L1071 (this comment)
  • avocado/utils/podman.py#L1806-L1809
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1067 - 1071, Replace the manual
argument quoting in both send_vllm_inference_request methods: at
avocado/utils/podman.py lines 1067-1071, use shlex.join(curl_cmd) and execute
the jq pipeline via ["sh", "-c", ...] instead of shell=True; at lines 1806-1809,
use shlex.join(curl_cmd) before passing full_cmd_str to
asyncio.create_subprocess_shell. Ensure shlex is imported and preserve the
existing jq pipeline behavior.

Source: Linters/SAST tools

Comment thread avocado/utils/podman.py
Comment on lines +1198 to +1242
def login(
self,
registry,
username=None,
password=None,
api_key=None,
api_key_username="iamapikey",
password_stdin=False,
user=None,
):
"""Login to a container registry.

:param str registry: Registry URL.
:param str username: Username for authentication (optional if using API key).
:param str password: Password for authentication (optional if using API key).
:param str api_key: API key for authentication (alternative to username/password).
:param str api_key_username: Username to use with API key authentication (default: "iamapikey" for IBM Cloud).
Other registries may use different conventions (e.g., "oauth2accesstoken" for GCR).
:param bool password_stdin: If True, read password from stdin.
:param str user: Optional system user to run podman command as.
:rtype: tuple with returncode, stdout, stderr.
"""
try:
args = ["login"]

if api_key:
args.extend(["--username", username or api_key_username])
args.extend(["--password", api_key])
elif username and password:
args.extend(["--username", username])
if not password_stdin:
args.extend(["--password", password])
elif password_stdin:
if username:
args.extend(["--username", username])
args.append("--password-stdin")
else:
raise PodmanException(
"Must provide either api_key, username/password, or password_stdin"
)

args.append(registry)
return self.execute(*args, user=user)
except PodmanException as ex:
raise PodmanException(f"Failed to login to registry {registry}.") from ex

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Do not pass the API key or password on the command line.

Line 1225 and Line 1229 append --password with the secret value. The full argument vector is visible in /proc/<pid>/cmdline to other users on the host. When user is set, Podman.execute also builds a single command string for su -c, so the secret is stored in the shell history of the target account and in the su process arguments.

Also, Podman.execute logs the arguments at Line 621 with LOG.debug("Executing %s", args), so the secret reaches the debug log.

Use --password-stdin for every credential path and write the secret to the process stdin.

Also applies to: 1923-1967

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 1198 - 1242, Update Podman.login to
never place api_key or password in the argument list; use --password-stdin for
both credential paths and provide the selected secret through the subprocess
stdin, including when user is set. Preserve username and api_key_username
handling, and update the relevant Podman.execute flow so stdin is passed safely
without exposing the secret in command strings or debug logs; apply the same
change to the other credential-login implementation around the referenced
symbol.

Comment thread avocado/utils/podman.py
Comment on lines +2000 to +2066
async def get_container_port(self, container_id, port=8000, user=None):
"""
Get the actual host port mapped to a container port.

:param container_id: Container ID
:param port: Container port to check (default: 8000)
:param user: Username if container was created by specific user
:return: Host port number or None
"""
return get_container_port(container_id, port, user, LOG)

async def save_container_logs(
self, container_id, log_dir, test_name="test", user=None
):
"""
Save complete container logs to a file.

:param container_id: Container ID
:param log_dir: Directory to save logs
:param test_name: Test name for log file naming (default: "test")
:param user: Username if container was created by specific user
:return: Path to saved log file or None
"""
return save_container_logs(container_id, log_dir, test_name, user, LOG)

async def wait_for_vllm_startup(
self,
container_id,
success_pattern="Application startup complete.",
failure_pattern=None,
additional_failure_checks=None,
timeout=300,
check_interval=10,
user=None,
show_live_logs=True,
live_log_lines=10,
):
"""
Async method: Wait for container to start by checking logs for a success pattern.

This is a generic async method that can be used for any container type.
The success and failure patterns can be customized based on the application.

:param container_id: Container ID to monitor
:param success_pattern: String pattern to look for in logs indicating successful startup
:param failure_pattern: Optional string pattern indicating startup failure (e.g., "BACKTRACE")
:param additional_failure_checks: Optional list of tuples [(pattern, case_sensitive), ...]
for additional failure detection. Example:
[("VFIO", False), ("fail", False)] checks for "VFIO" and "fail"
:param timeout: Maximum time to wait in seconds (default: 300)
:param check_interval: Time between log checks in seconds (default: 10)
:param user: Username if container was created by specific user
:param show_live_logs: If True, display recent log lines during each check (default: True)
:param live_log_lines: Number of recent log lines to display (default: 10)
:return: True if startup successful, False otherwise
"""
return wait_for_vllm_startup(
container_id,
success_pattern,
failure_pattern,
additional_failure_checks,
timeout,
check_interval,
user,
show_live_logs,
live_log_lines,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

These async def methods block the event loop.

AsyncPodman.get_container_port, AsyncPodman.save_container_logs, and AsyncPodman.wait_for_vllm_startup are coroutines, but each one calls the blocking module-level function directly. No await occurs. wait_for_vllm_startup calls time.sleep in a loop and can block for the full timeout, which defaults to 300 seconds. Every other task on the same event loop stops during that time.

avocado/plugins/runners/podman_image.py runs AsyncPodman inside asyncio.run, so an event loop is always active for this class.

Offload the calls with asyncio.to_thread.

🐛 Proposed fix
     async def get_container_port(self, container_id, port=8000, user=None):
@@
-        return get_container_port(container_id, port, user, LOG)
+        return await asyncio.to_thread(
+            get_container_port, container_id, port, user, LOG
+        )
@@
-        return save_container_logs(container_id, log_dir, test_name, user, LOG)
+        return await asyncio.to_thread(
+            save_container_logs, container_id, log_dir, test_name, user, LOG
+        )
@@
-        return wait_for_vllm_startup(
-            container_id,
-            success_pattern,
-            failure_pattern,
-            additional_failure_checks,
-            timeout,
-            check_interval,
-            user,
-            show_live_logs,
-            live_log_lines,
-        )
+        return await asyncio.to_thread(
+            wait_for_vllm_startup,
+            container_id,
+            success_pattern,
+            failure_pattern,
+            additional_failure_checks,
+            timeout,
+            check_interval,
+            user,
+            show_live_logs,
+            live_log_lines,
+        )

Note that wait_for_vllm_startup at Line 235 also accepts a log parameter after user. The positional call at Line 2056 passes show_live_logs into the log parameter and live_log_lines into show_live_logs. The live_log_lines value is never passed. Use keyword arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/utils/podman.py` around lines 2000 - 2066, Update
AsyncPodman.get_container_port, save_container_logs, and wait_for_vllm_startup
to await asyncio.to_thread when invoking their blocking module-level functions,
preserving each method’s current arguments and return values. In
wait_for_vllm_startup, pass arguments by keyword so the underlying log parameter
receives LOG and show_live_logs/live_log_lines map to their intended parameters.

Comment on lines +151 to +155
for _ in range(10):
status, output = RemoteSpawner.run_remote_cmd(
session,
f"pgrep -r R,S -f 'task-run -i {runtime_task.task.identifier}'",
10,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Quote the task identifier as one shell argument.

Line 154 interpolates runtime_task.task.identifier inside single quotes. Runnable identifiers can derive from user-controlled URIs or identifier formats. A single quote in the identifier terminates the quote and permits additional remote shell syntax.

Build the complete pgrep -f pattern first. Then pass it through shlex.quote().

Proposed fix
+            pattern = f"task-run -i {runtime_task.task.identifier}"
             status, output = RemoteSpawner.run_remote_cmd(
                 session,
-                f"pgrep -r R,S -f 'task-run -i {runtime_task.task.identifier}'",
+                f"pgrep -r R,S -f {shlex.quote(pattern)}",
                 10,
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@optional_plugins/spawner_remote/avocado_spawner_remote/__init__.py` around
lines 151 - 155, Update the remote command construction in the run_remote_cmd
loop to build the complete pgrep pattern containing
runtime_task.task.identifier, then quote that full pattern with shlex.quote
before interpolating it into the shell command. Add the required shlex import
and preserve the existing pgrep behavior and arguments.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@avocado/core/status/server.py`:
- Around line 28-33: Update the status-server port allocation around
find_free_port and StatusServer.create_server to bind and retain the listening
socket while scanning candidate ports, rather than returning only a number.
Propagate the resolved endpoint only after a bind succeeds, and ensure the
retained socket is used by the server so concurrent processes cannot select the
same port.

In `@avocado/plugins/runner_nrunner.py`:
- Around line 205-207: Update _determine_status_server to check
run.status_server_auto and, when enabled, ignore configured endpoint values in
favor of the default status-server range for both listen and URI resolution.
After resolving the endpoint, write the same concrete value back to both
run.status_server_listen and run.status_server_uri so RuntimeTaskGraph and the
server use the identical endpoint; preserve configured values when automatic
mode is disabled.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c87a8d8-95e1-49aa-b729-0323d9112219

📥 Commits

Reviewing files that changed from the base of the PR and between f44dfb8 and c7c50d5.

📒 Files selected for processing (2)
  • avocado/core/status/server.py
  • avocado/plugins/runner_nrunner.py

Comment on lines +28 to +33
port = network_ports.find_free_port(
start_port=start,
end_port=end,
address=host,
sequent=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bind the selected port before publishing it.

find_free_port() returns only a port number. It does not retain a listening socket. Another Avocado process can bind the selected port before StatusServer.create_server() runs. Two concurrent runs can then select the same port, and one status server fails to start.

Bind and retain the listening socket while scanning the range. Propagate the resolved endpoint only after the bind succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/core/status/server.py` around lines 28 - 33, Update the status-server
port allocation around find_free_port and StatusServer.create_server to bind and
retain the listening socket while scanning candidate ports, rather than
returning only a number. Propagate the resolved endpoint only after a bind
succeeds, and ensure the retained socket is used by the server so concurrent
processes cannot select the same port.

Comment on lines 205 to 207
def _determine_status_server(self, test_suite, config_key):
if test_suite.config.get("run.status_server_auto"):
# no UNIX domain sockets on Windows
if platform.system() != "Windows":
if self.status_server_dir is None:
self.status_server_dir = tempfile.TemporaryDirectory(
prefix="avocado_"
)
return os.path.join(self.status_server_dir.name, ".status_server.sock")
"""Return listen/uri config; default is a port range so multiple runs work."""
return test_suite.config.get(config_key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Honor run.status_server_auto when selecting the endpoint.

When run.status_server_auto is True, this method still returns configured values. For example, a custom run.status_server_uri makes RuntimeTaskGraph send status messages to that custom URI while the server resolves run.status_server_listen. This contradicts the option help and can disconnect tasks from the status server.

When automatic mode is enabled, select the default range for both values. After resolution, update both configuration keys to the same concrete endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@avocado/plugins/runner_nrunner.py` around lines 205 - 207, Update
_determine_status_server to check run.status_server_auto and, when enabled,
ignore configured endpoint values in favor of the default status-server range
for both listen and URI resolution. After resolving the endpoint, write the same
concrete value back to both run.status_server_listen and run.status_server_uri
so RuntimeTaskGraph and the server use the identical endpoint; preserve
configured values when automatic mode is disabled.

Signed-off-by: xianglongfei_uniontech <xianglongfei@uniontech.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Review Requested

Development

Successfully merging this pull request may close these issues.

3 participants