Bug Report
dvc.utils.env2bool decides truthiness with an unanchored re.search:
def env2bool(var, undefined=False):
var = os.getenv(var, None)
if var is None:
return undefined
return bool(re.search("1|y|yes|true", var, flags=re.IGNORECASE))
Because search matches anywhere in the string, any value that merely contains 1, y, yes or true is treated as true — including values that plainly aren't boolean:
'1' -> True # intended
'yes' -> True # intended
'true' -> True # intended
'0' -> False # intended
'no' -> False # intended
'false' -> False # intended
'off' -> False # intended
'my_path' -> True # contains "y"
'anything' -> True # contains "y"
'only' -> True # contains "y"
'v1.0' -> True # contains "1"
So a variable set to a path, a branch name, a version string, or anything else containing a y or a 1 silently reads as enabled.
Impact
env2bool is used for user-facing environment variables, so the input really is arbitrary user text:
DVC_EXP_AUTO_PUSH — dvc/repo/experiments/executor/base.py
DVC_SQLALCHEMY_ECHO — dvc/database.py
DVC_IGNORE_ISATTY — dvc/progress.py
DVC_TEST — dvc/analytics.py, dvc/repo/experiments/push.py, dvc/updater.py
The most likely way to hit it is a value meant to be negative that happens to contain a matching letter (deny, nay, not_yet all read as true), or someone putting a non-boolean value in one of these by mistake and getting the feature switched on rather than an error or a sensible default.
It's also asymmetric in a confusing way: no is correctly false, but nay is true.
Expected
Only a recognised affirmative value should be true — i.e. match the whole string, not a substring. Something like
return var.strip().lower() in {"1", "y", "yes", "true"}
or re.fullmatch(r"1|y|yes|true", var.strip(), flags=re.IGNORECASE) if you'd rather keep the regex. Note that with fullmatch the yes alternative is still needed (it isn't reachable via y any more), which is presumably why search looked sufficient originally.
Reproduce
import os
from dvc.utils import env2bool
os.environ["DVC_EXP_AUTO_PUSH"] = "my_branch"
print(env2bool("DVC_EXP_AUTO_PUSH")) # True
Environment
DVC main (56e5982), Python 3.11.9, Windows.
Happy to send a PR with the fullmatch/set-membership version and a test if you agree with the direction — flagging first since tightening this is technically a behaviour change for anyone currently relying on a loose value.
Bug Report
dvc.utils.env2booldecides truthiness with an unanchoredre.search:Because
searchmatches anywhere in the string, any value that merely contains1,y,yesortrueis treated as true — including values that plainly aren't boolean:So a variable set to a path, a branch name, a version string, or anything else containing a
yor a1silently reads as enabled.Impact
env2boolis used for user-facing environment variables, so the input really is arbitrary user text:DVC_EXP_AUTO_PUSH—dvc/repo/experiments/executor/base.pyDVC_SQLALCHEMY_ECHO—dvc/database.pyDVC_IGNORE_ISATTY—dvc/progress.pyDVC_TEST—dvc/analytics.py,dvc/repo/experiments/push.py,dvc/updater.pyThe most likely way to hit it is a value meant to be negative that happens to contain a matching letter (
deny,nay,not_yetall read as true), or someone putting a non-boolean value in one of these by mistake and getting the feature switched on rather than an error or a sensible default.It's also asymmetric in a confusing way:
nois correctly false, butnayis true.Expected
Only a recognised affirmative value should be true — i.e. match the whole string, not a substring. Something like
or
re.fullmatch(r"1|y|yes|true", var.strip(), flags=re.IGNORECASE)if you'd rather keep the regex. Note that withfullmatchtheyesalternative is still needed (it isn't reachable viayany more), which is presumably whysearchlooked sufficient originally.Reproduce
Environment
DVC main (
56e5982), Python 3.11.9, Windows.Happy to send a PR with the
fullmatch/set-membership version and a test if you agree with the direction — flagging first since tightening this is technically a behaviour change for anyone currently relying on a loose value.