saltstack / saltstack/salt

[Bug]: SyncClientMixin.low() masks all early failures with UnboundLocalError: proc_fn

Open Beginner friendly
#70,252 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug needs-triage
Dominant language
Python
Stars
15.7k
Forks
5.6k
Avg merge
2d 44m
Merged PRs (30d)
80

Description

What happened?

In salt/client/mixins.py::SyncClientMixin.low(), proc_fn is assigned at line 381, but the finally block at line 415 unconditionally references it at line 418:

Anything raising between lines 339 and 380 is correctly captured by the broad except at 404 — and then discarded, because the finally raises UnboundLocalError on its way out. Affected call sites include verify_fun() (line 340), the func_globals injection loop, salt.utils.args.format_call(), and jid_event.fire_event(data, "new").

Two consequences:

The real exception is never logged or returned. Callers see only UnboundLocalError: cannot access local variable 'proc_fn' where it is not associated with a value, which points at cleanup code unrelated to the actual fault.
Because the finally raises, execution never reaches the store_job() call or namespaced_event.fire_event(data, "ret") that follow it, so the failure is absent from the job cache and the event bus too.
Note that a permissions failure on the proc file itself does not trigger this — proc_fn is already bound by the time fopen() runs at line 382. The UnboundLocalError is always a symptom of an earlier, hidden fault, which makes it actively misleading to diagnose.

Any master. Reproducible with a plain RunnerClient; no special configuration required.

Steps to reproduce

import salt.config
import salt.runner

opts = salt.config.master_config("/etc/salt/master")
client = salt.runner.RunnerClient(opts)
client.low("nonexistent.function", {"fun": "nonexistent.function"})

The control flow in isolation:

import os

def low(fail_early):
try:
if fail_early:
raise RuntimeError("the real error")
proc_fn = os.path.join("/tmp", "proc", "jid")
open(proc_fn, "w+b")
except (Exception, SystemExit) as ex:
print("broad except captured:", ex) # real error captured here...
finally:
try:
os.remove(proc_fn) # ...then destroyed here
except OSError:
pass

low(True)

broad except captured: the real error

UnboundLocalError: local variable 'proc_fn' referenced before assignment

Expected behavior

verify_fun() raises CommandExecutionError: 'nonexistent.function' is not available., and low() returns it in data["return"] as the broad except intends.

Actual behavior

UnboundLocalError: cannot access local variable 'proc_fn' where it is not associated with a value

Suggested fix

         try:
  •            proc_fn = None
               self_functions = copy.copy(self.functions)
               salt.utils.lazy.verify_fun(self_functions, fun)
    

@@
finally:
# Job has finished or issue found, so let's clean up after ourselves

  •            try:
    
  •                os.remove(proc_fn)
    
  •            except OSError as err:
    
  •                log.debug("Error attempting to remove master job tracker: %s", err)
    
  •            if proc_fn is not None:
    
  •                try:
    
  •                    os.remove(proc_fn)
    
  •                except OSError as err:
    
  •                    log.debug("Error attempting to remove master job tracker: %s", err)
    
Type of salt install

Official pkg

Major version

3008.x

What supported OS are you seeing the problem on? Can select multiple. (If bug appears on an unsupported OS, please open a GitHub Discussion instead)

ubuntu-22.04

salt --versions-report output
Salt Version:
            Salt: 3008.2

Python Version:
          Python: 3.14.6 (main, Jun 11 2026, 02:19:05) [GCC 11.2.0]

Dependency Versions:
            cffi: 2.0.0
        cherrypy: 18.10.0
    cryptography: 48.0.0
        dateutil: 2.9.0.post0
       docker-py: Not Installed
           gitdb: 4.0.12
       gitpython: 3.1.50
          Jinja2: 3.1.6
         libgit2: Not Installed
    looseversion: 1.3.0
        M2Crypto: Not Installed
            Mako: Not Installed
         msgpack: 1.1.2
    msgpack-pure: Not Installed
    mysql-python: Not Installed
       packaging: 24.0
       pycparser: 3.00
        pycrypto: 3.23.0
    pycryptodome: 3.23.0
          pygit2: Not Installed
    python-gnupg: 0.5.6
          PyYAML: 6.0.3
           PyZMQ: 27.1.0
          relenv: 0.22.14
           smmap: 5.0.2
         timelib: 0.3.0
         Tornado: 6.5.7
             ZMQ: 4.3.5

Salt Extensions:
 saltext.azurerm: 4.4.1

Salt Package Information:
    Package Type: onedir

System Versions:
            dist: ubuntu 22.04.5 jammy
          locale: utf-8
         machine: x86_64
         release: 6.8.0-1053-aws
          system: Linux
         version: Ubuntu 22.04.5 jammy

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in salt/client/mixins.py, especially SyncClientMixin.low() around lines 338-421, and reproduce the plain RunnerClient example with a nonexistent function. Trace the early-failure and cleanup paths, then verify that the original CommandExecutionError is returned in data["return"] rather than being replaced by UnboundLocalError, with job and event handling still reached.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.