Skip to content

Commit d40e683

Browse files
committed
Harden security
- Fix copy-paste bug in _found_dep_loop: use weak_rev_dep (not rev_dep) for imply-related dependency loop messages - Escape CONFIG_ prefix with re.escape() to prevent regex injection via the CONFIG_ environment variable - Optimize _check_undef_syms from O(U*N*R) to O(N*R+U) using a reverse index, and replace string concatenation with "".join() - Skip redundant list rebuilds in _propagate_deps when dep is y (common case for top-level symbols) - Cache MenuNode.referenced as frozenset on first access; safe because menu node properties are immutable after parsing - Extract duplicated _needs_save and _extract_controlling_symbols from menuconfig/guiconfig into shared functions
1 parent bc3e15d commit d40e683

3 files changed

Lines changed: 126 additions & 139 deletions

File tree

‎guiconfig.py‎

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@
9999
TYPE_TO_STR,
100100
standard_kconfig,
101101
standard_config_filename,
102+
_needs_save as _kconf_needs_save,
103+
_extract_controlling_symbols,
102104
)
103105

104106
# If True, use GIF image data embedded in this file instead of separate GIF
@@ -462,28 +464,7 @@ def _load_config():
462464

463465

464466
def _needs_save():
465-
# Returns True if a just-loaded .config file is outdated (would get
466-
# modified when saving)
467-
468-
if _kconf.missing_syms:
469-
# Assignments to undefined symbols in the .config
470-
return True
471-
472-
for sym in _kconf.unique_defined_syms:
473-
if sym.user_value is None:
474-
if sym.config_string:
475-
# Unwritten symbol
476-
return True
477-
elif sym.orig_type in (BOOL, TRISTATE):
478-
if sym.tri_value != sym.user_value:
479-
# Written bool/tristate symbol, new value
480-
return True
481-
elif sym.str_value != sym.user_value:
482-
# Written string/int/hex symbol, new value
483-
return True
484-
485-
# No need to prompt for save
486-
return False
467+
return _kconf_needs_save(_kconf)
487468

488469

489470
def _create_id_to_node():
@@ -1275,35 +1256,6 @@ def _get_force_info(sym):
12751256
return " [{} {}]".format(prefix, shown)
12761257

12771258

1278-
def _extract_controlling_symbols(expr_list):
1279-
# Extracts the primary controlling symbol from each expression string
1280-
# Returns a list of unique symbol names
1281-
#
1282-
# For "A && B", extracts "A" (the symbol doing the select/imply)
1283-
# For "A || B", extracts both "A" and "B"
1284-
# For simple "A", extracts "A"
1285-
#
1286-
# This avoids showing condition symbols as if they're doing the select/imply
1287-
1288-
sym_names = []
1289-
for expr in expr_list:
1290-
# Split on && first - we only want symbols before &&
1291-
# For "FOO && BAR", we want FOO (the selector), not BAR (the condition)
1292-
and_idx = expr.find(" && ")
1293-
primary = expr[:and_idx].strip() if and_idx != -1 else expr.strip()
1294-
1295-
# Now handle || - all parts are equal
1296-
if " || " in primary:
1297-
for part in primary.split(" || "):
1298-
part = part.strip()
1299-
if part and part not in sym_names:
1300-
sym_names.append(part)
1301-
elif primary and primary not in sym_names:
1302-
sym_names.append(primary)
1303-
1304-
return sym_names
1305-
1306-
13071259
def _node_str(node):
13081260
# Returns the string shown to the right of the image (if any) for the node
13091261

‎kconfiglib.py‎

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,10 +1024,12 @@ def _init(
10241024
self.warnings = []
10251025

10261026
self.config_prefix = os.getenv("CONFIG_", "CONFIG_")
1027-
# Regular expressions for parsing .config files
1028-
self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)")
1027+
# Regular expressions for parsing .config files.
1028+
# Escape the prefix to prevent regex injection via CONFIG_ env var.
1029+
escaped_prefix = re.escape(self.config_prefix)
1030+
self._set_match = _re_match(escaped_prefix + r"([^=]+)=(.*)")
10291031
self._unset_match = _re_match(
1030-
r"# {}([^ ]+) is not set".format(self.config_prefix)
1032+
r"# {}([^ ]+) is not set".format(escaped_prefix)
10311033
)
10321034

10331035
self.config_header = os.getenv("KCONFIG_CONFIG_HEADER", "")
@@ -3864,33 +3866,37 @@ def _propagate_deps(self, node, visible_if):
38643866
self._make_and(cur.prompt[1], self._make_and(visible_if, dep)),
38653867
)
38663868

3867-
# Propagate dependencies to defaults
3868-
if cur.defaults:
3869-
cur.defaults = [
3870-
(default, self._make_and(cond, dep), loc)
3871-
for default, cond, loc in cur.defaults
3872-
]
3873-
3874-
# Propagate dependencies to ranges
3875-
if cur.ranges:
3876-
cur.ranges = [
3877-
(low, high, self._make_and(cond, dep), loc)
3878-
for low, high, cond, loc in cur.ranges
3879-
]
3880-
3881-
# Propagate dependencies to selects
3882-
if cur.selects:
3883-
cur.selects = [
3884-
(target, self._make_and(cond, dep), loc)
3885-
for target, cond, loc in cur.selects
3886-
]
3887-
3888-
# Propagate dependencies to implies
3889-
if cur.implies:
3890-
cur.implies = [
3891-
(target, self._make_and(cond, dep), loc)
3892-
for target, cond, loc in cur.implies
3893-
]
3869+
# When dep is y (the common case for top-level symbols),
3870+
# _make_and(cond, dep) returns cond unchanged, so the list
3871+
# comprehensions would rebuild identical lists. Skip them.
3872+
if dep is not self.y:
3873+
# Propagate dependencies to defaults
3874+
if cur.defaults:
3875+
cur.defaults = [
3876+
(default, self._make_and(cond, dep), loc)
3877+
for default, cond, loc in cur.defaults
3878+
]
3879+
3880+
# Propagate dependencies to ranges
3881+
if cur.ranges:
3882+
cur.ranges = [
3883+
(low, high, self._make_and(cond, dep), loc)
3884+
for low, high, cond, loc in cur.ranges
3885+
]
3886+
3887+
# Propagate dependencies to selects
3888+
if cur.selects:
3889+
cur.selects = [
3890+
(target, self._make_and(cond, dep), loc)
3891+
for target, cond, loc in cur.selects
3892+
]
3893+
3894+
# Propagate dependencies to implies
3895+
if cur.implies:
3896+
cur.implies = [
3897+
(target, self._make_and(cond, dep), loc)
3898+
for target, cond, loc in cur.implies
3899+
]
38943900

38953901
elif cur.prompt: # Not a symbol/choice
38963902
# Propagate dependencies to the prompt. 'visible if' is only
@@ -4196,6 +4202,18 @@ def is_num(s):
41964202

41974203
return True
41984204

4205+
# Build a reverse index {symbol -> [nodes]} in a single pass over
4206+
# all nodes, rather than doing a full tree walk per undefined symbol.
4207+
# This reduces the algorithm from O(U*N*R) to O(N*R + U) where U is
4208+
# the number of undefined symbols, N is the number of nodes, and R is
4209+
# the cost of building node.referenced sets.
4210+
sym_to_nodes = {}
4211+
for node in self.node_iter():
4212+
for ref_sym in node.referenced:
4213+
if ref_sym not in sym_to_nodes:
4214+
sym_to_nodes[ref_sym] = []
4215+
sym_to_nodes[ref_sym].append(node)
4216+
41994217
for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)():
42004218
# - sym.nodes empty means the symbol is undefined (has no
42014219
# definition locations)
@@ -4206,13 +4224,14 @@ def is_num(s):
42064224
# - The MODULES symbol always exists
42074225
if not sym.nodes and not is_num(sym.name) and sym.name != "MODULES":
42084226

4209-
msg = "undefined symbol {}:".format(sym.name)
4210-
for node in self.node_iter():
4211-
if sym in node.referenced:
4212-
msg += "\n\n- Referenced at {}:{}:\n\n{}".format(
4227+
parts = ["undefined symbol {}:".format(sym.name)]
4228+
for node in sym_to_nodes.get(sym, ()):
4229+
parts.append(
4230+
"\n\n- Referenced at {}:{}:\n\n{}".format(
42134231
node.loc[0], node.loc[1], node
42144232
)
4215-
self._warn(msg)
4233+
)
4234+
self._warn("".join(parts))
42164235

42174236
def _warn(self, msg, loc=None):
42184237
# For printing general warnings
@@ -6019,6 +6038,8 @@ class MenuNode(object):
60196038
"selects",
60206039
"implies",
60216040
"ranges",
6041+
# Cached referenced set (populated on first access)
6042+
"_cached_referenced",
60226043
)
60236044

60246045
def __init__(self):
@@ -6029,6 +6050,7 @@ def __init__(self):
60296050
self.selects = []
60306051
self.implies = []
60316052
self.ranges = []
6053+
self._cached_referenced = None
60326054

60336055
@property
60346056
def filename(self):
@@ -6088,6 +6110,9 @@ def referenced(self):
60886110
"""
60896111
See the class documentation.
60906112
"""
6113+
if self._cached_referenced is not None:
6114+
return self._cached_referenced
6115+
60916116
# self.dep is included to catch dependencies from a lone 'depends on'
60926117
# when there are no properties to propagate it to
60936118
res = expr_items(self.dep)
@@ -6115,7 +6140,8 @@ def referenced(self):
61156140
res.add(high)
61166141
res |= expr_items(cond)
61176142

6118-
return res
6143+
self._cached_referenced = frozenset(res)
6144+
return self._cached_referenced
61196145

61206146
def __repr__(self):
61216147
"""
@@ -6653,6 +6679,63 @@ def standard_config_filename():
66536679
return os.getenv("KCONFIG_CONFIG", ".config")
66546680

66556681

6682+
def _needs_save(kconf):
6683+
"""
6684+
Returns True if the current configuration state differs from what was
6685+
loaded from the .config file (i.e., saving would modify the .config).
6686+
6687+
Used by menuconfig and guiconfig to determine whether to prompt for save.
6688+
"""
6689+
if kconf.missing_syms:
6690+
# Assignments to undefined symbols in the .config
6691+
return True
6692+
6693+
for sym in kconf.unique_defined_syms:
6694+
if sym.user_value is None:
6695+
if sym.config_string:
6696+
# Unwritten symbol
6697+
return True
6698+
elif sym.orig_type in _BOOL_TRISTATE:
6699+
if sym.tri_value != sym.user_value:
6700+
# Written bool/tristate symbol, new value
6701+
return True
6702+
elif sym.str_value != sym.user_value:
6703+
# Written string/int/hex symbol, new value
6704+
return True
6705+
6706+
return False
6707+
6708+
6709+
def _extract_controlling_symbols(expr_list):
6710+
"""
6711+
Extracts the primary controlling symbol from each expression string.
6712+
Returns a list of unique symbol names.
6713+
6714+
For "A && B", extracts "A" (the symbol doing the select/imply).
6715+
For "A || B", extracts both "A" and "B".
6716+
For simple "A", extracts "A".
6717+
6718+
Used by menuconfig and guiconfig for select/imply display.
6719+
"""
6720+
sym_names = []
6721+
for expr in expr_list:
6722+
# Split on && first - we only want symbols before &&
6723+
# For "FOO && BAR", we want FOO (the selector), not BAR (the condition)
6724+
and_idx = expr.find(" && ")
6725+
primary = expr[:and_idx].strip() if and_idx != -1 else expr.strip()
6726+
6727+
# Now handle || - all parts are equal
6728+
if " || " in primary:
6729+
for part in primary.split(" || "):
6730+
part = part.strip()
6731+
if part and part not in sym_names:
6732+
sym_names.append(part)
6733+
elif primary and primary not in sym_names:
6734+
sym_names.append(primary)
6735+
6736+
return sym_names
6737+
6738+
66566739
def load_allconfig(kconf, filename):
66576740
"""
66586741
Use Kconfig.load_allconfig() instead, which was added in Kconfiglib 13.4.0.
@@ -7133,7 +7216,7 @@ def _found_dep_loop(loop, cur):
71337216

71347217
if item.weak_rev_dep is not item.kconfig.n:
71357218
msg += "(imply-related dependencies: {})\n\n".format(
7136-
expr_str(item.rev_dep)
7219+
expr_str(item.weak_rev_dep)
71377220
)
71387221

71397222
msg += "...depends again on " + loop[0].name_and_loc

‎menuconfig.py‎

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@
219219
TYPE_TO_STR,
220220
standard_kconfig,
221221
standard_config_filename,
222+
_needs_save as _kconf_needs_save,
223+
_extract_controlling_symbols,
222224
)
223225

224226
#
@@ -860,28 +862,7 @@ def _load_config():
860862

861863

862864
def _needs_save():
863-
# Returns True if a just-loaded .config file is outdated (would get
864-
# modified when saving)
865-
866-
if _kconf.missing_syms:
867-
# Assignments to undefined symbols in the .config
868-
return True
869-
870-
for sym in _kconf.unique_defined_syms:
871-
if sym.user_value is None:
872-
if sym.config_string:
873-
# Unwritten symbol
874-
return True
875-
elif sym.orig_type in (BOOL, TRISTATE):
876-
if sym.tri_value != sym.user_value:
877-
# Written bool/tristate symbol, new value
878-
return True
879-
elif sym.str_value != sym.user_value:
880-
# Written string/int/hex symbol, new value
881-
return True
882-
883-
# No need to prompt for save
884-
return False
865+
return _kconf_needs_save(_kconf)
885866

886867

887868
# Global variables used below:
@@ -3697,35 +3678,6 @@ def _get_force_info(sym):
36973678
return " [{} {}, +{}]".format(prefix, ", ".join(sym_names[:2]), len(sym_names) - 2)
36983679

36993680

3700-
def _extract_controlling_symbols(expr_list):
3701-
# Extracts the primary controlling symbol from each expression string
3702-
# Returns a list of unique symbol names
3703-
#
3704-
# For "A && B", extracts "A" (the symbol doing the select/imply)
3705-
# For "A || B", extracts both "A" and "B"
3706-
# For simple "A", extracts "A"
3707-
#
3708-
# This avoids showing condition symbols as if they're doing the select/imply
3709-
3710-
sym_names = []
3711-
for expr in expr_list:
3712-
# Split on && first - we only want symbols before &&
3713-
# For "FOO && BAR", we want FOO (the selector), not BAR (the condition)
3714-
and_idx = expr.find(" && ")
3715-
primary = expr[:and_idx].strip() if and_idx != -1 else expr.strip()
3716-
3717-
# Now handle || - all parts are equal
3718-
if " || " in primary:
3719-
for part in primary.split(" || "):
3720-
part = part.strip()
3721-
if part and part not in sym_names:
3722-
sym_names.append(part)
3723-
elif primary and primary not in sym_names:
3724-
sym_names.append(primary)
3725-
3726-
return sym_names
3727-
3728-
37293681
def _node_str(node):
37303682
# Returns the complete menu entry text for a menu node.
37313683
#

0 commit comments

Comments
 (0)